@elasticias/ui 1.0.42 → 1.0.44

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.44",
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,59 +983,85 @@
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
 
1020
1049
  /* ──────────── PAGER ─────────────────────────────────────────────── */
1021
1050
 
1051
+ /* Both halves wrap and can shrink. A row of controls has an intrinsic
1052
+ min-content width, so a pager that cannot wrap is as wide as its
1053
+ lines-per-page block plus every page button, and the card clips the
1054
+ rest: on a 390px phone, or in a narrow card on a tablet with the side
1055
+ panel open, "next" and the last page were cut off. When the two halves
1056
+ no longer fit on one line the page buttons take their own line, still
1057
+ at the end. */
1022
1058
  .pager {
1023
1059
  display: flex;
1060
+ flex-wrap: wrap;
1024
1061
  justify-content: space-between;
1025
1062
  align-items: center;
1026
1063
  gap: var(--s-3);
1064
+ min-width: 0;
1027
1065
  padding: var(--s-3) var(--s-5);
1028
1066
  border-top: 1px solid var(--rule);
1029
1067
  background: var(--paper-alt);
@@ -1031,8 +1069,21 @@
1031
1069
 
1032
1070
  .pager .pages {
1033
1071
  display: flex;
1072
+ flex-wrap: wrap;
1034
1073
  align-items: center;
1035
1074
  gap: 4px;
1075
+ min-width: 0;
1076
+ margin-inline-start: auto;
1077
+ }
1078
+
1079
+ /* The phone pager's "5 sur 20", between previous and next. */
1080
+ .pager .pg-status {
1081
+ padding: 0 8px;
1082
+ font-size: 12.5px;
1083
+ font-weight: 600;
1084
+ color: var(--text);
1085
+ font-variant-numeric: tabular-nums;
1086
+ white-space: nowrap;
1036
1087
  }
1037
1088
 
1038
1089
  .pager .pg {
@@ -1061,7 +1112,9 @@
1061
1112
 
1062
1113
  .pager .size {
1063
1114
  display: flex;
1115
+ flex-wrap: wrap;
1064
1116
  align-items: center;
1117
+ min-width: 0;
1065
1118
  gap: 8px;
1066
1119
  font-size: 12.5px;
1067
1120
  color: var(--text-mute);
@@ -1554,6 +1607,18 @@
1554
1607
  inset-inline-start: 0;
1555
1608
  }
1556
1609
 
1610
+ /* Placed by ef-row-actions from the trigger's rect (inline top / right /
1611
+ bottom / left), so no scrolling or clipping ancestor can cut it off. */
1612
+ .row-actions__menu.is-floating {
1613
+ position: fixed;
1614
+ /* max-height is set inline from the room on the menu's side; border-box
1615
+ so the padding and border count inside it and a capped menu stays on
1616
+ screen. */
1617
+ box-sizing: border-box;
1618
+ overflow-y: auto;
1619
+ overscroll-behavior: contain;
1620
+ }
1621
+
1557
1622
  /* Not enough room below the trigger — flip above it, and reverse the
1558
1623
  entry slide so the menu still moves away from its trigger. */
1559
1624
  .row-actions__menu.up {
@@ -1795,9 +1860,12 @@
1795
1860
  min-width: 0;
1796
1861
  }
1797
1862
 
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. */
1863
+ /* While the density or columns menu (in .tbl-head) is open, the
1864
+ table-wrap lets it escape its rounded clip; the data-card binds
1865
+ .has-open-menu for those two only. Row menus never need it: they are
1866
+ fixed to the viewport, because .tbl-scroll must keep its own overflow
1867
+ (lifting it would reset the reader's scroll position and spill the
1868
+ table across the page). */
1801
1869
  .tbl-wrap.has-open-menu {
1802
1870
  overflow: visible;
1803
1871
  }
@@ -4541,40 +4609,77 @@ input.ef-input[type='number'] {
4541
4609
  min-inline-size: var(--hit-touch);
4542
4610
  }
4543
4611
 
4544
- .ef-app-top__action,
4545
- .ef-theme-toggle,
4546
- .ef-app-top__logout,
4612
+ /* The row ⋯ trigger and the clear ✕ draw at their desktop size (34x28
4613
+ pill, 22px circle) and reach the 48px tier through an invisible
4614
+ overhang, like the top-bar icons. Growing the painted box instead
4615
+ turned every row's ⋯ into a 48px bordered circle.
4616
+
4617
+ The margin reserves the same 48x48 the old rule laid out, so rows,
4618
+ cards and inputs keep their height and spacing, and the overhang
4619
+ never leaves that box: two triggers in adjacent compact rows cannot
4620
+ overlap, because each target is the footprint its own row already
4621
+ made room for. The trigger's overhang adds its 1px border, since an
4622
+ absolute box is measured from the padding edge. */
4547
4623
  .row-actions__trigger,
4548
- .ef-clear,
4549
- .pg {
4550
- inline-size: var(--hit-touch);
4551
- block-size: var(--hit-touch);
4624
+ .ef-clear {
4625
+ position: relative;
4552
4626
  }
4553
4627
 
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;
4628
+ .row-actions__trigger {
4629
+ margin-block: calc((var(--hit-touch) - 28px) / 2);
4630
+ margin-inline: calc((var(--hit-touch) - 34px) / 2);
4568
4631
  }
4569
4632
 
4570
- .detail-toolbar .right .btn::after,
4571
- .search-toolbar .right .btn::after {
4633
+ .ef-clear {
4634
+ margin: calc((var(--hit-touch) - 22px) / 2);
4635
+ }
4636
+
4637
+ .row-actions__trigger::after,
4638
+ .ef-clear::after {
4639
+ content: '';
4640
+ position: absolute;
4641
+ }
4642
+
4643
+ .row-actions__trigger::after {
4644
+ inset-block: calc((var(--hit-touch) - 28px) / -2 - 1px);
4645
+ inset-inline: calc((var(--hit-touch) - 34px) / -2 - 1px);
4646
+ }
4647
+
4648
+ .ef-clear::after {
4649
+ inset: calc((var(--hit-touch) - 22px) / -2);
4650
+ }
4651
+
4652
+ /* Written as `.pager .pg` to match the base rule's specificity: a bare
4653
+ `.pg` lost to its `height: 32px`, so page buttons drew 48px wide but
4654
+ 32px tall on every touch screen. A floor rather than a fixed width,
4655
+ so a four-digit page number is not squeezed. */
4656
+ .pager .pg {
4657
+ min-inline-size: var(--hit-touch);
4658
+ block-size: var(--hit-touch);
4659
+ }
4660
+
4661
+ /* The top-bar icons keep their 40px painted box and reach the 48px
4662
+ tier through an invisible overhang, 4px each side. The 8px slot gap
4663
+ means neighbouring overhangs meet and never overlap, so a tap always
4664
+ lands on the control nearest it. */
4665
+ .ef-app-top__action::after,
4666
+ .ef-theme-toggle::after,
4667
+ .ef-app-top__logout::after {
4572
4668
  content: '';
4573
4669
  position: absolute;
4574
- /* (48 - 40) / 2 outward on every side */
4575
4670
  inset: calc((var(--hit-touch) - var(--hit-base)) / -2);
4576
4671
  }
4577
4672
 
4673
+ /* Where the lib's controls meet whatever the app projects into the
4674
+ actions slot, the gap is 12px rather than 8. An app control can carry
4675
+ a wider overhang than ours: the phone avatar paints 32px inside a
4676
+ 48px target, 8px each side. 8 + 4 needs 12px between the painted
4677
+ boxes, or the theme switch takes the avatar's trailing edge. */
4678
+ ef-app-top .ef-app-top__slot--actions > * + ef-theme-toggle,
4679
+ ef-app-top .ef-app-top__slot--actions > :not(ef-theme-toggle) + .ef-app-top__logout {
4680
+ margin-inline-start: 4px;
4681
+ }
4682
+
4578
4683
  /* The switch draws at its theme size (Aura: 40x24) and its click
4579
4684
  target is PrimeNG's invisible input, sized to exactly that box. In a
4580
4685
  list row a tap that lands just outside it did nothing, because the
@@ -4599,6 +4704,38 @@ input.ef-input[type='number'] {
4599
4704
  }
4600
4705
 
4601
4706
 
4707
+ /* Toolbar buttons collapse to icon-only below 767px, so the blanket
4708
+ min-block-size above stretched a 40px-wide button to 48px tall and
4709
+ produced a lozenge four-across in the header. Here the VISUAL stays
4710
+ 40px square and the TOUCH TARGET reaches the tier through a hit
4711
+ slop, which is the point of the tier: a thumb needs 48px, an eye
4712
+ does not.
4713
+
4714
+ Scoped to the same 767px as the rule that hides the label. Keyed on
4715
+ pointer type alone it also caught every tablet, which kept its label
4716
+ and got a 40px box: "Nouvelle commande" spilled out of the button and
4717
+ the detail actions piled into each other. Above 767px a touch screen
4718
+ keeps the labelled button, 48px tall from the generic rule above. */
4719
+ @media (pointer: coarse) and (max-width: 767px) {
4720
+ .detail-toolbar .right .btn,
4721
+ .search-toolbar .right .btn {
4722
+ position: relative;
4723
+ inline-size: var(--hit-base);
4724
+ block-size: var(--hit-base);
4725
+ min-inline-size: 0;
4726
+ min-block-size: 0;
4727
+ padding: 0;
4728
+ }
4729
+
4730
+ .detail-toolbar .right .btn::after,
4731
+ .search-toolbar .right .btn::after {
4732
+ content: '';
4733
+ position: absolute;
4734
+ /* (48 - 40) / 2 outward on every side */
4735
+ inset: calc((var(--hit-touch) - var(--hit-base)) / -2);
4736
+ }
4737
+ }
4738
+
4602
4739
  /* ── MOBILE BOTTOM BAR ──────────────────────────────────────────────
4603
4740
  The phone shell renders a 64px tab bar fixed to the bottom edge.
4604
4741
  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;
@@ -5119,6 +5194,13 @@ interface PageBtn {
5119
5194
  *
5120
5195
  * Renders a windowed page list (first / current ±1 / last with
5121
5196
  * ellipsis) for total > 7 pages. Below 7 pages all buttons show.
5197
+ *
5198
+ * On a phone (`EfViewportService.isMobile`) the window becomes
5199
+ * first · previous · "5 of 20" · next · last. The window is up to nine
5200
+ * 48px touch targets (about 464px) and a 390px phone has about 316px
5201
+ * inside a card, so it ran off the edge. The compact set still reaches
5202
+ * every page the window offered: its numbered neighbours are one step
5203
+ * away, and the first and the last page are one tap.
5122
5204
  */
5123
5205
  declare class EfPagerComponent {
5124
5206
  readonly pageNumber: _angular_core.InputSignal<number>;
@@ -5130,10 +5212,14 @@ declare class EfPagerComponent {
5130
5212
  readonly ofLabelKey: _angular_core.InputSignal<string>;
5131
5213
  readonly prevLabelKey: _angular_core.InputSignal<string>;
5132
5214
  readonly nextLabelKey: _angular_core.InputSignal<string>;
5215
+ readonly firstLabelKey: _angular_core.InputSignal<string>;
5216
+ readonly lastLabelKey: _angular_core.InputSignal<string>;
5133
5217
  /** Hide the lines-per-page selector + range when only paging is wanted. */
5134
5218
  readonly hideSize: _angular_core.InputSignal<boolean>;
5135
5219
  readonly pageChange: _angular_core.OutputEmitterRef<number>;
5136
5220
  readonly pageSizeChange: _angular_core.OutputEmitterRef<number>;
5221
+ /** Phone layout: first · prev · "n of N" · next · last. */
5222
+ readonly compact: _angular_core.Signal<boolean>;
5137
5223
  /** Falls back to the first configured option if the value ever goes empty. */
5138
5224
  private get defaultSize();
5139
5225
  /** `pageSizeOptions` shaped for `ef-select`, which reads a label and a
@@ -5153,7 +5239,7 @@ declare class EfPagerComponent {
5153
5239
  onSizeChange(value: number | string | null): void;
5154
5240
  trackBtn(index: number, btn: PageBtn): string;
5155
5241
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<EfPagerComponent, never>;
5156
- static ɵcmp: _angular_core.ɵɵComponentDeclaration<EfPagerComponent, "ef-pager", never, { "pageNumber": { "alias": "pageNumber"; "required": false; "isSignal": true; }; "pageSize": { "alias": "pageSize"; "required": false; "isSignal": true; }; "totalCount": { "alias": "totalCount"; "required": false; "isSignal": true; }; "pageSizeOptions": { "alias": "pageSizeOptions"; "required": false; "isSignal": true; }; "linesPerPageLabelKey": { "alias": "linesPerPageLabelKey"; "required": false; "isSignal": true; }; "ofLabelKey": { "alias": "ofLabelKey"; "required": false; "isSignal": true; }; "prevLabelKey": { "alias": "prevLabelKey"; "required": false; "isSignal": true; }; "nextLabelKey": { "alias": "nextLabelKey"; "required": false; "isSignal": true; }; "hideSize": { "alias": "hideSize"; "required": false; "isSignal": true; }; }, { "pageChange": "pageChange"; "pageSizeChange": "pageSizeChange"; }, never, never, true, never>;
5242
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<EfPagerComponent, "ef-pager", never, { "pageNumber": { "alias": "pageNumber"; "required": false; "isSignal": true; }; "pageSize": { "alias": "pageSize"; "required": false; "isSignal": true; }; "totalCount": { "alias": "totalCount"; "required": false; "isSignal": true; }; "pageSizeOptions": { "alias": "pageSizeOptions"; "required": false; "isSignal": true; }; "linesPerPageLabelKey": { "alias": "linesPerPageLabelKey"; "required": false; "isSignal": true; }; "ofLabelKey": { "alias": "ofLabelKey"; "required": false; "isSignal": true; }; "prevLabelKey": { "alias": "prevLabelKey"; "required": false; "isSignal": true; }; "nextLabelKey": { "alias": "nextLabelKey"; "required": false; "isSignal": true; }; "firstLabelKey": { "alias": "firstLabelKey"; "required": false; "isSignal": true; }; "lastLabelKey": { "alias": "lastLabelKey"; "required": false; "isSignal": true; }; "hideSize": { "alias": "hideSize"; "required": false; "isSignal": true; }; }, { "pageChange": "pageChange"; "pageSizeChange": "pageSizeChange"; }, never, never, true, never>;
5157
5243
  }
5158
5244
 
5159
5245
  /**