@letsprogram/ng-oat 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,39 +1,39 @@
1
1
  import * as i0 from '@angular/core';
2
- import { makeEnvironmentProviders, provideAppInitializer, inject, PLATFORM_ID, Injectable, ElementRef, DestroyRef, signal, output, afterNextRender, Directive, InjectionToken, Renderer2, ViewContainerRef, input, TemplateRef, effect, computed, Component, model, CUSTOM_ELEMENTS_SCHEMA, viewChild, viewChildren, contentChildren } from '@angular/core';
3
- import { DOCUMENT, isPlatformBrowser } from '@angular/common';
2
+ import { makeEnvironmentProviders, provideAppInitializer, inject, PLATFORM_ID, Injectable, ElementRef, DestroyRef, signal, output, afterNextRender, Directive, InjectionToken, Renderer2, ViewContainerRef, input, TemplateRef, effect, computed, Component, model, CUSTOM_ELEMENTS_SCHEMA, viewChild, viewChildren, contentChildren, contentChild } from '@angular/core';
3
+ import { DOCUMENT, isPlatformBrowser, NgTemplateOutlet, NgSwitch, NgSwitchCase, NgSwitchDefault } from '@angular/common';
4
4
 
5
5
  const DEFAULT_OPTIONS = {
6
- assets: { css: false, js: false },
7
- registerWebComponents: true,
6
+ assets: { css: false },
8
7
  basePath: 'assets/oat',
9
8
  };
10
- function injectTag(doc, tag, attrs, id) {
9
+ function injectTag(doc, attrs, id) {
11
10
  return new Promise((resolve, reject) => {
12
11
  if (doc.getElementById(id)) {
13
12
  resolve();
14
13
  return;
15
14
  }
16
- const el = doc.createElement(tag);
15
+ const el = doc.createElement('link');
17
16
  el.id = id;
18
17
  Object.entries(attrs).forEach(([k, v]) => el.setAttribute(k, v));
19
18
  el.addEventListener('load', () => resolve());
20
- el.addEventListener('error', () => reject(new Error(`Failed to load ${tag}: ${attrs['href'] || attrs['src']}`)));
19
+ el.addEventListener('error', () => reject(new Error(`Failed to load link: ${attrs['href']}`)));
21
20
  doc.head.appendChild(el);
22
21
  });
23
22
  }
24
23
  /**
25
- * Provides Oat UI core initialisation.
24
+ * Provides ng-oat core initialisation.
26
25
  *
27
- * By default, CSS and JS are **not** injected at runtime — add them via
28
- * `angular.json` `styles` / `scripts` arrays instead (recommended).
26
+ * By default, no runtime asset injection occurs — add CSS via
27
+ * `angular.json` `styles` array or `@import` in `styles.css` (recommended).
28
+ * Oat JS is no longer needed; all behavior is handled natively by Angular components.
29
29
  *
30
30
  * ```ts
31
- * provideNgOat() // recommended (CSS/JS via angular.json)
32
- * provideNgOat({ assets: { css: 'link', js: 'script' } }) // opt-in runtime injection *
31
+ * provideNgOat() // recommended (CSS via angular.json / styles.css)
32
+ * provideNgOat({ assets: { css: 'link' } }) // opt-in runtime CSS injection *
33
33
  * ```
34
34
  *
35
- * \* Runtime injection requires an assets glob in angular.json to copy files:
36
- * `{ glob: '**\/*', input: 'node_modules/@letsprogram/ng-oat/assets/oat', output: '/assets/oat' }`
35
+ * \\* Runtime injection requires an assets glob in angular.json to copy files:
36
+ * `{ glob: '**\\/*', input: 'node_modules/@letsprogram/ng-oat/assets/oat', output: '/assets/oat' }`
37
37
  */
38
38
  function provideNgOat(options) {
39
39
  const opts = {
@@ -49,17 +49,12 @@ function provideNgOat(options) {
49
49
  return;
50
50
  const base = opts.basePath.replace(/\/+$/, '');
51
51
  const promises = [];
52
- // CSS
52
+ // CSS — only when explicitly opted in
53
53
  if (opts.assets.css === 'link') {
54
- promises.push(injectTag(doc, 'link', { rel: 'stylesheet', href: `${base}/oat.min.css` }, 'ng-oat-css'));
55
- // Also inject the token layer CSS
56
- promises.push(injectTag(doc, 'link', { rel: 'stylesheet', href: `${base}/tokens.css` }, 'ng-oat-tokens-css'));
54
+ promises.push(injectTag(doc, { rel: 'stylesheet', href: `${base}/oat.min.css` }, 'ng-oat-css'));
55
+ promises.push(injectTag(doc, { rel: 'stylesheet', href: `${base}/tokens.css` }, 'ng-oat-tokens-css'));
57
56
  }
58
- // JS
59
- if (opts.assets.js === 'script') {
60
- promises.push(injectTag(doc, 'script', { src: `${base}/oat.min.js`, defer: '' }, 'ng-oat-js'));
61
- }
62
- return Promise.all(promises).then(() => { });
57
+ return promises.length ? Promise.all(promises).then(() => { }) : undefined;
63
58
  }),
64
59
  ]);
65
60
  }
@@ -681,7 +676,7 @@ class NgOatSidebar {
681
676
  this.close();
682
677
  }
683
678
  });
684
- // Listen for toggle clicks within our layout (in case oat.js isn't loaded)
679
+ // Listen for toggle clicks within our layout
685
680
  this.listen(host, 'click', (e) => {
686
681
  const toggle = e.target.closest('[data-sidebar-toggle]');
687
682
  if (toggle) {
@@ -749,10 +744,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
749
744
  }], ctorParameters: () => [], propDecorators: { ngOatSidebarScrollLock: [{ type: i0.Input, args: [{ isSignal: true, alias: "ngOatSidebarScrollLock", required: false }] }], ngOatSidebarChange: [{ type: i0.Output, args: ["ngOatSidebarChange"] }] } });
750
745
 
751
746
  /**
752
- * Angular integration for Oat's <ot-tabs> WebComponent.
747
+ * Angular directive that provides native tab behaviour on any element
748
+ * containing `role="tablist"` > `role="tab"` buttons and sibling
749
+ * `role="tabpanel"` containers.
753
750
  *
754
- * Oat's WC handles all ARIA, keyboard nav, and panel toggling.
755
- * This directive provides Angular-friendly signals and event binding.
751
+ * Works without oat.js — handles tab activation, ARIA attributes,
752
+ * panel visibility and full keyboard navigation internally.
756
753
  *
757
754
  * Usage:
758
755
  * ```html
@@ -777,36 +774,98 @@ class NgOatTabs {
777
774
  activeIndex = signal(0, ...(ngDevMode ? [{ debugName: "activeIndex" }] : []));
778
775
  /** Emits on tab change with { index, tab } */
779
776
  ngOatTabChange = output();
