@elasticias/ui 1.0.42 → 1.0.43

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": "@elasticias/ui",
3
- "version": "1.0.42",
3
+ "version": "1.0.43",
4
4
  "peerDependencies": {
5
5
  "@angular/common": "^21.0.0",
6
6
  "@angular/core": "^21.0.0",
@@ -765,12 +765,24 @@
765
765
  /* ──────────── RESULTS BAR + TABLE WRAPPER ──────────────────────── */
766
766
 
767
767
  .tbl-wrap {
768
+ min-width: 0;
768
769
  background: var(--paper-alt);
769
770
  border: 1px solid var(--rule);
770
771
  border-radius: var(--r-2xl);
771
772
  overflow: hidden;
772
773
  }
773
774
 
775
+ /* The table's own horizontal scroller, inside the clipping shell. A table
776
+ wider than its column (a tablet, or a desktop with the side panel open)
777
+ pans here rather than being cut off by .tbl-wrap or widening the page.
778
+ overscroll-behavior keeps a sideways drag that hits the end from
779
+ panning the page behind it. */
780
+ .tbl-scroll {
781
+ min-width: 0;
782
+ overflow-x: auto;
783
+ overscroll-behavior-x: contain;
784
+ }
785
+
774
786
  .tbl-head {
775
787
  display: flex;
776
788
  justify-content: space-between;
@@ -971,49 +983,66 @@
971
983
  }
972
984
 
973
985
  /* ──────────── APP-BAR ICON ACTIONS ──────────────────────────────
974
- The theme switch and sign-out sit at the trailing edge of
975
- ef-app-top. They borrow the row-actions trigger's shape rather
976
- than inventing a third kind of icon button. */
986
+ The theme switch, sign-out and any app action (`.ef-app-top__action`)
987
+ at the trailing edge of ef-app-top, and the theme switch in the login
988
+ header. Plain icons in the treatment of the phone's menu button
989
+ (`.ef-app-shell__menu`): no fill, no border, a 40px box, an 18px glyph.
990
+
991
+ They used to borrow the row-actions trigger's bordered pill, and the
992
+ touch tier grew that painted pill to 48px, so a tablet's top bar was a
993
+ row of big circles. The touch target now grows invisibly instead (see
994
+ the `pointer: coarse` block below): a thumb needs 48px, an eye does not.
995
+ The slot gap is 8px, so two 48px targets meet edge to edge and never
996
+ overlap.
997
+
998
+ Each carries an aria-label and a pTooltip (hover and focus), not a
999
+ `title`: a title never shows on touch and doubles PrimeNG's tooltip on
1000
+ a desktop. */
977
1001
 
978
1002
  .ef-theme-toggle,
979
1003
  .ef-app-top__logout,
980
1004
  .ef-app-top__action {
981
1005
  appearance: none;
982
- background: var(--paper-alt);
983
- border: 1px solid var(--rule);
984
- width: var(--hit);
985
- height: var(--hit);
986
- border-radius: var(--r-pill);
1006
+ position: relative;
1007
+ flex: 0 0 auto;
1008
+ background: transparent;
1009
+ border: 0;
1010
+ padding: 0;
1011
+ width: var(--hit-base);
1012
+ height: var(--hit-base);
1013
+ border-radius: var(--r-md);
987
1014
  display: inline-grid;
988
1015
  place-items: center;
989
1016
  color: var(--text-mute);
990
1017
  cursor: pointer;
991
1018
  transition:
992
1019
  background var(--t-fast) var(--ease-out),
993
- border-color var(--t-fast) var(--ease-out),
994
1020
  color var(--t-fast) var(--ease-out);
995
1021
  }
996
1022
 
997
- .ef-theme-toggle:hover,
998
- .ef-app-top__logout:hover,
999
- .ef-app-top__action:hover {
1000
- color: var(--text);
1001
- border-color: var(--rule-hover);
1023
+ /* Only where hover is real: iOS keeps :hover on the last element tapped,
1024
+ so the fill would stay on that icon until the next tap elsewhere. */
1025
+ @media (hover: hover) {
1026
+ .ef-theme-toggle:hover,
1027
+ .ef-app-top__logout:hover,
1028
+ .ef-app-top__action:hover {
1029
+ background: color-mix(in srgb, var(--text) 8%, transparent);
1030
+ color: var(--text);
1031
+ }
1002
1032
  }
1003
1033
 
1004
1034
  .ef-theme-toggle:focus-visible,
1005
1035
  .ef-app-top__logout:focus-visible,
1006
1036
  .ef-app-top__action:focus-visible {
1007
- outline: 0;
1008
- border-color: var(--tenant-400);
1009
- box-shadow: 0 0 0 4px var(--tenant-100);
1037
+ outline: 2px solid var(--tenant-400);
1038
+ outline-offset: 2px;
1010
1039
  color: var(--text);
1011
1040
  }
1012
1041
 
1013
1042
  .ef-theme-toggle i,
1014
1043
  .ef-app-top__logout i,
1015
1044
  .ef-app-top__action i {
1016
- font-size: 14px;
1045
+ font-size: 18px;
1017
1046
  line-height: 1;
1018
1047
  }
1019
1048
 
@@ -1554,6 +1583,18 @@
1554
1583
  inset-inline-start: 0;
1555
1584
  }
1556
1585
 
1586
+ /* Placed by ef-row-actions from the trigger's rect (inline top / right /
1587
+ bottom / left), so no scrolling or clipping ancestor can cut it off. */
1588
+ .row-actions__menu.is-floating {
1589
+ position: fixed;
1590
+ /* max-height is set inline from the room on the menu's side; border-box
1591
+ so the padding and border count inside it and a capped menu stays on
1592
+ screen. */
1593
+ box-sizing: border-box;
1594
+ overflow-y: auto;
1595
+ overscroll-behavior: contain;
1596
+ }
1597
+
1557
1598
  /* Not enough room below the trigger — flip above it, and reverse the
1558
1599
  entry slide so the menu still moves away from its trigger. */
1559
1600
  .row-actions__menu.up {
@@ -1795,9 +1836,12 @@
1795
1836
  min-width: 0;
1796
1837
  }
1797
1838
 
1798
- /* When a row-actions menu is open inside a data card, the table-wrap
1799
- should let the popover escape its overflow. The data-card binds
1800
- .has-open-menu when any descendant trigger is in `.open` state. */
1839
+ /* While the density or columns menu (in .tbl-head) is open, the
1840
+ table-wrap lets it escape its rounded clip; the data-card binds
1841
+ .has-open-menu for those two only. Row menus never need it: they are
1842
+ fixed to the viewport, because .tbl-scroll must keep its own overflow
1843
+ (lifting it would reset the reader's scroll position and spill the
1844
+ table across the page). */
1801
1845
  .tbl-wrap.has-open-menu {
1802
1846
  overflow: visible;
1803
1847
  }
@@ -4541,9 +4585,6 @@ input.ef-input[type='number'] {
4541
4585
  min-inline-size: var(--hit-touch);
4542
4586
  }
4543
4587
 
4544
- .ef-app-top__action,
4545
- .ef-theme-toggle,
4546
- .ef-app-top__logout,
4547
4588
  .row-actions__trigger,
4548
4589
  .ef-clear,
4549
4590
  .pg {
@@ -4551,30 +4592,28 @@ input.ef-input[type='number'] {
4551
4592
  block-size: var(--hit-touch);
4552
4593
  }
4553
4594
 
4554
- /* Toolbar buttons collapse to icon-only below 767px, so the blanket
4555
- min-block-size above stretched a 40px-wide button to 48px tall and
4556
- produced a lozenge four-across in the header. Here the VISUAL stays
4557
- 40px square and the TOUCH TARGET reaches the tier through a hit
4558
- slop, which is the point of the tier: a thumb needs 48px, an eye
4559
- does not. */
4560
- .detail-toolbar .right .btn,
4561
- .search-toolbar .right .btn {
4562
- position: relative;
4563
- inline-size: var(--hit-base);
4564
- block-size: var(--hit-base);
4565
- min-inline-size: 0;
4566
- min-block-size: 0;
4567
- padding: 0;
4568
- }
4569
-
4570
- .detail-toolbar .right .btn::after,
4571
- .search-toolbar .right .btn::after {
4595
+ /* The top-bar icons keep their 40px painted box and reach the 48px
4596
+ tier through an invisible overhang, 4px each side. The 8px slot gap
4597
+ means neighbouring overhangs meet and never overlap, so a tap always
4598
+ lands on the control nearest it. */
4599
+ .ef-app-top__action::after,
4600
+ .ef-theme-toggle::after,
4601
+ .ef-app-top__logout::after {
4572
4602
  content: '';
4573
4603
  position: absolute;
4574
- /* (48 - 40) / 2 outward on every side */
4575
4604
  inset: calc((var(--hit-touch) - var(--hit-base)) / -2);
4576
4605
  }
4577
4606
 
4607
+ /* Where the lib's controls meet whatever the app projects into the
4608
+ actions slot, the gap is 12px rather than 8. An app control can carry
4609
+ a wider overhang than ours: the phone avatar paints 32px inside a
4610
+ 48px target, 8px each side. 8 + 4 needs 12px between the painted
4611
+ boxes, or the theme switch takes the avatar's trailing edge. */
4612
+ ef-app-top .ef-app-top__slot--actions > * + ef-theme-toggle,
4613
+ ef-app-top .ef-app-top__slot--actions > :not(ef-theme-toggle) + .ef-app-top__logout {
4614
+ margin-inline-start: 4px;
4615
+ }
4616
+
4578
4617
  /* The switch draws at its theme size (Aura: 40x24) and its click
4579
4618
  target is PrimeNG's invisible input, sized to exactly that box. In a
4580
4619
  list row a tap that lands just outside it did nothing, because the
@@ -4599,6 +4638,38 @@ input.ef-input[type='number'] {
4599
4638
  }
4600
4639
 
4601
4640
 
4641
+ /* Toolbar buttons collapse to icon-only below 767px, so the blanket
4642
+ min-block-size above stretched a 40px-wide button to 48px tall and
4643
+ produced a lozenge four-across in the header. Here the VISUAL stays
4644
+ 40px square and the TOUCH TARGET reaches the tier through a hit
4645
+ slop, which is the point of the tier: a thumb needs 48px, an eye
4646
+ does not.
4647
+
4648
+ Scoped to the same 767px as the rule that hides the label. Keyed on
4649
+ pointer type alone it also caught every tablet, which kept its label
4650
+ and got a 40px box: "Nouvelle commande" spilled out of the button and
4651
+ the detail actions piled into each other. Above 767px a touch screen
4652
+ keeps the labelled button, 48px tall from the generic rule above. */
4653
+ @media (pointer: coarse) and (max-width: 767px) {
4654
+ .detail-toolbar .right .btn,
4655
+ .search-toolbar .right .btn {
4656
+ position: relative;
4657
+ inline-size: var(--hit-base);
4658
+ block-size: var(--hit-base);
4659
+ min-inline-size: 0;
4660
+ min-block-size: 0;
4661
+ padding: 0;
4662
+ }
4663
+
4664
+ .detail-toolbar .right .btn::after,
4665
+ .search-toolbar .right .btn::after {
4666
+ content: '';
4667
+ position: absolute;
4668
+ /* (48 - 40) / 2 outward on every side */
4669
+ inset: calc((var(--hit-touch) - var(--hit-base)) / -2);
4670
+ }
4671
+ }
4672
+
4602
4673
  /* ── MOBILE BOTTOM BAR ──────────────────────────────────────────────
4603
4674
  The phone shell renders a 64px tab bar fixed to the bottom edge.
4604
4675
  Anything else pinned down there has to clear it, so the height is
@@ -980,6 +980,9 @@ declare class EfConfirmDialogComponent {
980
980
  * The icon shows the theme you would move to, not the one you are in, which
981
981
  * is what makes a single-button toggle readable: a moon means "go dark".
982
982
  *
983
+ * Named by `aria-label` and a pTooltip that opens on hover and on
984
+ * keyboard focus (not `title`, which never shows on touch).
985
+ *
983
986
  * ```html
984
987
  * <ef-theme-toggle />
985
988
  * ```
@@ -990,13 +993,18 @@ declare class EfThemeToggleComponent {
990
993
  readonly darkIcon: _angular_core.InputSignal<string>;
991
994
  /** Icon shown while dark is active (i.e. "switch to light"). */
992
995
  readonly lightIcon: _angular_core.InputSignal<string>;
993
- /** Translation key for the accessible name. */
996
+ /**
997
+ * Where the tooltip opens. Below suits a top bar (the app bar, the
998
+ * login header); a toggle placed low on a page can open it above.
999
+ */
1000
+ readonly tooltipPosition: _angular_core.InputSignal<"top" | "left" | "bottom" | "right">;
1001
+ /** Translation key for the accessible name (also the tooltip text). */
994
1002
  readonly ariaLabelKey: _angular_core.InputSignal<string>;
995
1003
  readonly isDark: _angular_core.Signal<boolean>;
996
1004
  protected readonly labelKey: _angular_core.Signal<string>;
997
1005
  toggle(): void;
998
1006
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<EfThemeToggleComponent, never>;
999
- static ɵcmp: _angular_core.ɵɵComponentDeclaration<EfThemeToggleComponent, "ef-theme-toggle", never, { "darkIcon": { "alias": "darkIcon"; "required": false; "isSignal": true; }; "lightIcon": { "alias": "lightIcon"; "required": false; "isSignal": true; }; "ariaLabelKey": { "alias": "ariaLabelKey"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
1007
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<EfThemeToggleComponent, "ef-theme-toggle", never, { "darkIcon": { "alias": "darkIcon"; "required": false; "isSignal": true; }; "lightIcon": { "alias": "lightIcon"; "required": false; "isSignal": true; }; "tooltipPosition": { "alias": "tooltipPosition"; "required": false; "isSignal": true; }; "ariaLabelKey": { "alias": "ariaLabelKey"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
1000
1008
  }
1001
1009
 
1002
1010
  /**
@@ -1579,7 +1587,8 @@ declare class EfToolbarComponent {
1579
1587
  *
1580
1588
  * **Desktop / tablet:** rail (64px) + side (240px) + (top + main). The
1581
1589
  * side folds away from a toggle at the foot of the rail (or mod+B); the
1582
- * rail never does.
1590
+ * rail never does. A tablet starts with it folded unless the viewer has
1591
+ * a stored choice (`EF_SIDE_COLLAPSED_KEY`).
1583
1592
  * **Mobile:** delegates to `<ef-app-shell-mobile>` — top bar + scrollable
1584
1593
  * body + 5-tab bottom bar + FAB slot.
1585
1594
  *
@@ -1601,7 +1610,20 @@ declare class EfAppShellComponent {
1601
1610
  readonly isMobile: _angular_core.Signal<boolean>;
1602
1611
  readonly showSide: _angular_core.Signal<boolean>;
1603
1612
  private readonly prefs;
1604
- readonly sideCollapsed: _angular_core.WritableSignal<boolean>;
1613
+ /** The viewer's explicit choice ('1' folded, '0' open), or null when they never made one. */
1614
+ private readonly storedSide;
1615
+ /**
1616
+ * Folded or open. An explicit choice always wins. Without one, a
1617
+ * tablet starts folded: at 768-1279px the 240px panel left a
1618
+ * phone-width column for a desktop table, so the right-hand columns
1619
+ * were cut off. A desktop keeps it open and a phone never renders it.
1620
+ *
1621
+ * The default is derived, never written: storing it would turn it
1622
+ * into a choice the viewer did not make, pin it when they rotate or
1623
+ * resize across the breakpoint, and with a server-backed store send
1624
+ * a request on every page load.
1625
+ */
1626
+ readonly sideCollapsed: _angular_core.Signal<boolean>;
1605
1627
  /** Shown in the toggle's tooltip: the button is far from the content, the shortcut is not. */
1606
1628
  readonly shortcutHint: string;
1607
1629
  toggleSide(): void;
@@ -1727,6 +1749,21 @@ declare class EfAppTopComponent {
1727
1749
  /** Present only when the app provides `EF_SESSION`. */
1728
1750
  protected readonly session: _elasticias_core.EfSession;
1729
1751
  protected signOut(): void;
1752
+ /** A touch or pen press at least this long is a hold, not a tap. */
1753
+ static readonly HOLD_MS = 500;
1754
+ /** True on a screen whose primary pointer cannot hover (a phone, a tablet). */
1755
+ protected readonly touchOnly: _angular_core.WritableSignal<boolean>;
1756
+ private pressStart;
1757
+ constructor();
1758
+ protected onSignOutPointerDown(event: PointerEvent): void;
1759
+ /**
1760
+ * A cancelled press (the browser took the gesture) must not judge a
1761
+ * later click. Not on pointerleave: on touch it fires after pointerup
1762
+ * and before the synthesised click, so clearing there would erase
1763
+ * every long press just before the guard reads it.
1764
+ */
1765
+ protected forgetSignOutPress(): void;
1766
+ protected onSignOutClick(event: MouseEvent): void;
1730
1767
  private readonly active;
1731
1768
  readonly module: _angular_core.Signal<_elasticias_core.EfModule>;
1732
1769
  readonly navItem: _angular_core.Signal<_elasticias_core.EfNavItem>;
@@ -4826,11 +4863,6 @@ declare class EfDataCardComponent<TRow = any> implements AfterContentInit {
4826
4863
  private storageKey;
4827
4864
  private loadPreferences;
4828
4865
  private savePreferences;
4829
- /** Tracks how many `ef-row-actions` are currently open inside this
4830
- * card. When > 0, `.tbl-wrap` lifts its `overflow: hidden` so the
4831
- * popup can escape its rounded edges. */
4832
- readonly openMenuCount: _angular_core.WritableSignal<number>;
4833
- onRowActionsToggle(event: Event): void;
4834
4866
  /**
4835
4867
  * Children that own their own click. Opening the row on top of them would
4836
4868
  * run two things from one gesture.
@@ -5003,6 +5035,9 @@ declare class EfOverflowTooltipDirective {
5003
5035
  * - Click the trigger to toggle. Trigger gets `.open` (ink-active) while the menu is open.
5004
5036
  * - Only one row menu is open at a time — opening one closes the last.
5005
5037
  * - The menu flips above the trigger when there isn't room below it.
5038
+ * - The menu is fixed to the viewport, placed from the trigger's rect, so a
5039
+ * table's horizontal scroller cannot clip it. Scrolling any ancestor or
5040
+ * resizing the window closes it, rather than leaving it behind its row.
5006
5041
  * - Click outside or press `Escape` to close.
5007
5042
  * - Clicking an item runs its `command` then closes the menu.
5008
5043
  * - dblclick on the trigger or menu does NOT propagate, so the data-card's
@@ -5077,14 +5112,54 @@ declare class EfRowActionsComponent {
5077
5112
  * below wins, which is where it has always been.
5078
5113
  */
5079
5114
  private shouldDropUp;
5115
+ /** Gap between the trigger and the menu. Matches the CSS `calc(100% + 8px)`. */
5116
+ private static readonly MENU_GAP;
5117
+ /**
5118
+ * Where the fixed menu sits, as physical offsets from the viewport
5119
+ * edges. The four values are always set, so they override the CSS
5120
+ * `top` / `bottom` / `inset-inline-*` an absolute menu uses.
5121
+ */
5122
+ readonly menuPosition: _angular_core.WritableSignal<{
5123
+ top: string;
5124
+ right: string;
5125
+ bottom: string;
5126
+ left: string;
5127
+ maxHeight: string;
5128
+ }>;
5129
+ /**
5130
+ * Fixed rather than absolute: inside ef-data-card the table sits in a
5131
+ * horizontal scroller, and `overflow-x: auto` makes the block axis clip
5132
+ * too, so an absolute menu was cut off at the scroller's edge. Lifting
5133
+ * the scroller's overflow while a menu is open would reset its
5134
+ * scrollLeft and spill the table across the page, exactly when the
5135
+ * reader has scrolled across to reach the actions column.
5136
+ */
5137
+ private place;
5138
+ /**
5139
+ * An ancestor with a transform, filter or containment becomes the fixed
5140
+ * menu's containing block (a dialog mid-animation, for one), and the
5141
+ * viewport offsets land in the wrong place. Measure where the menu
5142
+ * actually is against where the trigger says it should be, and shift
5143
+ * by the difference. Also nudges it back inside the viewport.
5144
+ */
5145
+ private correctOffset;
5146
+ /** The nearest `dir` wins (the app sets it on <html>); computed style covers a page without one. */
5147
+ private isRtl;
5148
+ private triggerRect;
5149
+ /** The fixed containing block: the viewport without its scrollbars. */
5150
+ private static viewportWidth;
5151
+ private static viewportHeight;
5152
+ private displacementListener;
5153
+ private listenForDisplacement;
5154
+ private stopListeningForDisplacement;
5080
5155
  /** Rough height before the menu exists: item padding + line box, with
5081
5156
  * dividers and the menu's own padding and border. */
5082
5157
  private estimateMenuHeight;
5083
5158
  /** Re-decide against the rendered height — item labels wrap, and the
5084
5159
  * estimate can't know that. */
5085
5160
  private correctPlacementAfterRender;
5086
- /** Bubbling DOM event so ancestors (e.g. ef-data-card) can react —
5087
- * see `.tbl-wrap.has-open-menu { overflow: visible }`. */
5161
+ /** Bubbling DOM event so ancestors can react to a row menu opening or
5162
+ * closing. ef-data-card no longer needs it (the menu is fixed). */
5088
5163
  private dispatchToggle;
5089
5164
  /** Outside click — closes the menu. */
5090
5165
  onDocumentClick(event: MouseEvent): void;