@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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@letsprogram/ng-oat",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Angular component library built on top of the Oat CSS framework — signals-first, lightweight, and accessible.",
5
5
  "license": "MIT",
6
6
  "author": "SashiKumar Yadav",
@@ -7,22 +7,13 @@ interface NgOatOptions {
7
7
  assets?: {
8
8
  /**
9
9
  * How to load Oat CSS at runtime.
10
- * - `false` (default) — CSS is loaded via angular.json styles array (recommended).
10
+ * - `false` (default) — CSS is loaded via angular.json styles array or styles.css import (recommended).
11
11
  * - `'link'` — dynamically inject a `<link>` tag (requires assets glob in angular.json).
12
- * - `'import'` — reserved for future use.
13
12
  */
14
- css?: 'link' | 'import' | false;
15
- /**
16
- * How to load Oat JS at runtime.
17
- * - `false` (default) — JS is loaded via angular.json scripts array (recommended).
18
- * - `'script'` — dynamically inject a `<script>` tag (requires assets glob in angular.json).
19
- */
20
- js?: 'script' | false;
13
+ css?: 'link' | false;
21
14
  };
22
- /** Register ot-tabs/ot-dropdown custom elements from Oat JS (default: true). */
23
- registerWebComponents?: boolean;
24
15
  /**
25
- * Base path for vendored Oat assets when using dynamic injection (`css: 'link'` or `js: 'script'`).
16
+ * Base path for vendored Oat assets when using dynamic CSS injection (`css: 'link'`).
26
17
  * Only relevant if you opted into runtime injection AND configured an assets glob
27
18
  * to copy files from `node_modules/@letsprogram/ng-oat/assets/oat` to this path.
28
19
  * Default: `'assets/oat'`.
@@ -30,18 +21,19 @@ interface NgOatOptions {
30
21
  basePath?: string;
31
22
  }
32
23
  /**
33
- * Provides Oat UI core initialisation.
24
+ * Provides ng-oat core initialisation.
34
25
  *
35
- * By default, CSS and JS are **not** injected at runtime — add them via
36
- * `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.
37
29
  *
38
30
  * ```ts
39
- * provideNgOat() // recommended (CSS/JS via angular.json)
40
- * 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 *
41
33
  * ```
42
34
  *
43
- * \* Runtime injection requires an assets glob in angular.json to copy files:
44
- * `{ 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' }`
45
37
  */
46
38
  declare function provideNgOat(options?: NgOatOptions): EnvironmentProviders;
47
39
 
@@ -337,10 +329,12 @@ declare class NgOatSidebar {
337
329
  }
338
330
 
339
331
  /**
340
- * Angular integration for Oat's <ot-tabs> WebComponent.
332
+ * Angular directive that provides native tab behaviour on any element
333
+ * containing `role="tablist"` > `role="tab"` buttons and sibling
334
+ * `role="tabpanel"` containers.
341
335
  *
342
- * Oat's WC handles all ARIA, keyboard nav, and panel toggling.
343
- * This directive provides Angular-friendly signals and event binding.
336
+ * Works without oat.js — handles tab activation, ARIA attributes,
337
+ * panel visibility and full keyboard navigation internally.
344
338
  *
345
339
  * Usage:
346
340
  * ```html
@@ -368,10 +362,13 @@ declare class NgOatTabs {
368
362
  index: number;
369
363
  tab: HTMLElement;
370
364
  }>;
371
- private tabChangeListener;
365
+ private tabEls;
366
+ private panelEls;
367
+ private cleanupFns;
372
368
  constructor();
373
369
  /** Programmatically select a tab by index */
374
370
  selectTab(index: number): void;
371
+ private activate;
375
372
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<NgOatTabs, never>;
376
373
  static ɵdir: _angular_core.ɵɵDirectiveDeclaration<NgOatTabs, "ot-tabs[ngOatTabs], [ngOatTabs]", ["ngOatTabs"], {}, { "ngOatTabChange": "ngOatTabChange"; }, never, never, true, never>;
377
374
  }