780
- tabChangeListener = null;
777
+ tabEls = [];
778
+ panelEls = [];
779
+ cleanupFns = [];
781
780
  constructor() {
782
781
  afterNextRender(() => {
783
782
  if (!isPlatformBrowser(this.platformId))
784
783
  return;
785
784
  const host = this.el.nativeElement;
786
- // Listen for ot-tab-change custom event emitted by the WebComponent
787
- this.tabChangeListener = (e) => {
788
- const detail = e.detail;
789
- if (detail) {
790
- this.activeIndex.set(detail.index);
791
- this.ngOatTabChange.emit({ index: detail.index, tab: detail.tab });
785
+ // Discover tabs and panels
786
+ const tablist = host.querySelector(':scope > [role="tablist"]');
787
+ this.tabEls = tablist
788
+ ? Array.from(tablist.querySelectorAll('[role="tab"]'))
789
+ : [];
790
+ this.panelEls = Array.from(host.querySelectorAll(':scope > [role="tabpanel"]'));
791
+ // Wire ARIA ids
792
+ this.tabEls.forEach((tab, i) => {
793
+ const panel = this.panelEls[i];
794
+ if (!panel)
795
+ return;
796
+ const tabId = tab.id || `ng-oat-tab-${crypto.randomUUID().slice(0, 8)}`;
797
+ const panelId = panel.id || `ng-oat-panel-${crypto.randomUUID().slice(0, 8)}`;
798
+ tab.id = tabId;
799
+ panel.id = panelId;
800
+ tab.setAttribute('aria-controls', panelId);
801
+ panel.setAttribute('aria-labelledby', tabId);
802
+ });
803
+ // Activate the initial tab
804
+ this.activate(this.activeIndex());
805
+ // Click handler
806
+ const onClick = (e) => {
807
+ const btn = e.target.closest?.('[role="tab"]');
808
+ if (!btn)
809
+ return;
810
+ const idx = this.tabEls.indexOf(btn);
811
+ if (idx >= 0 && !btn.hasAttribute('disabled'))
812
+ this.activate(idx);
813
+ };
814
+ // Keyboard navigation (ArrowLeft / ArrowRight / Home / End)
815
+ const onKeydown = (e) => {
816
+ if (!e.target.closest?.('[role="tab"]'))
817
+ return;
818
+ const len = this.tabEls.length;
819
+ const current = this.activeIndex();
820
+ let next = -1;
821
+ if (e.key === 'ArrowRight')
822
+ next = (current + 1) % len;
823
+ else if (e.key === 'ArrowLeft')
824
+ next = (current - 1 + len) % len;
825
+ else if (e.key === 'Home')
826
+ next = 0;
827
+ else if (e.key === 'End')
828
+ next = len - 1;
829
+ if (next >= 0) {
830
+ e.preventDefault();
831
+ // Skip disabled tabs
832
+ const orig = next;
833
+ const dir = e.key === 'ArrowLeft' || e.key === 'End' ? -1 : 1;
834
+ while (this.tabEls[next]?.hasAttribute('disabled')) {
835
+ next = (next + dir + len) % len;
836
+ if (next === orig)
837
+ return; // all disabled
838
+ }
839
+ this.activate(next);
840
+ this.tabEls[next]?.focus();
792
841
  }
793
842
  };
794
- host.addEventListener('ot-tab-change', this.tabChangeListener);
795
- // Sync initial active index
796
- if (typeof host.activeIndex === 'number') {
797
- this.activeIndex.set(host.activeIndex);
798
- }
843
+ tablist?.addEventListener('click', onClick);
844
+ tablist?.addEventListener('keydown', onKeydown);
845
+ this.cleanupFns.push(() => tablist?.removeEventListener('click', onClick), () => tablist?.removeEventListener('keydown', onKeydown));
799
846
  this.destroyRef.onDestroy(() => {
800
- host.removeEventListener('ot-tab-change', this.tabChangeListener);
847
+ this.cleanupFns.forEach(fn => fn());
848
+ this.cleanupFns = [];
801
849
  });
802
850
  });
803
851
  }
804
852
  /** Programmatically select a tab by index */
805
853
  selectTab(index) {
806
- const host = this.el.nativeElement;
807
- if (typeof host.activeIndex === 'number') {
808
- host.activeIndex = index;
809
- }
854
+ this.activate(index);
855
+ }
856
+ activate(idx) {
857
+ if (idx < 0 || idx >= this.tabEls.length)
858
+ return;
859
+ this.tabEls.forEach((tab, i) => {
860
+ const isActive = i === idx;
861
+ tab.setAttribute('aria-selected', String(isActive));
862
+ tab.tabIndex = isActive ? 0 : -1;
863
+ });
864
+ this.panelEls.forEach((panel, i) => {
865
+ panel.hidden = i !== idx;
866
+ });
867
+ this.activeIndex.set(idx);
868
+ this.ngOatTabChange.emit({ index: idx, tab: this.tabEls[idx] });
810
869
  }
811
870
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: NgOatTabs, deps: [], target: i0.ɵɵFactoryTarget.Directive });
812
871
  static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.0", type: NgOatTabs, isStandalone: true, selector: "ot-tabs[ngOatTabs], [ngOatTabs]", outputs: { ngOatTabChange: "ngOatTabChange" }, exportAs: ["ngOatTabs"], ngImport: i0 });
@@ -922,10 +981,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
922
981
  }], ctorParameters: () => [], propDecorators: { ngOatDialogClose: [{ type: i0.Output, args: ["ngOatDialogClose"] }] } });
923
982
 
924
983
  /**
925
- * Angular service wrapping Oat's toast notification system.
984
+ * Angular service for Oat-styled toast notifications.
926
985
  *
927
- * Delegates to `window.ot.toast()` (provided by oat.min.js).
928
- * Falls back to a no-op if Oat JS is not loaded.
986
+ * Fully native implementation no dependency on oat.js.
987
+ * Uses the Oat CSS `.toast`, `.toast-container`, `data-entering/data-exiting`
988
+ * animation patterns, and popover API for stacking.
929
989
  *
930
990
  * Usage:
931
991
  * ```ts
@@ -939,11 +999,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
939
999
  class NgOatToast {
940
1000
  platformId = inject(PLATFORM_ID);
941
1001
  doc = inject(DOCUMENT);
942
- get ot() {
943
- if (!isPlatformBrowser(this.platformId))
944
- return null;
945
- return this.doc.defaultView?.ot ?? null;
946
- }
1002
+ /** Cache of toast containers keyed by placement */
1003
+ containers = new Map();
1004
+ // ── Convenience methods ─────────────────────────────────────────────
947
1005
  success(message, title, options) {
948
1006
  this.show(message, title, { ...options, variant: 'success' });
949
1007
  }
