@wildmason/aegis 1.13.0 → 1.14.1

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,6 +1,6 @@
1
1
  import * as _angular_core from '@angular/core';
2
2
  import { ElementRef, AfterViewInit, TemplateRef, OnDestroy } from '@angular/core';
3
- import { ConnectedPosition } from '@angular/cdk/overlay';
3
+ import { ScrollStrategy, OverlayRef, ConnectedPosition } from '@angular/cdk/overlay';
4
4
  import { ControlValueAccessor } from '@angular/forms';
5
5
  import * as rxjs from 'rxjs';
6
6
 
@@ -110,6 +110,26 @@ interface SelectOption {
110
110
  disabled?: boolean;
111
111
  }
112
112
 
113
+ /**
114
+ * A scroll strategy that keeps a connected overlay anchored to its trigger,
115
+ * including when the thing that scrolled was an ordinary `overflow: auto`
116
+ * element rather than the document.
117
+ *
118
+ * CDK's own reposition strategy does the right thing and is simply never told:
119
+ * it listens through `ScrollDispatcher`, which cannot hear a scroll from a
120
+ * container that is not marked `cdkScrollable`. See `onViewportChange` for the
121
+ * full account. This strategy listens in the capture phase instead, so the
122
+ * consumer annotates nothing.
123
+ */
124
+ declare class WmAnchoredScrollStrategy implements ScrollStrategy {
125
+ private overlayRef;
126
+ private release;
127
+ attach(overlayRef: OverlayRef): void;
128
+ enable(): void;
129
+ disable(): void;
130
+ detach(): void;
131
+ }
132
+
113
133
  /**
114
134
  * WmSelect — Accessible dropdown select built on Angular CDK Overlay.
115
135
  *
@@ -143,6 +163,16 @@ interface SelectOption {
143
163
  * - Always pair with a visible label via <wm-label> or aria-label on the host
144
164
  */