@@ -421,9 +418,9 @@ declare class NgOatDialog {
421
418
  static ɵdir: _angular_core.ɵɵDirectiveDeclaration<NgOatDialog, "dialog[ngOatDialog]", ["ngOatDialog"], {}, { "ngOatDialogClose": "ngOatDialogClose"; }, never, never, true, never>;
422
419
  }
423
420
 
424
- /** Toast placement positions (matches Oat's toast system) */
421
+ /** Toast placement positions */
425
422
  type NgOatToastPlacement = 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right';
426
- /** Toast variants (matches Oat's toast system) */
423
+ /** Toast variants */
427
424
  type NgOatToastVariant = 'info' | 'success' | 'danger' | 'warning';
428
425
  /** Options for showing a toast */
429
426
  interface NgOatToastOptions {
@@ -437,10 +434,11 @@ interface NgOatToastOptions {
437
434
  dismissible?: boolean;
438
435
  }
439
436
  /**
440
- * Angular service wrapping Oat's toast notification system.
437
+ * Angular service for Oat-styled toast notifications.
441
438
  *
442
- * Delegates to `window.ot.toast()` (provided by oat.min.js).
443
- * Falls back to a no-op if Oat JS is not loaded.
439
+ * Fully native implementation no dependency on oat.js.
440
+ * Uses the Oat CSS `.toast`, `.toast-container`, `data-entering/data-exiting`
441
+ * animation patterns, and popover API for stacking.
444
442
  *
445
443
  * Usage:
446
444
  * ```ts
@@ -454,7 +452,8 @@ interface NgOatToastOptions {
454
452
  declare class NgOatToast {
455
453
  private platformId;
456
454
  private doc;
457
- private get ot();
455
+ /** Cache of toast containers keyed by placement */
456
+ private containers;
458
457
  success(message: string, title?: string, options?: Omit<NgOatToastOptions, 'variant'>): void;
459
458
  info(message: string, title?: string, options?: Omit<NgOatToastOptions, 'variant'>): void;
460
459
  warning(message: string, title?: string, options?: Omit<NgOatToastOptions, 'variant'>): void;
@@ -462,7 +461,6 @@ declare class NgOatToast {
462
461
  show(message: string, title?: string, options?: NgOatToastOptions): HTMLElement | void;
463
462
  /**
464
463
  * Show a toast from a DOM element or template element.
465
- * For Angular TemplateRef, use showTemplate() instead.
466
464
  */
467
465
  showElement(element: HTMLElement, options?: NgOatToastOptions): void;
468
466
  /**
@@ -472,6 +470,12 @@ declare class NgOatToast {
472
470
  showTemplate(templateRef: TemplateRef<any>, vcr: ViewContainerRef, options?: NgOatToastOptions): void;
473
471
  /** Dismiss toasts. If placement given, only that position; otherwise all. */
474
472
  dismiss(placement?: NgOatToastPlacement): void;
473
+ /** Get or create a toast container for the given placement */
474
+ private getContainer;
475
+ /** Show a prepared element as a toast */
476
+ private showEl;
477
+ /** Remove a toast with exit animation */
478
+ private removeToast;
475
479
  /** Add a close button to a toast element */
476
480
  private addCloseButton;
477
481
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<NgOatToast, never>;
@@ -853,6 +857,9 @@ declare class NgOatDropdownComponent {
853
857
  open(): void;
854
858
  close(): void;
855
859
  toggle(): void;
860
+ /** Keyboard navigation for menu items (ArrowDown/Up/Home/End/Escape) */
861
+ private onKeydown;
862
+ private doc;
856
863
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<NgOatDropdownComponent, never>;
857
864
  static ɵcmp: _angular_core.ɵɵComponentDeclaration<NgOatDropdownComponent, "ng-oat-dropdown", never, {}, { "openChange": "openChange"; }, never, ["[trigger]", "*"], true, never>;
858
865
  }
@@ -1522,6 +1529,169 @@ declare class NgOatCardCarousel implements AfterViewInit, OnDestroy {
1522
1529
  static ɵcmp: _angular_core.ɵɵComponentDeclaration<NgOatCardCarousel, "ng-oat-card-carousel", never, { "heading": { "alias": "heading"; "required": false; "isSignal": true; }; "ariaLabel": { "alias": "ariaLabel"; "required": false; "isSignal": true; }; "items": { "alias": "items"; "required": false; "isSignal": true; }; "showSeeAll": { "alias": "showSeeAll"; "required": false; "isSignal": true; }; "scrollAmount": { "alias": "scrollAmount"; "required": false; "isSignal": true; }; }, { "seeAllClick": "seeAllClick"; "cardClick": "cardClick"; }, never, never, true, never>;
1523
1530
  }
1524
1531
 
1532
+ type NgOatToolbarColor = 'default' | 'primary' | 'accent';
1533
+ /**
1534
+ * Angular toolbar component — like mat-toolbar, built on Oat CSS.
1535
+ *
1536
+ * Renders a fixed-position `<nav data-topnav>` that leverages Oat's built-in
1537
+ * topnav styling (flex, border, shadow). Content-projected slots let you
1538
+ * arrange Logo / nav-links / actions however you like.
1539
+ *
1540
+ * ## Slots
1541
+ * - **`[toolbarStart]`** — Left-aligned content (logo, brand, hamburger)
1542
+ * - **Default `<ng-content>`** — Center / free-form content (nav links, search)
1543
+ * - **`[toolbarEnd]`** — Right-aligned content (user menu, theme toggle, actions)
1544
+ *
1545
+ * ## Layout
1546
+ * The toolbar uses `display:flex; align-items:center` with a spacer between
1547
+ * the default content and the end slot, so start items anchor left and end
1548
+ * items anchor right automatically.
1549
+ *
1550
+ * Usage:
1551
+ * ```html
1552
+ * <ng-oat-toolbar>
1553
+ * <a toolbarStart routerLink="/" class="brand">🌾 MyApp</a>
1554
+ * <nav>
1555
+ * <a routerLink="/home">Home</a>
1556
+ * <a routerLink="/about">About</a>
1557
+ * </nav>
1558
+ * <ng-oat-dropdown toolbarEnd>
1559
+ * <button trigger class="ghost">👤 User ▾</button>
1560
+ * <a role="menuitem">Profile</a>
1561
+ * <a role="menuitem">Settings</a>
1562
+ * <hr />
1563
+ * <a role="menuitem">Logout</a>
1564
+ * </ng-oat-dropdown>
1565
+ * </ng-oat-toolbar>
1566
+ * ```
1567
+ */
1568
+ declare class NgOatToolbar {
1569
+ readonly color: _angular_core.InputSignal<NgOatToolbarColor>;
1570
+ readonly dense: _angular_core.InputSignal<boolean>;
1571
+ readonly fixed: _angular_core.InputSignal<boolean>;
1572
+ protected attrVariant: _angular_core.Signal<"primary" | "accent" | null>;
1573
+ protected navClass: _angular_core.Signal<string>;
1574
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<NgOatToolbar, never>;
1575
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<NgOatToolbar, "ng-oat-toolbar", never, { "color": { "alias": "color"; "required": false; "isSignal": true; }; "dense": { "alias": "dense"; "required": false; "isSignal": true; }; "fixed": { "alias": "fixed"; "required": false; "isSignal": true; }; }, {}, never, ["[toolbarStart]", "*", "[toolbarEnd]"], true, never>;
1576
+ }
1577
+ /**
1578
+ * Toolbar row — use multiple rows stacked inside a toolbar.
1579
+ *
1580
+ * Usage:
1581
+ * ```html
1582
+ * <ng-oat-toolbar>
1583
+ * <ng-oat-toolbar-row>
1584
+ * <a toolbarStart>Brand</a>
1585
+ * <span toolbarEnd>Actions</span>
1586
+ * </ng-oat-toolbar-row>
1587
+ * <ng-oat-toolbar-row dense>
1588
+ * <nav>Sub-navigation tabs</nav>
1589
+ * </ng-oat-toolbar-row>
1590
+ * </ng-oat-toolbar>
1591
+ * ```
1592
+ */
1593
+ declare class NgOatToolbarRow {
1594
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<NgOatToolbarRow, never>;
1595
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<NgOatToolbarRow, "ng-oat-toolbar-row", never, {}, {}, never, ["*"], true, never>;
1596
+ }
1597
+
1598
+ type NgOatTheme = 'light' | 'dark' | 'system';
1599
+ /** Describes one option in the theme selector. */
1600
+ interface NgOatThemeOption {
1601
+ /** The theme value written to localStorage / emitted. */
1602
+ value: NgOatTheme;
1603
+ /** Human-readable label shown in dropdown items. */
1604
+ label: string;
1605
+ /** Optional text/emoji shown instead of the built-in SVG icon. */
1606
+ icon?: string;
1607
+ }
1608
+ /**
1609
+ * Structural directive for fully-custom icon rendering.
1610
+ *
1611
+ * Place inside `<ng-oat-theme-selector>` to replace the built-in SVG icons.
1612
+ * The template context receives the current `NgOatThemeOption` as `$implicit`.
1613
+ *
1614
+ * ```html
1615
+ * <ng-oat-theme-selector>
1616
+ * <ng-template ngOatThemeSelectorIcon let-opt>
1617
+ * <my-icon [name]="opt.value" />
1618
+ * </ng-template>
1619
+ * </ng-oat-theme-selector>
1620
+ * ```
1621
+ */
1622
+ declare class NgOatThemeSelectorIcon {
1623
+ readonly tpl: TemplateRef<any>;
1624
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<NgOatThemeSelectorIcon, never>;
1625
+ static ɵdir: _angular_core.ɵɵDirectiveDeclaration<NgOatThemeSelectorIcon, "ng-template[ngOatThemeSelectorIcon]", never, {}, {}, never, never, true, never>;
1626
+ }
1627
+ /**
1628
+ * Theme selector — shadcn-inspired light / dark / system toggle.
1629
+ *
1630
+ * Applies `colorScheme` on `<html>` and persists the choice to `localStorage`.
1631
+ * When "system" is selected it respects `prefers-color-scheme`.
1632
+ *
1633
+ * Usage:
1634
+ * ```html
1635
+ * <!-- Dropdown-style (default) -->
1636
+ * <ng-oat-theme-selector />
1637
+ *
1638
+ * <!-- Inline toggle group style -->
1639
+ * <ng-oat-theme-selector mode="toggle" />
1640
+ *
1641
+ * <!-- Listen for changes -->
1642
+ * <ng-oat-theme-selector (themeChange)="onTheme($event)" />
1643
+ * ```
1644
+ */
1645
+ declare class NgOatThemeSelector implements OnInit {
1646
+ /** Display mode: dropdown menu or inline toggle group */
1647
+ readonly mode: _angular_core.InputSignal<"dropdown" | "toggle">;
1648
+ /** Initial theme (overrides localStorage if set) */
1649
+ readonly initialTheme: _angular_core.InputSignal<NgOatTheme | undefined>;
1650
+ /**
1651
+ * Custom theme options. Override labels, provide emoji icons, or
1652
+ * change the set entirely.
1653
+ *
1654
+ * ```html
1655
+ * <ng-oat-theme-selector
1656
+ * [themes]="[
1657
+ * { value: 'light', label: 'Day', icon: '🌅' },
1658
+ * { value: 'dark', label: 'Night', icon: '🌃' },
1659
+ * { value: 'system', label: 'Auto', icon: '🖥️' },
1660
+ * ]" />
1661
+ * ```
1662
+ */
1663
+ readonly themes: _angular_core.InputSignal<NgOatThemeOption[]>;
1664
+ /** Emits when the user picks a theme */
1665
+ readonly themeChange: _angular_core.OutputEmitterRef<NgOatTheme>;
1666
+ /** Current active theme */
1667
+ readonly current: _angular_core.WritableSignal<NgOatTheme>;
1668
+ readonly open: _angular_core.WritableSignal<boolean>;
1669
+ /** Content-projected custom icon template */
1670
+ protected readonly iconTpl: _angular_core.Signal<NgOatThemeSelectorIcon | undefined>;
1671
+ /** Resolved themes (input or defaults) */
1672
+ protected resolvedThemes: _angular_core.Signal<NgOatThemeOption[]>;
1673
+ /** The currently active option object */
1674
+ protected activeOption: _angular_core.Signal<NgOatThemeOption>;
1675
+ private doc;
1676
+ private platformId;
1677
+ private mediaQuery;
1678
+ private mediaListener;
1679
+ /** Toggle the dropdown open/closed */
1680
+ toggleOpen(): void;
1681
+ /** Pick a theme and close the dropdown */
1682
+ pick(theme: NgOatTheme): void;
1683
+ /** Keyboard handler for Escape and arrow-key navigation inside the menu */
1684
+ onHostKey(e: KeyboardEvent): void;
1685
+ ngOnInit(): void;
1686
+ setTheme(theme: NgOatTheme): void;
1687
+ private applyResolved;
1688
+ private onDocClick;
1689
+ /** @internal */
1690
+ ngOnDestroy(): void;
1691
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<NgOatThemeSelector, never>;
1692
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<NgOatThemeSelector, "ng-oat-theme-selector", never, { "mode": { "alias": "mode"; "required": false; "isSignal": true; }; "initialTheme": { "alias": "initialTheme"; "required": false; "isSignal": true; }; "themes": { "alias": "themes"; "required": false; "isSignal": true; }; }, { "themeChange": "themeChange"; }, ["iconTpl"], never, true, never>;
1693
+ }
1694
+
1525
1695
  type NgOatInputType = 'text' | 'email' | 'password' | 'number' | 'tel' | 'url' | 'search' | 'date' | 'datetime-local' | 'time' | 'month' | 'week' | 'color';
1526
1696
  /**
1527
1697
  * Oat-styled text input implementing `FormValueControl<string>`.
@@ -1750,5 +1920,5 @@ interface TooltipPositioner {
1750
1920
  /** DI token for tooltip positioning strategy */
1751
1921
  declare const TOOLTIP_POSITIONER: InjectionToken<TooltipPositioner>;
1752
1922
 
1753
- 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 };
1754
- export type { NgOatAccordionItem, NgOatAlertVariant, NgOatAvatarSize, NgOatBadgeVariant, NgOatButtonSize, NgOatButtonStyle, NgOatButtonVariant, NgOatCarouselAspectRatio, NgOatCarouselOrientation, NgOatCarouselSlide, NgOatChipGroupHost, NgOatChipSize, NgOatChipVariant, NgOatInputType, NgOatOptions, NgOatProductCard, NgOatRadioOption, NgOatSelectOption, NgOatSeparatorOrientation, NgOatSkeletonType, NgOatSpinnerSize, NgOatTabItem, NgOatTableColumn, NgOatThemeConfig, NgOatToastOptions, NgOatToastPlacement, NgOatToastVariant, NgOatToggleGroupHost, NgOatToggleGroupSize, NgOatToggleGroupVariant, NgOatToggleSize, NgOatToggleVariant, OatBreadcrumbItem, OatTokens, Placement, TooltipPositioner };
1923
+ 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 };
1924
+ export type { NgOatAccordionItem, NgOatAlertVariant, NgOatAvatarSize, NgOatBadgeVariant, NgOatButtonSize, NgOatButtonStyle, NgOatButtonVariant, NgOatCarouselAspectRatio, NgOatCarouselOrientation, NgOatCarouselSlide, NgOatChipGroupHost, NgOatChipSize, NgOatChipVariant, NgOatInputType, NgOatOptions, NgOatProductCard, NgOatRadioOption, NgOatSelectOption, NgOatSeparatorOrientation, NgOatSkeletonType, NgOatSpinnerSize, NgOatTabItem, NgOatTableColumn, NgOatTheme, NgOatThemeConfig, NgOatThemeOption, NgOatToastOptions, NgOatToastPlacement, NgOatToastVariant, NgOatToggleGroupHost, NgOatToggleGroupSize, NgOatToggleGroupVariant, NgOatToggleSize, NgOatToggleVariant, NgOatToolbarColor, OatBreadcrumbItem, OatTokens, Placement, TooltipPositioner };
@@ -1,107 +0,0 @@
1
- // oat - Base Web Component Class
2
- // Provides lifecycle management, event handling, and utilities.
3
-
4
- export class OtBase extends HTMLElement {
5
- #initialized = false;
6
-
7
- // Called when element is added to DOM.
8
- connectedCallback() {
9
- if (this.#initialized) return;
10
-
11
- // Wait for DOM to be ready.
12
- if (document.readyState === 'loading') {
13
- document.addEventListener('DOMContentLoaded', () => this.#setup(), { once: true });
14
- } else {
15
- this.#setup();
16
- }
17
- }
18
-
19
- // Private setup to ensure that init() is only called once.
20
- #setup() {
21
- if (this.#initialized) return;
22
- this.#initialized = true;
23
- this.init();
24
- }
25
-
26
- // Called when element is removed from DOM.
27
- disconnectedCallback() {
28
- this.cleanup();
29
- }
30
-
31
- // Override in subclass for cleanup logic.
32
- cleanup() {}
33
-
34
- // Central event handler - enables automatic cleanup.
35
- // Usage: element.addEventListener('click', this)
36
- handleEvent(event) {
37
- const handler = this[`on${event.type}`];
38
- if (handler) handler.call(this, event);
39
- }
40
-
41
- // Given a keyboard event (left, right, home, end), the current selection idx
42
- // total items in a list, return 0-n index of the next/previous item
43
- // for doing a roving keyboard nav.
44
- keyNav(event, idx, len, prevKey, nextKey, homeEnd = false) {
45
- const { key } = event;
46
- let next = -1;
47
-
48
- if (key === nextKey) {
49
- next = (idx + 1) % len;
50
- } else if (key === prevKey) {
51
- next = (idx - 1 + len) % len;
52
- } else if (homeEnd) {
53
- if (key === 'Home') {
54
- next = 0;
55
- } else if (key === 'End') {
56
- next = len - 1;
57
- }
58
- }
59
-
60
- if (next >= 0) event.preventDefault();
61
- return next;
62
- }
63
-
64
- // Emit a custom event.
65
- emit(name, detail = null) {
66
- return this.dispatchEvent(new CustomEvent(name, {
67
- bubbles: true,
68
- composed: true,
69
- cancelable: true,
70
- detail
71
- }));
72
- }
73
-
74
- // Query selector within this element.
75
- $(selector) {
76
- return this.querySelector(selector);
77
- }
78
-
79
- // Query selector all within this element.
80
- $$(selector) {
81
- return Array.from(this.querySelectorAll(selector));
82
- }
83
-
84
- // Generate a unique ID string.
85
- uid() {
86
- return Math.random().toString(36).slice(2, 10);
87
- }
88
- }
89
-
90
- // Polyfill for command/commandfor (Safari)
91
- if (!('commandForElement' in HTMLButtonElement.prototype)) {
92
- document.addEventListener('click', e => {
93
- const btn = e.target.closest('button[commandfor]');
94
- if (!btn) return;
95
-
96
- const target = document.getElementById(btn.getAttribute('commandfor'));
97
- if (!target) return;
98
-
99
- const command = btn.getAttribute('command') || 'toggle';
100
-
101
- if (target instanceof HTMLDialogElement) {
102
- if (command === 'show-modal') target.showModal();
103
- else if (command === 'close') target.close();
104
- else target.open ? target.close() : target.showModal();
105
- }
106
- });
107
- }
@@ -1,74 +0,0 @@
1
- /**
2
- * oat - Dropdown Component
3
- * Provides positioning, keyboard navigation, and ARIA state management.
4
- *
5
- * Usage:
6
- * <ot-dropdown>
7
- * <button popovertarget="menu-id">Options</button>
8
- * <menu popover id="menu-id">
9
- * <button role="menuitem">Item 1</button>
10
- * <button role="menuitem">Item 2</button>
11
- * </menu>
12
- * </ot-dropdown>
13
- */
14
-
15
- import { OtBase } from './base.js';
16
-
17
- class OtDropdown extends OtBase {
18
- #menu;
19
- #trigger;
20
- #position;
21
- #items;
22
-
23
- init() {
24
- this.#menu = this.$('[popover]');
25
- this.#trigger = this.$('[popovertarget]');
26
-
27
- if (!this.#menu || !this.#trigger) return;
28
-
29
- this.#menu.addEventListener('toggle', this);
30
- this.#menu.addEventListener('keydown', this);
31
-
32
- this.#position = () => {
33
- // Position has to be calculated and applied manually because
34
- // popover positioning is like fixed, relative to the window.
35
- const r = this.#trigger.getBoundingClientRect();
36
- const m = this.#menu.getBoundingClientRect();
37
-
38
- // Flip if menu overflows viewport.
39
- this.#menu.style.top = `${r.bottom + m.height > window.innerHeight ? r.top - m.height : r.bottom}px`;
40
- this.#menu.style.left = `${r.left + m.width > window.innerWidth ? r.right - m.width : r.left}px`;
41
- };
42
- }
43
-
44
- ontoggle(e) {
45
- if (e.newState === 'open') {
46
- this.#position();
47
- window.addEventListener('scroll', this.#position, true);
48
- window.addEventListener('resize', this.#position);
49
- this.#items = this.$$('[role="menuitem"]');
50
- this.#items[0]?.focus();
51
- this.#trigger.ariaExpanded = 'true';
52
- } else {
53
- this.cleanup();
54
- this.#items = null;
55
- this.#trigger.ariaExpanded = 'false';
56
- this.#trigger.focus();
57
- }
58
- }
59
-
60
- onkeydown(e) {
61
- if (!e.target.matches('[role="menuitem"]')) return;
62
-
63
- const idx = this.#items.indexOf(e.target);
64
- const next = this.keyNav(e, idx, this.#items.length, 'ArrowUp', 'ArrowDown', true);
65
- if (next >= 0) this.#items[next].focus();
66
- }
67
-
68
- cleanup() {
69
- window.removeEventListener('scroll', this.#position, true);
70
- window.removeEventListener('resize', this.#position);
71
- }
72
- }
73
-
74
- customElements.define('ot-dropdown', OtDropdown);
@@ -1,12 +0,0 @@
1
- import './base.js';
2
- import './tabs.js';
3
- import './dropdown.js';
4
- import './tooltip.js';
5
- import './sidebar.js';
6
- import { toast, toastEl, toastClear } from './toast.js';
7
-
8
- // Register the global window.ot.* APIs.
9
- const ot = window.ot || (window.ot = {});
10
- ot.toast = toast;
11
- ot.toast.el = toastEl;
12
- ot.toast.clear = toastClear;
@@ -1,22 +0,0 @@
1
- /**
2
- * Sidebar toggle handler
3
- * Toggles data-sidebar-open on layout when toggle button is clicked
4
- */
5
- document.addEventListener('click', (e) => {
6
- const toggle = e.target.closest('[data-sidebar-toggle]');
7
- if (toggle) {
8
- const layout = toggle.closest('[data-sidebar-layout]');
9
- layout?.toggleAttribute('data-sidebar-open');
10
- return;
11
- }
12
-
13
- // Dismiss sidebar when clicking outside (when sidebar is not an overlay).
14
- if (!e.target.closest('[data-sidebar]')) {
15
- const layout = document.querySelector('[data-sidebar-layout][data-sidebar-open]');
16
- // Hardcode breakpoint (for now) as there's no way to use a CSS variable in
17
- // the @media{} query which could've been picked up here.
18
- if (layout && window.matchMedia('(max-width: 768px)').matches) {
19
- layout.removeAttribute('data-sidebar-open');
20
- }
21
- }
22
- });
@@ -1,94 +0,0 @@
1
- /**
2
- * oat - Tabs Component
3
- * Provides keyboard navigation and ARIA state management.
4
- *
5
- * Usage:
6
- * <ot-tabs>
7
- * <div role="tablist">
8
- * <button role="tab">Tab 1</button>
9
- * <button role="tab">Tab 2</button>
10
- * </div>
11
- * <div role="tabpanel">Content 1</div>
12
- * <div role="tabpanel">Content 2</div>
13
- * </ot-tabs>
14
- */
15
-
16
- import { OtBase } from './base.js';
17
-
18
- class OtTabs extends OtBase {
19
- #tabs = [];
20
- #panels = [];
21
-
22
- init() {
23
- const tablist = this.$(':scope > [role="tablist"]');
24
- this.#tabs = tablist ? [...tablist.querySelectorAll('[role="tab"]')] : [];
25
- this.#panels = this.$$(':scope > [role="tabpanel"]');
26
-
27
- if (this.#tabs.length === 0 || this.#panels.length === 0) {
28
- console.warn('ot-tabs: Missing tab or tabpanel elements');
29
- return;
30
- }
31
-
32
- // Generate IDs and set up ARIA.
33
- this.#tabs.forEach((tab, i) => {
34
- const panel = this.#panels[i];
35
- if (!panel) return;
36
-
37
- const tabId = tab.id || `ot-tab-${this.uid()}`;
38
- const panelId = panel.id || `ot-panel-${this.uid()}`;
39
-
40
- tab.id = tabId;
41
- panel.id = panelId;
42
- tab.setAttribute('aria-controls', panelId);
43
- panel.setAttribute('aria-labelledby', tabId);
44
- });
45
-
46
- tablist.addEventListener('click', this);
47
- tablist.addEventListener('keydown', this);
48
-
49
- // Find initially active tab or default to first.
50
- const activeTab = this.#tabs.findIndex(t => t.ariaSelected === 'true');
51
- this.#activate(activeTab >= 0 ? activeTab : 0);
52
- }
53
-
54
- onclick(e) {
55
- const index = this.#tabs.indexOf(e.target.closest('[role="tab"]'));
56
- if (index >= 0) this.#activate(index);
57
- }
58
-
59
- onkeydown(e) {
60
- if (!e.target.closest('[role="tab"]')) return;
61
-
62
- const next = this.keyNav(e, this.activeIndex, this.#tabs.length, 'ArrowLeft', 'ArrowRight');
63
- if (next >= 0) {
64
- this.#activate(next);
65
- this.#tabs[next].focus();
66
- }
67
- }
68
-
69
- #activate(idx) {
70
- this.#tabs.forEach((tab, i) => {
71
- const isActive = i === idx;
72
- tab.ariaSelected = String(isActive);
73
- tab.tabIndex = isActive ? 0 : -1;
74
- });
75
-
76
- this.#panels.forEach((panel, i) => {
77
- panel.hidden = i !== idx;
78
- });
79
-
80
- this.emit('ot-tab-change', { index: idx, tab: this.#tabs[idx] });
81
- }
82
-
83
- get activeIndex() {
84
- return this.#tabs.findIndex(t => t.ariaSelected === 'true');
85
- }
86
-
87
- set activeIndex(value) {
88
- if (value >= 0 && value < this.#tabs.length) {
89
- this.#activate(value);
90
- }
91
- }
92
- }
93
-
94
- customElements.define('ot-tabs', OtTabs);