@@ -956,44 +1014,62 @@ class NgOatToast {
956
1014
  error(message, title, options) {
957
1015
  this.show(message, title, { ...options, variant: 'danger' });
958
1016
  }
1017
+ // ── Core show method ────────────────────────────────────────────────
959
1018
  show(message, title, options = {}) {
960
- const ot = this.ot;
961
- if (!ot?.toast) {
962
- console.warn('ng-oat: Oat JS not loaded. Toast not shown. Ensure oat.min.js is loaded via provideNgOat() or angular.json scripts.');
1019
+ if (!isPlatformBrowser(this.platformId))
963
1020
  return;
1021
+ const { variant = 'info', dismissible, ...rest } = options;
1022
+ const el = this.doc.createElement('output');
1023
+ el.setAttribute('data-variant', variant);
1024
+ if (title) {
1025
+ const titleEl = this.doc.createElement('h6');
1026
+ titleEl.className = 'toast-title';
1027
+ titleEl.textContent = title;
1028
+ el.appendChild(titleEl);
964
1029
  }
965
- const { dismissible, ...rest } = options;
966
- const el = ot.toast(message, title, rest);
967
- if (dismissible && el) {
1030
+ const msgEl = this.doc.createElement('div');
1031
+ msgEl.className = 'toast-message';
1032
+ msgEl.textContent = message;
1033
+ el.appendChild(msgEl);
1034
+ if (dismissible) {
968
1035
  this.addCloseButton(el);
969
1036
  }
970
- return el;
1037
+ return this.showEl(el, rest);
971
1038
  }
972
1039
  /**
973
1040
  * Show a toast from a DOM element or template element.
974
- * For Angular TemplateRef, use showTemplate() instead.
975
1041
  */
976
1042
  showElement(element, options = {}) {
977
- const ot = this.ot;
978
- if (!ot?.toast?.el)
1043
+ if (!isPlatformBrowser(this.platformId))
979
1044
  return;
980
- ot.toast.el(element, options);
1045
+ let target;
1046
+ if (element instanceof HTMLTemplateElement) {
1047
+ target = element.content.firstElementChild?.cloneNode(true);
1048
+ }
1049
+ else {
1050
+ target = element.cloneNode(true);
1051
+ }
1052
+ if (!target)
1053
+ return;
1054
+ target.removeAttribute('id');
1055
+ this.showEl(target, options);
981
1056
  }
982
1057
  /**
983
1058
  * Show a toast from an Angular TemplateRef.
984
1059
  * Requires a ViewContainerRef to render the template.
985
1060
  */
986
1061
  showTemplate(templateRef, vcr, options = {}) {
987
- const ot = this.ot;
988
- if (!ot?.toast?.el)
1062
+ if (!isPlatformBrowser(this.platformId))
989
1063
  return;
990
1064
  const view = vcr.createEmbeddedView(templateRef);
991
1065
  view.detectChanges();
992
- // Wrap template nodes in a container
993
1066
  const container = this.doc.createElement('div');
994
1067
  view.rootNodes.forEach((node) => container.appendChild(node));
995
- ot.toast.el(container, options);
996
- // Clean up the view after the toast duration
1068
+ const { dismissible, ...rest } = options;
1069
+ if (dismissible) {
1070
+ this.addCloseButton(container);
1071
+ }
1072
+ this.showEl(container, rest);
997
1073
  const duration = options.duration ?? 4000;
998
1074
  if (duration > 0) {
999
1075
  setTimeout(() => view.destroy(), duration + 500);
@@ -1001,7 +1077,82 @@ class NgOatToast {
1001
1077
  }
1002
1078
  /** Dismiss toasts. If placement given, only that position; otherwise all. */
1003
1079
  dismiss(placement) {
1004
- this.ot?.toast?.clear(placement);
1080
+ if (!isPlatformBrowser(this.platformId))
1081
+ return;
1082
+ if (placement) {
1083
+ const c = this.containers.get(placement);
1084
+ if (c) {
1085
+ c.innerHTML = '';
1086
+ c.hidePopover();
1087
+ }
1088
+ }
1089
+ else {
1090
+ this.containers.forEach(c => {
1091
+ c.innerHTML = '';
1092
+ c.hidePopover();
1093
+ });
1094
+ }
1095
+ }
1096
+ // ── Internal helpers ────────────────────────────────────────────────
1097
+ /** Get or create a toast container for the given placement */
1098
+ getContainer(placement) {
1099
+ let c = this.containers.get(placement);
1100
+ if (!c) {
1101
+ c = this.doc.createElement('div');
1102
+ c.className = 'toast-container';
1103
+ c.setAttribute('popover', 'manual');
1104
+ c.setAttribute('data-placement', placement);
1105
+ this.doc.body.appendChild(c);
1106
+ this.containers.set(placement, c);
1107
+ }
1108
+ return c;
1109
+ }
1110
+ /** Show a prepared element as a toast */
1111
+ showEl(el, options = {}) {
1112
+ const { placement = 'top-right', duration = 4000 } = options;
1113
+ const container = this.getContainer(placement);
1114
+ el.classList.add('toast');
1115
+ let timeout;
1116
+ // Pause auto-dismiss on hover
1117
+ el.onmouseenter = () => clearTimeout(timeout);
1118
+ el.onmouseleave = () => {
1119
+ if (duration > 0) {
1120
+ timeout = setTimeout(() => this.removeToast(el, container), duration);
1121
+ }
1122
+ };
1123
+ // Show with enter animation
1124
+ el.setAttribute('data-entering', '');
1125
+ container.appendChild(el);
1126
+ container.showPopover();
1127
+ // Double rAF so computed styles apply before transition starts
1128
+ requestAnimationFrame(() => {
1129
+ requestAnimationFrame(() => {
1130
+ el.removeAttribute('data-entering');
1131
+ });
1132
+ });
1133
+ // Auto-dismiss
1134
+ if (duration > 0) {
1135
+ timeout = setTimeout(() => this.removeToast(el, container), duration);
1136
+ }
1137
+ return el;
1138
+ }
1139
+ /** Remove a toast with exit animation */
1140
+ removeToast(el, container) {
1141
+ if (el.hasAttribute('data-exiting'))
1142
+ return;
1143
+ el.setAttribute('data-exiting', '');
1144
+ const cleanup = () => {
1145
+ el.remove();
1146
+ if (!container.children.length) {
1147
+ container.hidePopover();
1148
+ }
1149
+ };
1150
+ el.addEventListener('transitionend', cleanup, { once: true });
1151
+ // Fallback timeout for clients that disable animations
1152
+ const t = getComputedStyle(el).getPropertyValue('--transition').trim();
1153
+ const val = parseFloat(t) || 300;
1154
+ const ms = t.endsWith('ms') ? val : val * 1000;
1155
+ setTimeout(cleanup, ms > 0 ? ms : 350);
1005
1156
  }
1006
1157
  /** Add a close button to a toast element */
1007
1158
  addCloseButton(el) {
@@ -1013,10 +1164,8 @@ class NgOatToast {
1013
1164
  btn.addEventListener('mouseenter', () => { btn.style.opacity = '1'; });
1014
1165
  btn.addEventListener('mouseleave', () => { btn.style.opacity = '0.6'; });
1015
1166
  btn.addEventListener('click', () => {
1016
- el.setAttribute('data-exiting', '');
1017
- const cleanup = () => el.remove();
1018
- el.addEventListener('transitionend', cleanup, { once: true });
1019
- setTimeout(cleanup, 350);
1167
+ const container = el.parentElement;
1168
+ this.removeToast(el, container);
1020
1169
  });
1021
1170
  el.style.position = 'relative';
1022
1171
  el.style.paddingRight = '2rem';
@@ -1975,17 +2124,21 @@ class NgOatDropdownComponent {
1975
2124
  const items = host.querySelectorAll('[role="menuitem"]');
1976
2125
  items[0]?.focus();
1977
2126
  this.triggerEl?.setAttribute('aria-expanded', 'true');
2127
+ // Add keyboard navigation
2128
+ this.popoverEl?.addEventListener('keydown', this.onKeydown);
1978
2129
  }
1979
2130
  else {
1980
2131
  window.removeEventListener('scroll', this.positionFn, true);
1981
2132
  window.removeEventListener('resize', this.positionFn);
1982
2133
  this.triggerEl?.setAttribute('aria-expanded', 'false');
1983
2134
  this.triggerEl?.focus();
2135
+ this.popoverEl?.removeEventListener('keydown', this.onKeydown);
1984
2136
  }
1985
2137
  };
1986
2138
  this.popoverEl.addEventListener('toggle', onToggle);
1987
2139
  this.destroyRef.onDestroy(() => {
1988
2140
  this.popoverEl?.removeEventListener('toggle', onToggle);
2141
+ this.popoverEl?.removeEventListener('keydown', this.onKeydown);
1989
2142
  window.removeEventListener('scroll', this.positionFn, true);
1990
2143
  window.removeEventListener('resize', this.positionFn);
1991
2144
  });
@@ -2000,6 +2153,44 @@ class NgOatDropdownComponent {
2000
2153
  toggle() {
2001
2154
  this.popoverEl?.togglePopover?.();
2002
2155
  }
2156
+ /** Keyboard navigation for menu items (ArrowDown/Up/Home/End/Escape) */
2157
+ onKeydown = (e) => {
2158
+ const host = this.elRef.nativeElement;
2159
+ const items = Array.from(host.querySelectorAll('[role="menuitem"]:not([disabled])'));
2160
+ if (!items.length)
2161
+ return;
2162
+ const current = items.indexOf(this.doc.activeElement);
2163
+ switch (e.key) {
2164
+ case 'ArrowDown': {
2165
+ e.preventDefault();
2166
+ const next = current < items.length - 1 ? current + 1 : 0;
2167
+ items[next]?.focus();
2168
+ break;
2169
+ }
2170
+ case 'ArrowUp': {
2171
+ e.preventDefault();
2172
+ const prev = current > 0 ? current - 1 : items.length - 1;
2173
+ items[prev]?.focus();
2174
+ break;
2175
+ }
2176
+ case 'Home': {
2177
+ e.preventDefault();
2178
+ items[0]?.focus();
2179
+ break;
2180
+ }
2181
+ case 'End': {
2182
+ e.preventDefault();
2183
+ items[items.length - 1]?.focus();
2184
+ break;
2185
+ }
2186
+ case 'Escape': {
2187
+ e.preventDefault();
2188
+ this.close();
2189
+ break;
2190
+ }
2191
+ }
2192
+ };
2193
+ doc = inject(DOCUMENT);
2003
2194
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: NgOatDropdownComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2004
2195
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.0", type: NgOatDropdownComponent, isStandalone: true, selector: "ng-oat-dropdown", outputs: { openChange: "openChange" }, ngImport: i0, template: `
2005
2196
  <ot-dropdown>
@@ -4326,6 +4517,455 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
4326
4517
  `, styles: [":host{display:block}\n"] }]
4327
4518
  }], propDecorators: { heading: [{ type: i0.Input, args: [{ isSignal: true, alias: "heading", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: false }] }], showSeeAll: [{ type: i0.Input, args: [{ isSignal: true, alias: "showSeeAll", required: false }] }], scrollAmount: [{ type: i0.Input, args: [{ isSignal: true, alias: "scrollAmount", required: false }] }], seeAllClick: [{ type: i0.Output, args: ["seeAllClick"] }], cardClick: [{ type: i0.Output, args: ["cardClick"] }], trackRef: [{ type: i0.ViewChild, args: ['track', { isSignal: true }] }] } });
4328
4519
 
4520
+ /**
4521
+ * Angular toolbar component — like mat-toolbar, built on Oat CSS.
4522
+ *
4523
+ * Renders a fixed-position `<nav data-topnav>` that leverages Oat's built-in
4524
+ * topnav styling (flex, border, shadow). Content-projected slots let you
4525
+ * arrange Logo / nav-links / actions however you like.
4526
+ *
4527
+ * ## Slots
4528
+ * - **`[toolbarStart]`** — Left-aligned content (logo, brand, hamburger)
4529
+ * - **Default `<ng-content>`** — Center / free-form content (nav links, search)
4530
+ * - **`[toolbarEnd]`** — Right-aligned content (user menu, theme toggle, actions)
4531
+ *
4532
+ * ## Layout
4533
+ * The toolbar uses `display:flex; align-items:center` with a spacer between
4534
+ * the default content and the end slot, so start items anchor left and end
4535
+ * items anchor right automatically.
4536
+ *
4537
+ * Usage:
4538
+ * ```html
4539
+ * <ng-oat-toolbar>
4540
+ * <a toolbarStart routerLink="/" class="brand">🌾 MyApp</a>
4541
+ * <nav>
4542
+ * <a routerLink="/home">Home</a>
4543
+ * <a routerLink="/about">About</a>
4544
+ * </nav>
4545
+ * <ng-oat-dropdown toolbarEnd>
4546
+ * <button trigger class="ghost">👤 User ▾</button>
4547
+ * <a role="menuitem">Profile</a>
4548
+ * <a role="menuitem">Settings</a>
4549
+ * <hr />
4550
+ * <a role="menuitem">Logout</a>
4551
+ * </ng-oat-dropdown>
4552
+ * </ng-oat-toolbar>
4553
+ * ```
4554
+ */
4555
+ class NgOatToolbar {
4556
+ color = input('default', ...(ngDevMode ? [{ debugName: "color" }] : []));
4557
+ dense = input(false, ...(ngDevMode ? [{ debugName: "dense" }] : []));
4558
+ fixed = input(true, ...(ngDevMode ? [{ debugName: "fixed" }] : []));
4559
+ attrVariant = computed(() => {
4560
+ const c = this.color();
4561
+ return c === 'default' ? null : c;
4562
+ }, ...(ngDevMode ? [{ debugName: "attrVariant" }] : []));
4563
+ navClass = computed(() => {
4564
+ const classes = [];
4565
+ const c = this.color();
4566
+ if (c !== 'default')
4567
+ classes.push(`oat-toolbar-${c}`);
4568
+ if (this.dense())
4569
+ classes.push('oat-toolbar-dense');
4570
+ if (!this.fixed())
4571
+ classes.push('oat-toolbar-static');
4572
+ return classes.join(' ');
4573
+ }, ...(ngDevMode ? [{ debugName: "navClass" }] : []));
4574
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: NgOatToolbar, deps: [], target: i0.ɵɵFactoryTarget.Component });
4575
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.0", type: NgOatToolbar, isStandalone: true, selector: "ng-oat-toolbar", inputs: { color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, dense: { classPropertyName: "dense", publicName: "dense", isSignal: true, isRequired: false, transformFunction: null }, fixed: { classPropertyName: "fixed", publicName: "fixed", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "toolbar" }, properties: { "attr.data-variant": "attrVariant()" } }, ngImport: i0, template: `
4576
+ <nav data-topnav [class]="navClass()">
4577
+ <ng-content select="[toolbarStart]" />
4578
+ <ng-content />
4579
+ <span class="oat-toolbar-spacer"></span>
4580
+ <ng-content select="[toolbarEnd]" />
4581
+ </nav>
4582
+ `, isInline: true, styles: [":host{display:block}.oat-toolbar-spacer{flex:1 1 auto}nav[data-topnav].oat-toolbar-static{position:relative;inset:unset;z-index:auto}nav[data-topnav].oat-toolbar-dense{min-height:var(--space-10);padding-block:var(--space-1)}nav[data-topnav].oat-toolbar-primary{background-color:var(--primary);color:var(--primary-foreground);border-bottom-color:var(--primary)}nav[data-topnav].oat-toolbar-primary a{color:var(--primary-foreground)}nav[data-topnav].oat-toolbar-accent{background-color:var(--accent);color:var(--foreground);border-bottom-color:var(--accent)}\n"] });
4583
+ }
4584
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: NgOatToolbar, decorators: [{
4585
+ type: Component,
4586
+ args: [{ selector: 'ng-oat-toolbar', host: {
4587
+ 'role': 'toolbar',
4588
+ '[attr.data-variant]': 'attrVariant()',
4589
+ }, template: `
4590
+ <nav data-topnav [class]="navClass()">
4591
+ <ng-content select="[toolbarStart]" />
4592
+ <ng-content />
4593
+ <span class="oat-toolbar-spacer"></span>
4594
+ <ng-content select="[toolbarEnd]" />
4595
+ </nav>
4596
+ `, styles: [":host{display:block}.oat-toolbar-spacer{flex:1 1 auto}nav[data-topnav].oat-toolbar-static{position:relative;inset:unset;z-index:auto}nav[data-topnav].oat-toolbar-dense{min-height:var(--space-10);padding-block:var(--space-1)}nav[data-topnav].oat-toolbar-primary{background-color:var(--primary);color:var(--primary-foreground);border-bottom-color:var(--primary)}nav[data-topnav].oat-toolbar-primary a{color:var(--primary-foreground)}nav[data-topnav].oat-toolbar-accent{background-color:var(--accent);color:var(--foreground);border-bottom-color:var(--accent)}\n"] }]
4597
+ }], propDecorators: { color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], dense: [{ type: i0.Input, args: [{ isSignal: true, alias: "dense", required: false }] }], fixed: [{ type: i0.Input, args: [{ isSignal: true, alias: "fixed", required: false }] }] } });
4598
+ /**
4599
+ * Toolbar row — use multiple rows stacked inside a toolbar.
4600
+ *
4601
+ * Usage:
4602
+ * ```html
4603
+ * <ng-oat-toolbar>
4604
+ * <ng-oat-toolbar-row>
4605
+ * <a toolbarStart>Brand</a>
4606
+ * <span toolbarEnd>Actions</span>
4607
+ * </ng-oat-toolbar-row>
4608
+ * <ng-oat-toolbar-row dense>
4609
+ * <nav>Sub-navigation tabs</nav>
4610
+ * </ng-oat-toolbar-row>
4611
+ * </ng-oat-toolbar>
4612
+ * ```
4613
+ */
4614
+ class NgOatToolbarRow {
4615
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: NgOatToolbarRow, deps: [], target: i0.ɵɵFactoryTarget.Component });
4616
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.0", type: NgOatToolbarRow, isStandalone: true, selector: "ng-oat-toolbar-row", host: { classAttribute: "oat-toolbar-row" }, ngImport: i0, template: `<ng-content />`, isInline: true, styles: [":host{display:flex;align-items:center;gap:var(--space-3);width:100%}\n"] });
4617
+ }
4618
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: NgOatToolbarRow, decorators: [{
4619
+ type: Component,
4620
+ args: [{ selector: 'ng-oat-toolbar-row', host: { class: 'oat-toolbar-row' }, template: `<ng-content />`, styles: [":host{display:flex;align-items:center;gap:var(--space-3);width:100%}\n"] }]
4621
+ }] });
4622
+
4623
+ const DEFAULT_THEMES = [
4624
+ { value: 'light', label: 'Light' },
4625
+ { value: 'dark', label: 'Dark' },
4626
+ { value: 'system', label: 'System' },
4627
+ ];
4628
+ const STORAGE_KEY = 'ng-oat-theme';
4629
+ /**
4630
+ * Structural directive for fully-custom icon rendering.
4631
+ *
4632
+ * Place inside `<ng-oat-theme-selector>` to replace the built-in SVG icons.
4633
+ * The template context receives the current `NgOatThemeOption` as `$implicit`.
4634
+ *
4635
+ * ```html
4636
+ * <ng-oat-theme-selector>
4637
+ * <ng-template ngOatThemeSelectorIcon let-opt>
4638
+ * <my-icon [name]="opt.value" />
4639
+ * </ng-template>
4640
+ * </ng-oat-theme-selector>
4641
+ * ```
4642
+ */
4643
+ class NgOatThemeSelectorIcon {
4644
+ tpl = inject(TemplateRef);
4645
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: NgOatThemeSelectorIcon, deps: [], target: i0.ɵɵFactoryTarget.Directive });
4646
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.0", type: NgOatThemeSelectorIcon, isStandalone: true, selector: "ng-template[ngOatThemeSelectorIcon]", ngImport: i0 });
4647
+ }
4648
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: NgOatThemeSelectorIcon, decorators: [{
4649
+ type: Directive,
4650
+ args: [{ selector: 'ng-template[ngOatThemeSelectorIcon]' }]
4651
+ }] });
4652
+ /**
4653
+ * Theme selector — shadcn-inspired light / dark / system toggle.
4654
+ *
4655
+ * Applies `colorScheme` on `<html>` and persists the choice to `localStorage`.
4656
+ * When "system" is selected it respects `prefers-color-scheme`.
4657
+ *
4658
+ * Usage:
4659
+ * ```html
4660
+ * <!-- Dropdown-style (default) -->
4661
+ * <ng-oat-theme-selector />
4662
+ *
4663
+ * <!-- Inline toggle group style -->
4664
+ * <ng-oat-theme-selector mode="toggle" />
4665
+ *
4666
+ * <!-- Listen for changes -->
4667
+ * <ng-oat-theme-selector (themeChange)="onTheme($event)" />
4668
+ * ```
4669
+ */
4670
+ class NgOatThemeSelector {
4671
+ /** Display mode: dropdown menu or inline toggle group */
4672
+ mode = input('dropdown', ...(ngDevMode ? [{ debugName: "mode" }] : []));
4673
+ /** Initial theme (overrides localStorage if set) */
4674
+ initialTheme = input(undefined, ...(ngDevMode ? [{ debugName: "initialTheme" }] : []));
4675
+ /**
4676
+ * Custom theme options. Override labels, provide emoji icons, or
4677
+ * change the set entirely.
4678
+ *
4679
+ * ```html
4680
+ * <ng-oat-theme-selector
4681
+ * [themes]="[
4682
+ * { value: 'light', label: 'Day', icon: '🌅' },
4683
+ * { value: 'dark', label: 'Night', icon: '🌃' },
4684
+ * { value: 'system', label: 'Auto', icon: '🖥️' },
4685
+ * ]" />
4686
+ * ```
4687
+ */
4688
+ themes = input(DEFAULT_THEMES, ...(ngDevMode ? [{ debugName: "themes" }] : []));
4689
+ /** Emits when the user picks a theme */
4690
+ themeChange = output();
4691
+ /** Current active theme */
4692
+ current = signal('system', ...(ngDevMode ? [{ debugName: "current" }] : []));
4693
+ open = signal(false, ...(ngDevMode ? [{ debugName: "open" }] : []));
4694
+ /** Content-projected custom icon template */
4695
+ iconTpl = contentChild(NgOatThemeSelectorIcon, ...(ngDevMode ? [{ debugName: "iconTpl" }] : []));
4696
+ /** Resolved themes (input or defaults) */
4697
+ resolvedThemes = computed(() => this.themes(), ...(ngDevMode ? [{ debugName: "resolvedThemes" }] : []));
4698
+ /** The currently active option object */
4699
+ activeOption = computed(() => this.resolvedThemes().find(t => t.value === this.current()) ?? this.resolvedThemes()[0], ...(ngDevMode ? [{ debugName: "activeOption" }] : []));
4700
+ doc = inject(DOCUMENT);
4701
+ platformId = inject(PLATFORM_ID);
4702
+ mediaQuery = null;
4703
+ mediaListener = (e) => this.applyResolved(e.matches ? 'dark' : 'light');
4704
+ /** Toggle the dropdown open/closed */
4705
+ toggleOpen() {
4706
+ this.open.update(v => !v);
4707
+ }
4708
+ /** Pick a theme and close the dropdown */
4709
+ pick(theme) {
4710
+ this.setTheme(theme);
4711
+ this.open.set(false);
4712
+ }
4713
+ /** Keyboard handler for Escape and arrow-key navigation inside the menu */
4714
+ onHostKey(e) {
4715
+ if (e.key === 'Escape' && this.open()) {
4716
+ e.preventDefault();
4717
+ this.open.set(false);
4718
+ // Return focus to trigger
4719
+ const trigger = e.target?.closest('.oat-ts-dropdown')?.querySelector('.oat-ts-trigger');
4720
+ trigger?.focus();
4721
+ return;
4722
+ }
4723
+ if (!this.open())
4724
+ return;
4725
+ if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
4726
+ e.preventDefault();
4727
+ const menu = e.target?.closest('.oat-ts-dropdown')?.querySelector('.oat-ts-menu');
4728
+ if (!menu)
4729
+ return;
4730
+ const items = Array.from(menu.querySelectorAll('button[role="option"]'));
4731
+ const idx = items.indexOf(e.target);
4732
+ const next = e.key === 'ArrowDown'
4733
+ ? items[(idx + 1) % items.length]
4734
+ : items[(idx - 1 + items.length) % items.length];
4735
+ next?.focus();
4736
+ }
4737
+ }
4738
+ ngOnInit() {
4739
+ if (!isPlatformBrowser(this.platformId))
4740
+ return;
4741
+ this.mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
4742
+ // Determine starting theme: explicit input > localStorage > system
4743
+ const init = this.initialTheme();
4744
+ if (init) {
4745
+ this.setTheme(init);
4746
+ }
4747
+ else {
4748
+ const stored = localStorage.getItem(STORAGE_KEY);
4749
+ this.setTheme(stored ?? 'system');
4750
+ }
4751
+ // Close dropdown on outside click
4752
+ this.doc.addEventListener('click', this.onDocClick);
4753
+ }
4754
+ setTheme(theme) {
4755
+ this.current.set(theme);
4756
+ this.themeChange.emit(theme);
4757
+ if (!isPlatformBrowser(this.platformId))
4758
+ return;
4759
+ localStorage.setItem(STORAGE_KEY, theme);
4760
+ // Unsubscribe from previous media listener
4761
+ this.mediaQuery?.removeEventListener('change', this.mediaListener);
4762
+ if (theme === 'system') {
4763
+ this.applyResolved(this.mediaQuery?.matches ? 'dark' : 'light');
4764
+ this.mediaQuery?.addEventListener('change', this.mediaListener);
4765
+ }
4766
+ else {
4767
+ this.applyResolved(theme);
4768
+ }
4769
+ }
4770
+ applyResolved(resolved) {
4771
+ this.doc.documentElement.style.colorScheme = resolved;
4772
+ }
4773
+ onDocClick = (e) => {
4774
+ if (!e.target?.closest?.('.oat-ts-dropdown')) {
4775
+ this.open.set(false);
4776
+ }
4777
+ };
4778
+ /** @internal */
4779
+ ngOnDestroy() {
4780
+ this.mediaQuery?.removeEventListener('change', this.mediaListener);
4781
+ if (isPlatformBrowser(this.platformId)) {
4782
+ this.doc.removeEventListener('click', this.onDocClick);
4783
+ }
4784
+ }
4785
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: NgOatThemeSelector, deps: [], target: i0.ɵɵFactoryTarget.Component });
4786
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: NgOatThemeSelector, isStandalone: true, selector: "ng-oat-theme-selector", inputs: { mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, initialTheme: { classPropertyName: "initialTheme", publicName: "initialTheme", isSignal: true, isRequired: false, transformFunction: null }, themes: { classPropertyName: "themes", publicName: "themes", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { themeChange: "themeChange" }, host: { listeners: { "keydown": "onHostKey($event)" }, properties: { "attr.data-mode": "mode()" }, classAttribute: "oat-theme-selector" }, queries: [{ propertyName: "iconTpl", first: true, predicate: NgOatThemeSelectorIcon, descendants: true, isSignal: true }], ngImport: i0, template: `
4787
+ @if (mode() === 'dropdown') {
4788
+ <div class="oat-ts-dropdown">
4789
+ <button
4790
+ class="oat-ts-trigger"
4791
+ type="button"
4792
+ aria-haspopup="listbox"
4793
+ [attr.aria-expanded]="open()"
4794
+ aria-label="Theme: {{ current() }}"
4795
+ (click)="toggleOpen()">
4796
+ <ng-container *ngTemplateOutlet="iconTpl()?.tpl ?? null; context: { $implicit: activeOption() }" />
4797
+ @if (!iconTpl()) {
4798
+ @if (activeOption().icon) {
4799
+ <span class="oat-ts-emoji" aria-hidden="true">{{ activeOption().icon }}</span>
4800
+ } @else {
4801
+ <ng-container [ngSwitch]="current()">
4802
+ <svg *ngSwitchCase="'light'" class="oat-ts-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>
4803
+ <svg *ngSwitchCase="'dark'" class="oat-ts-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
4804
+ <svg *ngSwitchDefault class="oat-ts-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>
4805
+ </ng-container>
4806
+ }
4807
+ }
4808
+ <svg class="oat-ts-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor"
4809
+ stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
4810
+ <polyline points="6 9 12 15 18 9"/>
4811
+ </svg>
4812
+ </button>
4813
+ @if (open()) {
4814
+ <div class="oat-ts-menu" role="listbox" aria-label="Select theme">
4815
+ @for (t of resolvedThemes(); track t.value) {
4816
+ <button
4817
+ role="option"
4818
+ type="button"
4819
+ [attr.aria-selected]="current() === t.value"
4820
+ [class.active]="current() === t.value"
4821
+ (click)="pick(t.value)">
4822
+ <ng-container *ngTemplateOutlet="iconTpl()?.tpl ?? null; context: { $implicit: t }" />
4823
+ @if (!iconTpl()) {
4824
+ @if (t.icon) {
4825
+ <span class="oat-ts-emoji" aria-hidden="true">{{ t.icon }}</span>
4826
+ } @else {
4827
+ <ng-container [ngSwitch]="t.value">
4828
+ <svg *ngSwitchCase="'light'" class="oat-ts-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>
4829
+ <svg *ngSwitchCase="'dark'" class="oat-ts-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
4830
+ <svg *ngSwitchDefault class="oat-ts-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>
4831
+ </ng-container>
4832
+ }
4833
+ }
4834
+ <span>{{ t.label }}</span>
4835
+ @if (current() === t.value) {
4836
+ <svg class="oat-ts-check" viewBox="0 0 24 24" fill="none" stroke="currentColor"
4837
+ stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
4838
+ <polyline points="20 6 9 17 4 12"/>
4839
+ </svg>
4840
+ }
4841
+ </button>
4842
+ }
4843
+ </div>
4844
+ }
4845
+ </div>
4846
+ } @else {
4847
+ <div class="oat-ts-toggle" role="radiogroup" aria-label="Theme">
4848
+ @for (t of resolvedThemes(); track t.value) {
4849
+ <button
4850
+ type="button"
4851
+ role="radio"
4852
+ [attr.aria-checked]="current() === t.value"
4853
+ [attr.aria-label]="t.label"
4854
+ [class.active]="current() === t.value"
4855
+ (click)="setTheme(t.value)">
4856
+ <ng-container *ngTemplateOutlet="iconTpl()?.tpl ?? null; context: { $implicit: t }" />
4857
+ @if (!iconTpl()) {
4858
+ @if (t.icon) {
4859
+ <span class="oat-ts-emoji" aria-hidden="true">{{ t.icon }}</span>
4860
+ } @else {
4861
+ <ng-container [ngSwitch]="t.value">
4862
+ <svg *ngSwitchCase="'light'" class="oat-ts-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>
4863
+ <svg *ngSwitchCase="'dark'" class="oat-ts-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
4864
+ <svg *ngSwitchDefault class="oat-ts-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>
4865
+ </ng-container>
4866
+ }
4867
+ }
4868
+ </button>
4869
+ }
4870
+ </div>
4871
+ }
4872
+ `, isInline: true, styles: [":host{display:inline-flex}.oat-ts-svg{width:1rem;height:1rem;flex-shrink:0}.oat-ts-emoji{font-size:1rem;line-height:1;flex-shrink:0}.oat-ts-dropdown{position:relative}.oat-ts-trigger{display:inline-flex;align-items:center;gap:var(--space-1);cursor:pointer;padding:var(--space-1) var(--space-2);border-radius:var(--radius-medium);border:1px solid var(--border);background:transparent;color:var(--foreground);transition:background .15s,border-color .15s;line-height:1}.oat-ts-trigger:hover{background:var(--accent)}.oat-ts-trigger:focus-visible{outline:2px solid var(--ring);outline-offset:2px}.oat-ts-chevron{width:.75rem;height:.75rem;opacity:.5}.oat-ts-menu{position:absolute;top:calc(100% + var(--space-1));right:0;z-index:50;min-width:8rem;border:1px solid var(--border);border-radius:var(--radius-medium);background:var(--background);box-shadow:var(--shadow-small);padding:var(--space-1);display:flex;flex-direction:column;animation:oat-ts-fade-in .12s ease-out}@keyframes oat-ts-fade-in{0%{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}.oat-ts-menu button{all:unset;box-sizing:border-box;display:flex;align-items:center;gap:var(--space-2);padding:var(--space-1) var(--space-2);border-radius:var(--radius-small);cursor:pointer;font-size:var(--text-6);line-height:1.25;color:var(--foreground);transition:background .1s}.oat-ts-menu button:hover,.oat-ts-menu button:focus-visible{background:var(--accent)}.oat-ts-menu button:focus-visible{outline:2px solid var(--ring);outline-offset:-2px}.oat-ts-menu button.active{font-weight:500}.oat-ts-check{width:.875rem;height:.875rem;margin-inline-start:auto;opacity:.7}.oat-ts-toggle{display:inline-flex;border:1px solid var(--border);border-radius:var(--radius-medium);overflow:hidden}.oat-ts-toggle button{all:unset;box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;padding:var(--space-1) var(--space-2);cursor:pointer;transition:background .15s;color:var(--muted-foreground, var(--foreground))}.oat-ts-toggle button:hover{background:var(--accent)}.oat-ts-toggle button:focus-visible{outline:2px solid var(--ring);outline-offset:-2px}.oat-ts-toggle button.active{background:var(--accent);color:var(--foreground)}.oat-ts-toggle button+button{border-left:1px solid var(--border)}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: NgSwitch, selector: "[ngSwitch]", inputs: ["ngSwitch"] }, { kind: "directive", type: NgSwitchCase, selector: "[ngSwitchCase]", inputs: ["ngSwitchCase"] }, { kind: "directive", type: NgSwitchDefault, selector: "[ngSwitchDefault]" }] });
4873
+ }
4874
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: NgOatThemeSelector, decorators: [{
4875
+ type: Component,
4876
+ args: [{ selector: 'ng-oat-theme-selector', imports: [NgTemplateOutlet, NgSwitch, NgSwitchCase, NgSwitchDefault], host: {
4877
+ 'class': 'oat-theme-selector',
4878
+ '[attr.data-mode]': 'mode()',
4879
+ '(keydown)': 'onHostKey($event)',
4880
+ }, template: `
4881
+ @if (mode() === 'dropdown') {
4882
+ <div class="oat-ts-dropdown">
4883
+ <button
4884
+ class="oat-ts-trigger"
4885
+ type="button"
4886
+ aria-haspopup="listbox"
4887
+ [attr.aria-expanded]="open()"
4888
+ aria-label="Theme: {{ current() }}"
4889
+ (click)="toggleOpen()">
4890
+ <ng-container *ngTemplateOutlet="iconTpl()?.tpl ?? null; context: { $implicit: activeOption() }" />
4891
+ @if (!iconTpl()) {
4892
+ @if (activeOption().icon) {
4893
+ <span class="oat-ts-emoji" aria-hidden="true">{{ activeOption().icon }}</span>
4894
+ } @else {
4895
+ <ng-container [ngSwitch]="current()">
4896
+ <svg *ngSwitchCase="'light'" class="oat-ts-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>
4897
+ <svg *ngSwitchCase="'dark'" class="oat-ts-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
4898
+ <svg *ngSwitchDefault class="oat-ts-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>
4899
+ </ng-container>
4900
+ }
4901
+ }
4902
+ <svg class="oat-ts-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor"
4903
+ stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
4904
+ <polyline points="6 9 12 15 18 9"/>
4905
+ </svg>
4906
+ </button>
4907
+ @if (open()) {
4908
+ <div class="oat-ts-menu" role="listbox" aria-label="Select theme">
4909
+ @for (t of resolvedThemes(); track t.value) {
4910
+ <button
4911
+ role="option"
4912
+ type="button"
4913
+ [attr.aria-selected]="current() === t.value"
4914
+ [class.active]="current() === t.value"
4915
+ (click)="pick(t.value)">
4916
+ <ng-container *ngTemplateOutlet="iconTpl()?.tpl ?? null; context: { $implicit: t }" />
4917
+ @if (!iconTpl()) {
4918
+ @if (t.icon) {
4919
+ <span class="oat-ts-emoji" aria-hidden="true">{{ t.icon }}</span>
4920
+ } @else {
4921
+ <ng-container [ngSwitch]="t.value">
4922
+ <svg *ngSwitchCase="'light'" class="oat-ts-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>
4923
+ <svg *ngSwitchCase="'dark'" class="oat-ts-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
4924
+ <svg *ngSwitchDefault class="oat-ts-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>
4925
+ </ng-container>
4926
+ }
4927
+ }
4928
+ <span>{{ t.label }}</span>
4929
+ @if (current() === t.value) {
4930
+ <svg class="oat-ts-check" viewBox="0 0 24 24" fill="none" stroke="currentColor"
4931
+ stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
4932
+ <polyline points="20 6 9 17 4 12"/>
4933
+ </svg>
4934
+ }
4935
+ </button>
4936
+ }
4937
+ </div>
4938
+ }
4939
+ </div>
4940
+ } @else {
4941
+ <div class="oat-ts-toggle" role="radiogroup" aria-label="Theme">
4942
+ @for (t of resolvedThemes(); track t.value) {
4943
+ <button
4944
+ type="button"
4945
+ role="radio"
4946
+ [attr.aria-checked]="current() === t.value"
4947
+ [attr.aria-label]="t.label"
4948
+ [class.active]="current() === t.value"
4949
+ (click)="setTheme(t.value)">
4950
+ <ng-container *ngTemplateOutlet="iconTpl()?.tpl ?? null; context: { $implicit: t }" />
4951
+ @if (!iconTpl()) {
4952
+ @if (t.icon) {
4953
+ <span class="oat-ts-emoji" aria-hidden="true">{{ t.icon }}</span>
4954
+ } @else {
4955
+ <ng-container [ngSwitch]="t.value">
4956
+ <svg *ngSwitchCase="'light'" class="oat-ts-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>
4957
+ <svg *ngSwitchCase="'dark'" class="oat-ts-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
4958
+ <svg *ngSwitchDefault class="oat-ts-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>
4959
+ </ng-container>
4960
+ }
4961
+ }
4962
+ </button>
4963
+ }
4964
+ </div>
4965
+ }
4966
+ `, styles: [":host{display:inline-flex}.oat-ts-svg{width:1rem;height:1rem;flex-shrink:0}.oat-ts-emoji{font-size:1rem;line-height:1;flex-shrink:0}.oat-ts-dropdown{position:relative}.oat-ts-trigger{display:inline-flex;align-items:center;gap:var(--space-1);cursor:pointer;padding:var(--space-1) var(--space-2);border-radius:var(--radius-medium);border:1px solid var(--border);background:transparent;color:var(--foreground);transition:background .15s,border-color .15s;line-height:1}.oat-ts-trigger:hover{background:var(--accent)}.oat-ts-trigger:focus-visible{outline:2px solid var(--ring);outline-offset:2px}.oat-ts-chevron{width:.75rem;height:.75rem;opacity:.5}.oat-ts-menu{position:absolute;top:calc(100% + var(--space-1));right:0;z-index:50;min-width:8rem;border:1px solid var(--border);border-radius:var(--radius-medium);background:var(--background);box-shadow:var(--shadow-small);padding:var(--space-1);display:flex;flex-direction:column;animation:oat-ts-fade-in .12s ease-out}@keyframes oat-ts-fade-in{0%{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}.oat-ts-menu button{all:unset;box-sizing:border-box;display:flex;align-items:center;gap:var(--space-2);padding:var(--space-1) var(--space-2);border-radius:var(--radius-small);cursor:pointer;font-size:var(--text-6);line-height:1.25;color:var(--foreground);transition:background .1s}.oat-ts-menu button:hover,.oat-ts-menu button:focus-visible{background:var(--accent)}.oat-ts-menu button:focus-visible{outline:2px solid var(--ring);outline-offset:-2px}.oat-ts-menu button.active{font-weight:500}.oat-ts-check{width:.875rem;height:.875rem;margin-inline-start:auto;opacity:.7}.oat-ts-toggle{display:inline-flex;border:1px solid var(--border);border-radius:var(--radius-medium);overflow:hidden}.oat-ts-toggle button{all:unset;box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;padding:var(--space-1) var(--space-2);cursor:pointer;transition:background .15s;color:var(--muted-foreground, var(--foreground))}.oat-ts-toggle button:hover{background:var(--accent)}.oat-ts-toggle button:focus-visible{outline:2px solid var(--ring);outline-offset:-2px}.oat-ts-toggle button.active{background:var(--accent);color:var(--foreground)}.oat-ts-toggle button+button{border-left:1px solid var(--border)}\n"] }]
4967
+ }], propDecorators: { mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], initialTheme: [{ type: i0.Input, args: [{ isSignal: true, alias: "initialTheme", required: false }] }], themes: [{ type: i0.Input, args: [{ isSignal: true, alias: "themes", required: false }] }], themeChange: [{ type: i0.Output, args: ["themeChange"] }], iconTpl: [{ type: i0.ContentChild, args: [i0.forwardRef(() => NgOatThemeSelectorIcon), { isSignal: true }] }] } });
4968
+
4329
4969
  /**
4330
4970
  * Oat-styled text input implementing `FormValueControl<string>`.
4331
4971
  * Works seamlessly with Signal Forms `[formField]` — no CVA needed.
@@ -4870,5 +5510,5 @@ const OAT_VERSION_TOKEN = new InjectionToken('OAT_VERSION', {
4870
5510
  * Generated bundle index. Do not edit.
4871
5511
  */
4872
5512
 
4873
- export { NG_OAT_CHIP_GROUP, NG_OAT_TOGGLE_GROUP, NgOatAccordion, NgOatAlert, NgOatAvatar, NgOatBadge, NgOatBreadcrumb, NgOatButton, NgOatCard, NgOatCardCarousel, NgOatCardFooter, NgOatCardHeader, NgOatCarousel, NgOatCheckbox, NgOatChip, NgOatChipGroup, NgOatChipInput, NgOatDialog, NgOatDialogComponent, NgOatDropdown, NgOatDropdownComponent, NgOatFileUpload, NgOatFormError, NgOatInput, NgOatInputOtp, NgOatMeter, NgOatPagination, NgOatProgress, NgOatRadioGroup, NgOatSearchInput, NgOatSelect, NgOatSeparator, NgOatSidebar, NgOatSidebarComponent, NgOatSkeleton, NgOatSpinner, NgOatSplitButton, NgOatSwitch, NgOatTable, NgOatTabs, NgOatTabsComponent, NgOatTextarea, NgOatThemeRef, NgOatToast, NgOatToggle, NgOatToggleGroup, NgOatTooltip, NgOatTooltipComponent, OAT_TOKEN_MAP, OAT_VERSION, OAT_VERSION_TOKEN, TOOLTIP_POSITIONER, provideNgOat, provideNgOatTheme };
5513
+ export { NG_OAT_CHIP_GROUP, NG_OAT_TOGGLE_GROUP, NgOatAccordion, NgOatAlert, NgOatAvatar, NgOatBadge, NgOatBreadcrumb, NgOatButton, NgOatCard, NgOatCardCarousel, NgOatCardFooter, NgOatCardHeader, NgOatCarousel, NgOatCheckbox, NgOatChip, NgOatChipGroup, NgOatChipInput, NgOatDialog, NgOatDialogComponent, NgOatDropdown, NgOatDropdownComponent, NgOatFileUpload, NgOatFormError, NgOatInput, NgOatInputOtp, NgOatMeter, NgOatPagination, NgOatProgress, NgOatRadioGroup, NgOatSearchInput, NgOatSelect, NgOatSeparator, NgOatSidebar, NgOatSidebarComponent, NgOatSkeleton, NgOatSpinner, NgOatSplitButton, NgOatSwitch, NgOatTable, NgOatTabs, NgOatTabsComponent, NgOatTextarea, NgOatThemeRef, NgOatThemeSelector, NgOatThemeSelectorIcon, NgOatToast, NgOatToggle, NgOatToggleGroup, NgOatToolbar, NgOatToolbarRow, NgOatTooltip, NgOatTooltipComponent, OAT_TOKEN_MAP, OAT_VERSION, OAT_VERSION_TOKEN, TOOLTIP_POSITIONER, provideNgOat, provideNgOatTheme };
4874
5514
  //# sourceMappingURL=letsprogram-ng-oat.mjs.map