145
165
  declare class WmSelect {
166
+ /**
167
+ * Keeps the panel anchored to its trigger while an ancestor scrolls.
168
+ *
169
+ * One instance per component, because a ScrollStrategy holds a single
170
+ * overlayRef and could not tell two open overlays apart. Without it the
171
+ * panel takes CDK's default, which cannot hear a scroll from any container
172
+ * that is not marked `cdkScrollable` - so in a fixed-height shell the panel
173
+ * keeps the coordinates it was born with while the trigger scrolls away.
174
+ */
175
+ protected readonly scrollStrategy: WmAnchoredScrollStrategy;
146
176
  /** Array of options to display. */
147
177
  options: _angular_core.InputSignal<SelectOption[]>;
148
178
  /** The currently selected value. Two-way bindable. */
@@ -202,6 +232,16 @@ declare class WmSelect {
202
232
  * Import WmAutoFocus from @wildmason/aegis/components and add it to your component's imports.
203
233
  */
204
234
  declare class WmPopover {
235
+ /**
236
+ * Keeps the panel anchored to its trigger while an ancestor scrolls.
237
+ *
238
+ * One instance per component, because a ScrollStrategy holds a single
239
+ * overlayRef and could not tell two open overlays apart. Without it the
240
+ * panel takes CDK's default, which cannot hear a scroll from any container
241
+ * that is not marked `cdkScrollable` - so in a fixed-height shell the panel
242
+ * keeps the coordinates it was born with while the trigger scrolls away.
243
+ */
244
+ protected readonly scrollStrategy: WmAnchoredScrollStrategy;
205
245
  /** Whether the panel is currently open. */
206
246
  readonly isOpen: _angular_core.InputSignal<boolean>;
207
247
  /** Panel width in pixels, or 'auto' to match content. */
@@ -306,7 +346,7 @@ declare class WmButton {
306
346
  readonly variant: _angular_core.InputSignal<ButtonVariant>;
307
347
  readonly size: _angular_core.InputSignal<ButtonSize>;
308
348
  readonly disabled: _angular_core.InputSignal<boolean>;
309
- readonly type: _angular_core.InputSignal<"button" | "submit" | "reset">;
349
+ readonly type: _angular_core.InputSignal<"reset" | "submit" | "button">;
310
350
  /** Makes the button square (equal width/height). Use for icon-only buttons. */
311
351
  readonly iconOnly: _angular_core.InputSignal<boolean>;
312
352
  /** Accessible label forwarded to the inner <button>. Required for icon-only buttons. */
@@ -595,6 +635,16 @@ declare class ComboboxPanel {
595
635
  * <wm-combobox formControlName="tag" [options]="tags" />
596
636
  */
597
637
  declare class WmCombobox implements ControlValueAccessor, OnDestroy {
638
+ /**
639
+ * Keeps the panel anchored to its trigger while an ancestor scrolls.
640
+ *
641
+ * One instance per component, because a ScrollStrategy holds a single
642
+ * overlayRef and could not tell two open overlays apart. Without it the
643
+ * panel takes CDK's default, which cannot hear a scroll from any container
644
+ * that is not marked `cdkScrollable` - so in a fixed-height shell the panel
645
+ * keeps the coordinates it was born with while the trigger scrolls away.
646
+ */
647
+ protected readonly scrollStrategy: WmAnchoredScrollStrategy;
598
648
  /** Full option list (before filtering). */
599
649
  readonly options: _angular_core.InputSignal<ComboboxOption[]>;
600
650
  /** Selected value (single-select). Two-way bindable. */
@@ -774,6 +824,164 @@ declare class WmToastContainer implements OnDestroy {
774
824
  static ɵcmp: _angular_core.ɵɵComponentDeclaration<WmToastContainer, "wm-toast-container", never, {}, {}, never, never, true, never>;
775
825
  }
776
826
 
827
+ /** Where the panel sits relative to its host, before any viewport fallback. */
828
+ type WmTooltipPosition = 'top' | 'bottom' | 'left' | 'right';
829
+ /**
830
+ * How the panel is related to the host for assistive technology.
831
+ *
832
+ * `description` points the host at the panel with `aria-describedby`. Use it
833
+ * when the host already has a name and the tooltip adds detail — an icon with
834
+ * an `aria-label`, a truncated cell, a status mark.
835
+ *
836
+ * `label` uses `aria-labelledby` instead, for an icon-only control whose only
837
+ * name is the tooltip. Do not use it alongside an `aria-label` on the same
838
+ * host: `aria-labelledby` wins, and the two texts then disagree about what the
839
+ * control is called depending on whether the tooltip happens to be open.
840
+ */
841
+ type WmTooltipRelation = 'description' | 'label';
842
+ /**
843
+ * WmTooltip — accessible floating explanation for any element.
844
+ *
845
+ * ```html
846
+ * <button class="wm-btn btn-icon-only" aria-label="Undo" [wmTooltip]="'Undo the last commit'">…</button>
847
+ * ```
848
+ *
849
+ * Why this exists next to the CSS `[data-tooltip]` chip: that chip is a
850
+ * pseudo-element on the host, so it cannot escape an ancestor's
851
+ * `overflow: hidden`, cannot wrap, and is invisible to assistive technology —
852
+ * it is a visual hint that must be paired with an `aria-label` saying the same
853
+ * thing twice. Reach for the chip for a one-to-five word label on a toolbar
854
+ * button. Reach for this directive when the text explains something, when the
855
+ * host sits inside a scrolling container, or when the explanation must be part
856
+ * of the accessible description rather than a duplicate of it.
857
+ *
858
+ * Accessibility:
859
+ * - The panel carries `role="tooltip"` and a generated id.
860
+ * - While open, the host points at that id with `aria-describedby` (default)
861
+ * or `aria-labelledby`. The id is appended to whatever the host already
862
+ * referenced and removed again on hide, so an application's own description
863
+ * survives untouched.
864
+ * - Opens on hover and on `:focus-visible`, so a mouse click that moves focus
865
+ * to a button does not leave a tooltip hanging behind the pointer.
866
+ * - WCAG 1.4.13 (Content on Hover or Focus): Escape dismisses without moving
867
+ * focus; the pointer can travel into the panel and read it without it
868
+ * closing; nothing auto-dismisses on a timer.
869
+ *
870
+ * Keyboard reachability is the caller's responsibility. A tooltip on an element
871
+ * that cannot take focus is unreachable by keyboard, so set
872
+ * `wmTooltipFocusable` to make the host a tab stop — but NOT inside a
873
+ * roving-tabindex container (tree, listbox, grid, tablist). There the container
874
+ * is a single tab stop by design, and adding one stop per row is worse for
875
+ * keyboard users than the gap it closes. Put the tooltip on the row itself, or
876
+ * expose it from a control the roving focus already reaches.
877
+ */
878
+ declare class WmTooltip {
879
+ /** Tooltip body. Blank or absent means the tooltip never opens. */
880
+ readonly wmTooltip: _angular_core.InputSignal<string>;
881
+ /** Preferred side. Flips automatically when the viewport has no room. */
882
+ readonly wmTooltipPosition: _angular_core.InputSignal<WmTooltipPosition>;
883
+ /** Milliseconds a pointer must rest on the host before the panel opens. */
884
+ readonly wmTooltipDelay: _angular_core.InputSignal<number>;
885
+ /** Suppress the tooltip without unbinding it — for a control whose explanation only applies in some states. */
886
+ readonly wmTooltipDisabled: _angular_core.InputSignal<boolean>;
887
+ /** Whether the panel describes the host or names it. See WmTooltipRelation. */
888
+ readonly wmTooltipRelation: _angular_core.InputSignal<WmTooltipRelation>;
889
+ /**
890
+ * Make a non-focusable host a tab stop so a keyboard user can reach the
891
+ * tooltip. Only applied when the host is not already focusable, and never
892
+ * safe inside a roving-tabindex container — see the class comment.
893
+ */
894
+ readonly wmTooltipFocusable: _angular_core.InputSignal<boolean>;
895
+ private readonly host;
896
+ private readonly injector;
897
+ private readonly renderer;
898
+ private readonly panelId;
899
+ private overlayRef;
900
+ private panelRef;
901
+ private portal;
902
+ private showTimer;
903
+ private hideTimer;
904
+ private raiseFrame;
905
+ private pointerOnHost;
906
+ private pointerOnPanel;
907
+ private hostFocused;
908
+ private detachDocumentListeners;
909
+ /**
910
+ * Elements between the host and the document that can clip it, resolved
911
+ * once per open. They cannot change while the panel is up, and walking the
912
+ * ancestor chain on every scroll event would be work done per frame.
913
+ */
914
+ private clippingAncestors;
915
+ constructor();
916
+ /** Open the tooltip immediately, bypassing the hover delay. */
917
+ show(): void;
918
+ /** Close the tooltip immediately, without the pointer grace period. */
919
+ hideNow(): void;
920
+ /** True while the panel is attached. Exposed for tests and for callers driving it manually. */
921
+ get isOpen(): boolean;
922
+ protected onPointerEnterHost(): void;
923
+ protected onPointerLeaveHost(): void;
924
+ protected onFocusHost(): void;
925
+ protected onBlurHost(): void;
926
+ private text;
927
+ private scheduleShow;
928
+ private scheduleHide;
929
+ private clearTimer;
930
+ private ensureOverlay;
931
+ private positions;
932
+ private readonly onPointerEnterPanel;
933
+ private readonly onPointerLeavePanel;
934
+ private attachDocumentListeners;
935
+ private releaseDocumentListeners;
936
+ /**
937
+ * Follow the host, or give up on it.
938
+ *
939
+ * Repositioning alone is not enough. Once the host has scrolled out of its
940
+ * container the panel would be pushed against the container edge and left
941
+ * explaining a row the reader can no longer see - worse than not being there,
942
+ * because it now appears to describe whatever it happens to cover.
943
+ */
944
+ private reanchor;
945
+ /** Every ancestor that can clip the host, nearest first. */
946
+ private resolveClippingAncestors;
947
+ /** True once no part of the host is inside every container that clips it. */
948
+ private isHostClipped;
949
+ private relationAttribute;
950
+ private linkHostToPanel;
951
+ private unlinkHostFromPanel;
952
+ private isNativelyFocusable;
953
+ private matchesFocusVisible;
954
+ private teardown;
955
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<WmTooltip, never>;
956
+ static ɵdir: _angular_core.ɵɵDirectiveDeclaration<WmTooltip, "[wmTooltip]", ["wmTooltip"], { "wmTooltip": { "alias": "wmTooltip"; "required": false; "isSignal": true; }; "wmTooltipPosition": { "alias": "wmTooltipPosition"; "required": false; "isSignal": true; }; "wmTooltipDelay": { "alias": "wmTooltipDelay"; "required": false; "isSignal": true; }; "wmTooltipDisabled": { "alias": "wmTooltipDisabled"; "required": false; "isSignal": true; }; "wmTooltipRelation": { "alias": "wmTooltipRelation"; "required": false; "isSignal": true; }; "wmTooltipFocusable": { "alias": "wmTooltipFocusable"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
957
+ }
958
+
959
+ /**
960
+ * Notify a handler whenever anything that can move an anchored overlay moves.
961
+ *
962
+ * This exists because of one property of the DOM that CDK's `ScrollDispatcher`
963
+ * does not account for: **`scroll` does not bubble.**
964
+ *
965
+ * `ScrollDispatcher` has exactly two sources. It listens for `scroll` on the
966
+ * document in the *bubbling* phase, which therefore only ever hears the
967
+ * document itself scrolling; and it hears containers that registered
968
+ * themselves through a `cdkScrollable` directive. In an application shell that
969
+ * is a fixed-height frame with an inner scrolling element — which is every
970
+ * Wildmason product — the document never scrolls and nothing is registered, so
971
+ * both sources are empty and any scroll strategy built on the dispatcher is a
972
+ * silent no-op. The overlay keeps the coordinates it was born with while its
973
+ * trigger scrolls out from under it.
974
+ *
975
+ * A **capture-phase** listener on the document sees the event from any
976
+ * descendant, so nothing has to be annotated. Requiring every consumer to mark
977
+ * their scroll containers — and failing invisibly when they forget — is not a
978
+ * contract a design system should hand out.
979
+ *
980
+ * @param handler called on any scroll or resize while the subscription is live
981
+ * @returns a function that removes exactly what was added
982
+ */
983
+ declare function onViewportChange(handler: () => void): () => void;
984
+
777
985
  interface LicenseStatus {
778
986
  valid: boolean;
779
987
  org: string | null;
@@ -843,5 +1051,5 @@ declare class WmLicenseDialog {
843
1051
  static ɵcmp: _angular_core.ɵɵComponentDeclaration<WmLicenseDialog, "wm-license-dialog", never, { "visible": { "alias": "visible"; "required": true; "isSignal": true; }; }, { "dismiss": "dismiss"; }, never, never, true, never>;
844
1052
  }
845
1053
 
846
- export { Checkbox, GhostField, NumberInput, PageHeader, Toggle, WmAutoFocus, WmButton, WmCombobox, WmDialog, WmLicenseDialog, WmLicenseNudge, WmLicenseService, WmModal, WmPopover, WmSelect, WmTab, WmTabs, WmToastContainer, WmToastService };
847
- export type { ButtonSize, ButtonVariant, ComboboxOption, DialogSize, LicenseStatus, SelectOption, Toast, ToastType, WmDeclaredUseType };
1054
+ export { Checkbox, GhostField, NumberInput, PageHeader, Toggle, WmAnchoredScrollStrategy, WmAutoFocus, WmButton, WmCombobox, WmDialog, WmLicenseDialog, WmLicenseNudge, WmLicenseService, WmModal, WmPopover, WmSelect, WmTab, WmTabs, WmToastContainer, WmToastService, WmTooltip, onViewportChange };
1055
+ export type { ButtonSize, ButtonVariant, ComboboxOption, DialogSize, LicenseStatus, SelectOption, Toast, ToastType, WmDeclaredUseType, WmTooltipPosition, WmTooltipRelation };
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "@wildmason/aegis",
3
- "version": "1.13.0",
3
+ "version": "1.14.1",
4
4
  "description": "Aegis design system \u00e2\u20ac\u201d CSS tokens and Angular component library",
5
5
  "scripts": {
6
6
  "check:offline-fonts": "node scripts/check-offline-fonts.mjs",
7
- "build": "npm run check:offline-fonts && npm run check:destructive-contrast && npm run check:semantic-contrast && ng-packagr -p ng-package.json",
7
+ "build": "npm run check:offline-fonts && npm run check:destructive-contrast && npm run check:semantic-contrast && npm run check:tooltip-contrast && ng-packagr -p ng-package.json",
8
8
  "prepublishOnly": "npm run build",
9
9
  "check:destructive-contrast": "node scripts/check-destructive-contrast.mjs",
10
- "check:semantic-contrast": "node scripts/check-semantic-contrast.mjs"
10
+ "check:semantic-contrast": "node scripts/check-semantic-contrast.mjs",
11
+ "check:tooltip-contrast": "node scripts/check-tooltip-contrast.mjs"
11
12
  },
12
13
  "files": [
13
14
  "dist",
package/themes.css CHANGED
@@ -269,7 +269,22 @@
269
269
  * those wearing the tone's own 8% tint — the tint is the binding case,
270
270
  * because it moves the background toward the text.
271
271
  *
272
- * Themes whose tone is already readable inherit it unchanged.
272
+ * EVERY THEME MUST STATE ALL FOUR `-readable` TOKENS. A theme that is happy
273
+ * with its own tone writes `var(--wm-color-<tone>)` and says so; it does not
274
+ * leave the declaration out. These aliases below are a floor for a theme
275
+ * being written, never a fallback a shipped theme may rely on, and
276
+ * `scripts/check-semantic-contrast.mjs` fails the build on an omission.
277
+ *
278
+ * The reason is a trap in this file's own shape. The warm-light block below
279
+ * is selected as `:root, [data-theme='warm-light']`, so it is BOTH the
280
+ * default theme AND a later bare-`:root` declaration. Anything it sets wins
281
+ * over these aliases for every theme that stays silent — which means a
282
+ * silent dark theme receives warm-light's dark-on-light values. That is
283
+ * exactly what shipped in 1.12.0 and 1.13.0: five (theme, tone) pairs
284
+ * inherited `#553f00` / `#3c4801` and rendered at 1.08:1 to 1.38:1 — Matt
285
+ * found the wildmason one by eye, as an unreadable glyph in Helm's branch
286
+ * picker. Every other token in this file escaped only because every theme
287
+ * happens to declare it.
273
288
  */
274
289
  --wm-color-danger-readable: var(--wm-color-danger);
275
290
  --wm-color-warning-readable: var(--wm-color-warning);
@@ -572,6 +587,11 @@
572
587
  --wm-color-danger: #ff5555;
573
588
  --wm-color-danger-solid: #cf4545;
574
589
  --wm-color-danger-readable: #febdb7;
590
+ /* Stated rather than inherited, and that is the whole rule for these four —
591
+ see the note over the generic aliases in :root. Both of these tones already
592
+ read: 6.66:1 / Lc 82.4 and 5.65:1 / Lc 69.7 at their worst ground. */
593
+ --wm-color-warning-readable: var(--wm-color-warning);
594
+ --wm-color-success-readable: var(--wm-color-success);
575
595
 
576
596
  /* Item type colors */
577
597
  --wm-color-task: #8be9fd;
@@ -648,6 +668,10 @@
648
668
  --wm-color-danger-solid: #b65b5b;
649
669
  --wm-color-danger-readable: #fea7a4;
650
670
  --wm-color-success-readable: #6dcfb0;
671
+ /* #f0b464 reads on the bare surfaces but lands under Lc 61 on its own 8%
672
+ tint over --wm-bg-float. Hue held, chroma at 95%, lightness moved only:
673
+ 6.43:1 / Lc 61.1 at the worst ground. */
674
+ --wm-color-warning-readable: #eeb56a;
651
675
 
652
676
  /* Item type colors */
653
677
  --wm-color-task: #70a8f0;
@@ -873,6 +897,8 @@
873
897
  --wm-color-danger-solid: #e02267;
874
898
  --wm-color-danger-readable: #ffb1bf;
875
899
  --wm-color-warning-readable: #feb878;
900
+ /* Already reads at 5.96:1 / Lc 65.8; stated so it cannot inherit. */
901
+ --wm-color-success-readable: var(--wm-color-success);
876
902
 
877
903
  /* Item type colors */
878
904
  --wm-color-task: #66d9ef;
@@ -1105,6 +1131,11 @@
1105
1131
  --wm-color-danger-solid: #b45b64;
1106
1132
  --wm-color-danger-readable: #febec1;
1107
1133
  --wm-color-success-readable: #b2d8b2;
1134
+ /* The token Matt caught: this theme inherited warm-light's #553f00 and drew
1135
+ the branch picker's stash glyph at 1.11:1 — a dark brown on a dark grey.
1136
+ #ebcb8b itself lands just under Lc 61 on its own tint over --wm-bg-float,
1137
+ so hue held, chroma at 90%: 4.91:1 / Lc 61.1 at the worst ground. */
1138
+ --wm-color-warning-readable: #e8cc93;
1108
1139
 
1109
1140
  /* Item type colors — Wildmason selection */
1110
1141
  --wm-color-task: #82a682;