@xmesh/system-design 0.0.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.
- package/README.md +472 -0
- package/assets/brand-lockup-dark.svg +9 -0
- package/assets/brand-lockup-light.svg +9 -0
- package/assets/brand-mark.svg +9 -0
- package/colors_and_type.css +11 -0
- package/dist/lit/components/alert/index.css +201 -0
- package/dist/lit/components/alert/index.d.ts +25 -0
- package/dist/lit/components/alert/index.js +191 -0
- package/dist/lit/components/app-bar/index.css +80 -0
- package/dist/lit/components/app-bar/index.d.ts +19 -0
- package/dist/lit/components/app-bar/index.js +120 -0
- package/dist/lit/components/artifact/index.css +166 -0
- package/dist/lit/components/artifact/index.d.ts +37 -0
- package/dist/lit/components/artifact/index.js +294 -0
- package/dist/lit/components/autocomplete/index.css +171 -0
- package/dist/lit/components/autocomplete/index.d.ts +47 -0
- package/dist/lit/components/autocomplete/index.js +404 -0
- package/dist/lit/components/avatar/index.css +62 -0
- package/dist/lit/components/avatar/index.d.ts +19 -0
- package/dist/lit/components/avatar/index.js +112 -0
- package/dist/lit/components/avatar-group/index.css +60 -0
- package/dist/lit/components/avatar-group/index.d.ts +19 -0
- package/dist/lit/components/avatar-group/index.js +97 -0
- package/dist/lit/components/badge/index.css +72 -0
- package/dist/lit/components/badge/index.d.ts +18 -0
- package/dist/lit/components/badge/index.js +115 -0
- package/dist/lit/components/brand-mark/index.css +109 -0
- package/dist/lit/components/brand-mark/index.d.ts +24 -0
- package/dist/lit/components/brand-mark/index.js +116 -0
- package/dist/lit/components/breadcrumbs/index.css +91 -0
- package/dist/lit/components/breadcrumbs/index.d.ts +19 -0
- package/dist/lit/components/breadcrumbs/index.js +104 -0
- package/dist/lit/components/bubble/index.css +182 -0
- package/dist/lit/components/bubble/index.d.ts +72 -0
- package/dist/lit/components/bubble/index.js +617 -0
- package/dist/lit/components/button/index.css +342 -0
- package/dist/lit/components/button/index.d.ts +32 -0
- package/dist/lit/components/button/index.js +202 -0
- package/dist/lit/components/card/index.css +99 -0
- package/dist/lit/components/card/index.d.ts +20 -0
- package/dist/lit/components/card/index.js +133 -0
- package/dist/lit/components/chat/index.css +292 -0
- package/dist/lit/components/chat/index.d.ts +74 -0
- package/dist/lit/components/chat/index.js +589 -0
- package/dist/lit/components/checkbox/index.css +126 -0
- package/dist/lit/components/checkbox/index.d.ts +21 -0
- package/dist/lit/components/checkbox/index.js +138 -0
- package/dist/lit/components/chip/index.css +145 -0
- package/dist/lit/components/chip/index.d.ts +30 -0
- package/dist/lit/components/chip/index.js +230 -0
- package/dist/lit/components/chip-group/index.css +19 -0
- package/dist/lit/components/chip-group/index.d.ts +24 -0
- package/dist/lit/components/chip-group/index.js +171 -0
- package/dist/lit/components/code/index.css +42 -0
- package/dist/lit/components/code/index.d.ts +12 -0
- package/dist/lit/components/code/index.js +68 -0
- package/dist/lit/components/composer/index.css +548 -0
- package/dist/lit/components/composer/index.d.ts +67 -0
- package/dist/lit/components/composer/index.js +713 -0
- package/dist/lit/components/data-table/index.css +166 -0
- package/dist/lit/components/data-table/index.d.ts +55 -0
- package/dist/lit/components/data-table/index.js +390 -0
- package/dist/lit/components/dialog/index.css +124 -0
- package/dist/lit/components/dialog/index.d.ts +24 -0
- package/dist/lit/components/dialog/index.js +199 -0
- package/dist/lit/components/divider/index.css +27 -0
- package/dist/lit/components/divider/index.d.ts +13 -0
- package/dist/lit/components/divider/index.js +67 -0
- package/dist/lit/components/empty-state/index.css +69 -0
- package/dist/lit/components/empty-state/index.d.ts +21 -0
- package/dist/lit/components/empty-state/index.js +123 -0
- package/dist/lit/components/expansion-panel/index.css +120 -0
- package/dist/lit/components/expansion-panel/index.d.ts +22 -0
- package/dist/lit/components/expansion-panel/index.js +174 -0
- package/dist/lit/components/field/index.css +223 -0
- package/dist/lit/components/field/index.d.ts +106 -0
- package/dist/lit/components/field/index.js +388 -0
- package/dist/lit/components/file-input/index.css +257 -0
- package/dist/lit/components/file-input/index.d.ts +30 -0
- package/dist/lit/components/file-input/index.js +298 -0
- package/dist/lit/components/form/index.css +29 -0
- package/dist/lit/components/form/index.d.ts +38 -0
- package/dist/lit/components/form/index.js +192 -0
- package/dist/lit/components/grid/index.css +53 -0
- package/dist/lit/components/grid/index.d.ts +14 -0
- package/dist/lit/components/grid/index.js +82 -0
- package/dist/lit/components/kbd/index.css +35 -0
- package/dist/lit/components/kbd/index.d.ts +11 -0
- package/dist/lit/components/kbd/index.js +43 -0
- package/dist/lit/components/list/index.css +15 -0
- package/dist/lit/components/list/index.d.ts +28 -0
- package/dist/lit/components/list/index.js +188 -0
- package/dist/lit/components/list-item/index.css +119 -0
- package/dist/lit/components/list-item/index.d.ts +20 -0
- package/dist/lit/components/list-item/index.js +127 -0
- package/dist/lit/components/menu/index.css +94 -0
- package/dist/lit/components/menu/index.d.ts +47 -0
- package/dist/lit/components/menu/index.js +386 -0
- package/dist/lit/components/navigation-drawer/index.css +114 -0
- package/dist/lit/components/navigation-drawer/index.d.ts +29 -0
- package/dist/lit/components/navigation-drawer/index.js +218 -0
- package/dist/lit/components/overlay/index.css +171 -0
- package/dist/lit/components/overlay/index.d.ts +65 -0
- package/dist/lit/components/overlay/index.js +566 -0
- package/dist/lit/components/pagination/index.css +102 -0
- package/dist/lit/components/pagination/index.d.ts +22 -0
- package/dist/lit/components/pagination/index.js +184 -0
- package/dist/lit/components/primitives/index.css +504 -0
- package/dist/lit/components/primitives/index.d.ts +25 -0
- package/dist/lit/components/primitives/index.js +283 -0
- package/dist/lit/components/progress/index.css +143 -0
- package/dist/lit/components/progress/index.d.ts +23 -0
- package/dist/lit/components/progress/index.js +180 -0
- package/dist/lit/components/radio-group/index.css +178 -0
- package/dist/lit/components/radio-group/index.d.ts +35 -0
- package/dist/lit/components/radio-group/index.js +292 -0
- package/dist/lit/components/select/index.css +151 -0
- package/dist/lit/components/select/index.d.ts +50 -0
- package/dist/lit/components/select/index.js +390 -0
- package/dist/lit/components/sidebar-item/index.css +133 -0
- package/dist/lit/components/sidebar-item/index.d.ts +20 -0
- package/dist/lit/components/sidebar-item/index.js +105 -0
- package/dist/lit/components/skeleton/index.css +81 -0
- package/dist/lit/components/skeleton/index.d.ts +19 -0
- package/dist/lit/components/skeleton/index.js +119 -0
- package/dist/lit/components/slider/index.css +171 -0
- package/dist/lit/components/slider/index.d.ts +36 -0
- package/dist/lit/components/slider/index.js +302 -0
- package/dist/lit/components/snackbar/index.css +279 -0
- package/dist/lit/components/snackbar/index.d.ts +33 -0
- package/dist/lit/components/snackbar/index.js +195 -0
- package/dist/lit/components/stack/index.css +41 -0
- package/dist/lit/components/stack/index.d.ts +20 -0
- package/dist/lit/components/stack/index.js +103 -0
- package/dist/lit/components/switch/index.css +126 -0
- package/dist/lit/components/switch/index.d.ts +17 -0
- package/dist/lit/components/switch/index.js +116 -0
- package/dist/lit/components/table/index.css +85 -0
- package/dist/lit/components/table/index.d.ts +25 -0
- package/dist/lit/components/table/index.js +139 -0
- package/dist/lit/components/tabs/index.css +116 -0
- package/dist/lit/components/tabs/index.d.ts +49 -0
- package/dist/lit/components/tabs/index.js +320 -0
- package/dist/lit/components/text-field/index.css +90 -0
- package/dist/lit/components/text-field/index.d.ts +17 -0
- package/dist/lit/components/text-field/index.js +101 -0
- package/dist/lit/components/textarea/index.css +55 -0
- package/dist/lit/components/textarea/index.d.ts +26 -0
- package/dist/lit/components/textarea/index.js +124 -0
- package/dist/lit/components/tooltip/index.css +37 -0
- package/dist/lit/components/tooltip/index.d.ts +31 -0
- package/dist/lit/components/tooltip/index.js +196 -0
- package/dist/lit/components/validation/index.css +386 -0
- package/dist/lit/components/validation/index.d.ts +45 -0
- package/dist/lit/components/validation/index.js +318 -0
- package/dist/lit/index.d.ts +50 -0
- package/dist/lit/index.js +59 -0
- package/package.json +81 -0
- package/styles/README.md +346 -0
- package/styles/_elevation.css +24 -0
- package/styles/_fonts.css +6 -0
- package/styles/_layout.css +37 -0
- package/styles/_primitives.css +154 -0
- package/styles/_scroll.css +75 -0
- package/styles/_semantic.css +146 -0
- package/styles/_space.css +61 -0
- package/styles/_type.css +139 -0
- package/styles/_xmesh-extensions.css +232 -0
- package/styles/index.css +44 -0
- package/styles/md3/_color.css +102 -0
- package/styles/md3/_elevation.css +26 -0
- package/styles/md3/_motion.css +35 -0
- package/styles/md3/_shape.css +22 -0
- package/styles/md3/_state.css +22 -0
- package/styles/md3/_type.css +111 -0
|
@@ -0,0 +1,566 @@
|
|
|
1
|
+
/*
|
|
2
|
+
overlay/index.ts — <xm-overlay>, the platform-native overlay foundation.
|
|
3
|
+
|
|
4
|
+
┌──────────────────────────────────────────────────────────────────────┐
|
|
5
|
+
│ INTERNAL FOUNDATION (FR-141 / AD-5 / AD-12). │
|
|
6
|
+
│ <xm-overlay> is not a catalog component. It is the single primitive │
|
|
7
|
+
│ that xm-dialog, xm-menu, xm-tooltip, xm-select, and xm-autocomplete │
|
|
8
|
+
│ COMPOSE so none of them hand-roll divergent stacking, focus-trap, or │
|
|
9
|
+
│ positioning. Consumers drive it through the PUBLIC API below — they │
|
|
10
|
+
│ never query or style its shadow root, and it never reaches into theirs │
|
|
11
|
+
│ (AD-12, one-way deps: overlay → tokens, overlay → primitives). │
|
|
12
|
+
└──────────────────────────────────────────────────────────────────────┘
|
|
13
|
+
|
|
14
|
+
── Why platform-native only (the dependency decision, RESOLVED — do not
|
|
15
|
+
re-litigate: OQ-C → AD-5 / FR-141a) ────────────────────────────────
|
|
16
|
+
• modal → native <dialog> + showModal() (focus-trap + background
|
|
17
|
+
`inert` for FREE; we do NOT re-implement a tab cycle)
|
|
18
|
+
• non-modal → Popover API (popover="manual", top layer)
|
|
19
|
+
• positioning→ CSS anchor() / @position-try (anchored to the consumer's
|
|
20
|
+
trigger via a per-instance anchor-name)
|
|
21
|
+
• scroll-lock→ native (lock <html> overflow while a modal is open)
|
|
22
|
+
No positioning or focus-trap dependency is bundled. Target is modern
|
|
23
|
+
evergreen browsers (anchor()/@position-try is Baseline-newly-available:
|
|
24
|
+
Chrome 125+, Firefox 132+, Safari 26.0+).
|
|
25
|
+
|
|
26
|
+
── Cross-shadow anchoring (the crux, FR-141) ──────────────────────────
|
|
27
|
+
CSS `anchor-name` is TREE-SCOPED: a popover in this overlay's shadow root
|
|
28
|
+
can only resolve an anchor-name set on an element in the SAME tree. A
|
|
29
|
+
consumer's trigger lives in ANOTHER shadow root, so native anchor() can't
|
|
30
|
+
see it. When the anchor is cross-root (the common case) the overlay pins a
|
|
31
|
+
fixed top/left computed from the trigger's getBoundingClientRect() — a
|
|
32
|
+
built-in, DEPENDENCY-FREE fallback (NOT Floating UI). Same-root anchors
|
|
33
|
+
keep the pure-CSS anchor() path. Floating UI remains the heavier escape
|
|
34
|
+
hatch for deep-nested-menu / virtual-row cases only.
|
|
35
|
+
|
|
36
|
+
── Escape hatch (OFF by default, AD-5a / NFR-16) ──────────────────────
|
|
37
|
+
A consumer that hits a genuine native edge case — anchor() unavailable,
|
|
38
|
+
a deep multi-level menu, or virtual-list rows — MAY load Floating UI for
|
|
39
|
+
THAT component only:
|
|
40
|
+
|
|
41
|
+
import { computePosition, flip, shift } from
|
|
42
|
+
"https://esm.sh/@floating-ui/dom@1"; // per-consumer escape hatch
|
|
43
|
+
|
|
44
|
+
…and feed the computed top/left to its <xm-overlay> via inline style on
|
|
45
|
+
a slotted positioner. Such an overlay MUST still declare a `tier` and
|
|
46
|
+
ride the tier ordering below — it must NEVER invent its own z-index.
|
|
47
|
+
The DEFAULT build imports NO Floating UI. (Search this file: there is
|
|
48
|
+
no `@floating-ui` import — that is intentional.)
|
|
49
|
+
|
|
50
|
+
── PUBLIC API (what consumers compose against) ────────────────────────
|
|
51
|
+
Attributes / properties
|
|
52
|
+
open boolean (reflect) — declarative open state. Setting it
|
|
53
|
+
true opens, false closes. Also driven
|
|
54
|
+
imperatively by show()/hide().
|
|
55
|
+
mode "modal" | "non-modal" (default "modal")
|
|
56
|
+
modal → <dialog>.showModal() (scrim +
|
|
57
|
+
focus-trap + inert); non-modal → popover.
|
|
58
|
+
tier "tooltip"|"menu"|"dialog" (default "dialog")
|
|
59
|
+
fixed stacking tier (see below).
|
|
60
|
+
placement "top"|"bottom"|"left"|"right"|"top-start"|"bottom-start"|…
|
|
61
|
+
(default "bottom-start") anchored
|
|
62
|
+
placement for non-modal overlays.
|
|
63
|
+
.anchor HTMLElement | null (PROPERTY, not attribute) the trigger
|
|
64
|
+
to position a non-modal overlay against
|
|
65
|
+
(CSS anchor()). May live in another
|
|
66
|
+
shadow root.
|
|
67
|
+
.opener HTMLElement | null (PROPERTY) element focus is restored to
|
|
68
|
+
on close. Defaults to .anchor, else the
|
|
69
|
+
activeElement at open time. Restores
|
|
70
|
+
cross-shadow-root.
|
|
71
|
+
label string — accessible name for the overlay region.
|
|
72
|
+
|
|
73
|
+
Methods
|
|
74
|
+
show() open the overlay (idempotent).
|
|
75
|
+
hide(reason?: OverlayCloseReason) close it (idempotent).
|
|
76
|
+
toggle() flip open state.
|
|
77
|
+
|
|
78
|
+
Events (bubbles + composed, tier (b) per the shared contract)
|
|
79
|
+
xm-overlay-open detail: { tier, mode }
|
|
80
|
+
xm-overlay-close detail: { tier, mode, reason } reason ∈
|
|
81
|
+
"escape" | "backdrop" | "api" | "ancestor-closed"
|
|
82
|
+
A consumer maps xm-overlay-close → its own close/dismiss event.
|
|
83
|
+
|
|
84
|
+
Slots
|
|
85
|
+
default overlay content header leading region
|
|
86
|
+
footer trailing region (action row)
|
|
87
|
+
|
|
88
|
+
── Stacking tier (fixed, last-opened-wins within a tier — AD-5a) ──────
|
|
89
|
+
tooltip < menu / select-listbox < dialog. A dialog always paints above a
|
|
90
|
+
menu; a tooltip never covers a menu. The native top layer already gives
|
|
91
|
+
last-opened-wins WITHIN one primitive class, but a tooltip popover and a
|
|
92
|
+
dialog also share the top layer, so tier order is enforced explicitly via
|
|
93
|
+
`--xm-overlay-z-*` applied to the host (popovers) — modal <dialog>s sit in
|
|
94
|
+
the top layer above all popovers by spec, which matches `dialog` being the
|
|
95
|
+
highest tier.
|
|
96
|
+
|
|
97
|
+
── Innermost-Esc + focus restore (AD-5a / NFR-14) ─────────────────────
|
|
98
|
+
Esc closes the INNERMOST overlay only: the keydown handler calls
|
|
99
|
+
stopPropagation() the moment it handles Esc, so one keypress never closes
|
|
100
|
+
two layers (a popover opened inside a modal closes the popover, not the
|
|
101
|
+
dialog). Closing a modal closes its descendant popovers (tracked via the
|
|
102
|
+
opener chain). Focus restores to each layer's opener, innermost-first.
|
|
103
|
+
|
|
104
|
+
── Tokens (AD-1) ─────────────────────────────────────────────────────
|
|
105
|
+
Scrim var(--md-sys-color-scrim) with alpha at the use site (CSS).
|
|
106
|
+
Surface inverse-surface family + inverse-on-surface ink (the card
|
|
107
|
+
stack; AD-13). Elevation level3 (menus/popovers) / level4–5
|
|
108
|
+
(modals) per DESIGN.md. No blur / backdrop-filter; no gradient.
|
|
109
|
+
|
|
110
|
+
── shadow-DOM CSS link + BEM (AD-2/3/4) ──────────────────────
|
|
111
|
+
Lit from lit (never `import "lit"`); CSS <link>
|
|
112
|
+
inside the shadow root via the built-file-relative new URL(...); BEM block
|
|
113
|
+
`overlay`, registered in scripts/check-bem.sh STRICT_BLOCKS.
|
|
114
|
+
*/
|
|
115
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
116
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
117
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
118
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
119
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
120
|
+
};
|
|
121
|
+
import { LitElement, html } from "lit";
|
|
122
|
+
import { customElement, property, query } from "lit/decorators.js";
|
|
123
|
+
// Resolve CSS relative to the *built* file:
|
|
124
|
+
// lit/build/components/overlay/index.js → ../overlay/index.css.
|
|
125
|
+
const OVERLAY_CSS = new URL("../overlay/index.css", import.meta.url).href;
|
|
126
|
+
let overlaySeq = 0;
|
|
127
|
+
// Live stack of every open overlay, in open order (last = innermost). Used to
|
|
128
|
+
// route Esc to the innermost layer and to cascade-close descendant popovers
|
|
129
|
+
// when an ancestor (modal) closes.
|
|
130
|
+
const OPEN_STACK = [];
|
|
131
|
+
let XmOverlay = class XmOverlay extends LitElement {
|
|
132
|
+
constructor() {
|
|
133
|
+
super(...arguments);
|
|
134
|
+
this.open = false;
|
|
135
|
+
this.mode = "modal";
|
|
136
|
+
this.tier = "dialog";
|
|
137
|
+
this.placement = "bottom-start";
|
|
138
|
+
this.label = "";
|
|
139
|
+
/** Trigger element to anchor a non-modal overlay against (CSS anchor()).
|
|
140
|
+
A property, not an attribute — it is an element reference, possibly from
|
|
141
|
+
another shadow root. */
|
|
142
|
+
this.anchor = null;
|
|
143
|
+
/** Element focus is restored to on close. Defaults to `anchor`, else the
|
|
144
|
+
document's deepest activeElement at open time. */
|
|
145
|
+
this.opener = null;
|
|
146
|
+
this._id = `xm-overlay-${++overlaySeq}`;
|
|
147
|
+
this._anchorName = `--xm-overlay-anchor-${overlaySeq}`;
|
|
148
|
+
this._restoreFocusTo = null;
|
|
149
|
+
this._scrollLocked = false;
|
|
150
|
+
this._wasOpen = false;
|
|
151
|
+
// Bumped on every _activate so a stale deferred (updateComplete) callback from
|
|
152
|
+
// a rapid open→close→open within one microtask doesn't double-fire open/show.
|
|
153
|
+
this._activationSeq = 0;
|
|
154
|
+
// ── Esc routing (innermost-only) ────────────────────────────────────
|
|
155
|
+
this._onKeydown = (event) => {
|
|
156
|
+
if (event.key !== "Escape")
|
|
157
|
+
return;
|
|
158
|
+
// Only the innermost open overlay reacts. A capturing listener fires once
|
|
159
|
+
// per keypress globally; we self-elect by being the last in OPEN_STACK.
|
|
160
|
+
const innermost = OPEN_STACK[OPEN_STACK.length - 1];
|
|
161
|
+
if (innermost !== this)
|
|
162
|
+
return;
|
|
163
|
+
// We are the innermost: handle and STOP so one Esc never closes two
|
|
164
|
+
// layers. preventDefault also blocks the native <dialog> cancel path so
|
|
165
|
+
// we own the close (and its reason/focus restore) uniformly.
|
|
166
|
+
event.preventDefault();
|
|
167
|
+
event.stopPropagation();
|
|
168
|
+
this._deactivate("escape");
|
|
169
|
+
};
|
|
170
|
+
// Native <dialog> emits `cancel` on Esc before `close`. We drive Esc through
|
|
171
|
+
// the capturing handler above, so suppress the native path to avoid a
|
|
172
|
+
// double-close (and so non-innermost dialogs never self-cancel).
|
|
173
|
+
this._onDialogCancel = (event) => {
|
|
174
|
+
event.preventDefault();
|
|
175
|
+
};
|
|
176
|
+
this._onDialogClose = () => {
|
|
177
|
+
// Reached only if something closed the <dialog> outside our API (e.g. form
|
|
178
|
+
// method=dialog). Sync our state.
|
|
179
|
+
if (this._wasOpen)
|
|
180
|
+
this._deactivate("api");
|
|
181
|
+
};
|
|
182
|
+
// Light-dismiss for modal: a click on the backdrop (the <dialog> element
|
|
183
|
+
// itself, outside the surface) closes it. Like Esc, only the innermost open
|
|
184
|
+
// overlay dismisses — clicking the dialog backdrop while a menu opened from
|
|
185
|
+
// inside it is open dismisses the menu first, not the whole dialog.
|
|
186
|
+
this._onDialogClick = (event) => {
|
|
187
|
+
if (event.target !== this._dialog)
|
|
188
|
+
return;
|
|
189
|
+
const innermost = OPEN_STACK[OPEN_STACK.length - 1];
|
|
190
|
+
if (innermost !== this) {
|
|
191
|
+
innermost?._deactivate("backdrop");
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
this._deactivate("backdrop");
|
|
195
|
+
};
|
|
196
|
+
this._reposition = () => {
|
|
197
|
+
if (this._wasOpen && this.mode === "non-modal")
|
|
198
|
+
this._pinToAnchor();
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
// The trigger that opened us lives in another shadow root; delegatesFocus
|
|
202
|
+
// keeps the overlay itself out of the tab order while letting content focus.
|
|
203
|
+
static { this.shadowRootOptions = {
|
|
204
|
+
...LitElement.shadowRootOptions,
|
|
205
|
+
delegatesFocus: true,
|
|
206
|
+
}; }
|
|
207
|
+
render() {
|
|
208
|
+
const isModal = this.mode === "modal";
|
|
209
|
+
const cls = `overlay overlay--tier-${this.tier} overlay--${isModal ? "modal" : "non-modal"}`;
|
|
210
|
+
// The visible surface — shared between the modal <dialog> and the
|
|
211
|
+
// non-modal popover so the card chrome is authored once.
|
|
212
|
+
const surface = html `
|
|
213
|
+
<div class="overlay__surface" part="surface">
|
|
214
|
+
<slot name="header"></slot>
|
|
215
|
+
<div class="overlay__body"><slot></slot></div>
|
|
216
|
+
<slot name="footer"></slot>
|
|
217
|
+
</div>
|
|
218
|
+
`;
|
|
219
|
+
return html `
|
|
220
|
+
<link rel="stylesheet" href="${OVERLAY_CSS}" />
|
|
221
|
+
<style>
|
|
222
|
+
:host {
|
|
223
|
+
display: contents;
|
|
224
|
+
}
|
|
225
|
+
/* Per-instance positioning glue (anchor-name + tier z-index) is set
|
|
226
|
+
inline so it can carry the runtime anchor reference; the rest of
|
|
227
|
+
the chrome lives in index.css. */
|
|
228
|
+
.overlay--non-modal .overlay__popover {
|
|
229
|
+
z-index: var(${this._tierZVar()});
|
|
230
|
+
position-anchor: var(--xm-overlay-anchor, ${this._anchorName});
|
|
231
|
+
}
|
|
232
|
+
.overlay--non-modal .overlay__popover.overlay__popover--pinned {
|
|
233
|
+
position-anchor: none;
|
|
234
|
+
}
|
|
235
|
+
</style>
|
|
236
|
+
|
|
237
|
+
${isModal
|
|
238
|
+
? html `
|
|
239
|
+
<dialog
|
|
240
|
+
id="${this._id}"
|
|
241
|
+
class="${cls} overlay__dialog"
|
|
242
|
+
@close=${this._onDialogClose}
|
|
243
|
+
@cancel=${this._onDialogCancel}
|
|
244
|
+
@click=${this._onDialogClick}
|
|
245
|
+
>
|
|
246
|
+
${surface}
|
|
247
|
+
</dialog>
|
|
248
|
+
`
|
|
249
|
+
: html `
|
|
250
|
+
<div
|
|
251
|
+
id="${this._id}"
|
|
252
|
+
class="${cls} overlay__popover"
|
|
253
|
+
popover="manual"
|
|
254
|
+
>
|
|
255
|
+
${surface}
|
|
256
|
+
</div>
|
|
257
|
+
`}
|
|
258
|
+
`;
|
|
259
|
+
}
|
|
260
|
+
updated(changed) {
|
|
261
|
+
if (changed.has("open")) {
|
|
262
|
+
if (this.open && !this._wasOpen)
|
|
263
|
+
this._activate();
|
|
264
|
+
else if (!this.open && this._wasOpen)
|
|
265
|
+
this._deactivate("api");
|
|
266
|
+
}
|
|
267
|
+
if (this.open && (changed.has("anchor") || changed.has("placement"))) {
|
|
268
|
+
this._applyAnchor();
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
disconnectedCallback() {
|
|
272
|
+
super.disconnectedCallback();
|
|
273
|
+
if (this._wasOpen)
|
|
274
|
+
this._deactivate("api");
|
|
275
|
+
document.removeEventListener("keydown", this._onKeydown, true);
|
|
276
|
+
}
|
|
277
|
+
// ── Public imperative API ───────────────────────────────────────────
|
|
278
|
+
show() {
|
|
279
|
+
this.open = true;
|
|
280
|
+
}
|
|
281
|
+
hide(reason = "api") {
|
|
282
|
+
if (!this._wasOpen) {
|
|
283
|
+
this.open = false;
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
// Route through _deactivate so the close reason is preserved on the event;
|
|
287
|
+
// the reflected `open` is cleared there.
|
|
288
|
+
this._deactivate(reason);
|
|
289
|
+
}
|
|
290
|
+
toggle() {
|
|
291
|
+
this.open = !this.open;
|
|
292
|
+
}
|
|
293
|
+
// ── Activation ──────────────────────────────────────────────────────
|
|
294
|
+
_activate() {
|
|
295
|
+
this._wasOpen = true;
|
|
296
|
+
// Capture the element to return focus to BEFORE we move focus into the
|
|
297
|
+
// overlay — but ONLY when this overlay actually moves focus in. A modal
|
|
298
|
+
// (showModal) traps focus, and a consumer that passed an explicit `opener`
|
|
299
|
+
// is managing focus; both want it restored on close. A bare non-modal
|
|
300
|
+
// popover with no opener (e.g. a tooltip shown on hover) never moves focus,
|
|
301
|
+
// so it must NOT yank focus to the anchor on close (that would steal focus).
|
|
302
|
+
this._restoreFocusTo =
|
|
303
|
+
this.mode === "modal"
|
|
304
|
+
? this.opener ?? this._deepActiveElement()
|
|
305
|
+
: this.opener ?? null;
|
|
306
|
+
OPEN_STACK.push(this);
|
|
307
|
+
// One capturing keydown listener on the document handles Esc for the whole
|
|
308
|
+
// stack; it dispatches to the innermost overlay and stops there.
|
|
309
|
+
document.addEventListener("keydown", this._onKeydown, true);
|
|
310
|
+
const activation = ++this._activationSeq;
|
|
311
|
+
// Wait for the dialog/popover element to exist in the shadow root.
|
|
312
|
+
this.updateComplete.then(() => {
|
|
313
|
+
// A newer activation (open→close→open within the microtask) superseded
|
|
314
|
+
// this one — bail so show()/showPopover() and the open event fire once.
|
|
315
|
+
if (activation !== this._activationSeq)
|
|
316
|
+
return;
|
|
317
|
+
if (!this.open)
|
|
318
|
+
return;
|
|
319
|
+
if (this.mode === "modal") {
|
|
320
|
+
const dlg = this._dialog;
|
|
321
|
+
if (dlg && !dlg.open) {
|
|
322
|
+
dlg.showModal(); // focus-trap + background inert, for free
|
|
323
|
+
this._lockScroll();
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
const pop = this._popover;
|
|
328
|
+
if (pop && !pop.matches(":popover-open")) {
|
|
329
|
+
this._applyAnchor();
|
|
330
|
+
try {
|
|
331
|
+
pop.showPopover();
|
|
332
|
+
}
|
|
333
|
+
catch {
|
|
334
|
+
// Popover may already be open (race) — ignore.
|
|
335
|
+
}
|
|
336
|
+
// Re-pin once the popover is laid out so offsetWidth/Height are real
|
|
337
|
+
// (the cross-root fallback needs the rendered size to place + clamp).
|
|
338
|
+
if (pop.classList.contains("overlay__popover--pinned")) {
|
|
339
|
+
this._pinToAnchor();
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
this.dispatchEvent(new CustomEvent("xm-overlay-open", {
|
|
344
|
+
bubbles: true,
|
|
345
|
+
composed: true,
|
|
346
|
+
detail: { tier: this.tier, mode: this.mode },
|
|
347
|
+
}));
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
_deactivate(reason) {
|
|
351
|
+
if (!this._wasOpen)
|
|
352
|
+
return;
|
|
353
|
+
this._wasOpen = false;
|
|
354
|
+
const idx = OPEN_STACK.indexOf(this);
|
|
355
|
+
if (idx !== -1)
|
|
356
|
+
OPEN_STACK.splice(idx, 1);
|
|
357
|
+
// Each open overlay registers its OWN bound listener, so each must remove
|
|
358
|
+
// its own — removing only when the stack empties leaks the inner layers'
|
|
359
|
+
// listeners (and double-fires them on re-open).
|
|
360
|
+
document.removeEventListener("keydown", this._onKeydown, true);
|
|
361
|
+
window.removeEventListener("scroll", this._reposition, true);
|
|
362
|
+
window.removeEventListener("resize", this._reposition);
|
|
363
|
+
// Cascade: closing a modal closes any popover opened from inside it. Walk
|
|
364
|
+
// the stack for descendants whose opener resolves to inside this overlay.
|
|
365
|
+
if (this.mode === "modal") {
|
|
366
|
+
for (const other of [...OPEN_STACK]) {
|
|
367
|
+
if (this.contains(other) || this._ownsOpener(other)) {
|
|
368
|
+
other._deactivate("ancestor-closed");
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
if (this.mode === "modal") {
|
|
373
|
+
const dlg = this._dialog;
|
|
374
|
+
if (dlg?.open)
|
|
375
|
+
dlg.close();
|
|
376
|
+
this._unlockScroll();
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
const pop = this._popover;
|
|
380
|
+
if (pop?.matches(":popover-open")) {
|
|
381
|
+
try {
|
|
382
|
+
pop.hidePopover();
|
|
383
|
+
}
|
|
384
|
+
catch {
|
|
385
|
+
// Already hidden — ignore.
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
this.open = false;
|
|
390
|
+
// Innermost-first focus restore: restore to this layer's opener. Because
|
|
391
|
+
// the stack unwinds innermost-first (Esc hits the innermost first), each
|
|
392
|
+
// layer restores in turn. Works cross-shadow-root: we hold the element ref.
|
|
393
|
+
this._restoreFocus();
|
|
394
|
+
this.dispatchEvent(new CustomEvent("xm-overlay-close", {
|
|
395
|
+
bubbles: true,
|
|
396
|
+
composed: true,
|
|
397
|
+
detail: { tier: this.tier, mode: this.mode, reason },
|
|
398
|
+
}));
|
|
399
|
+
}
|
|
400
|
+
// ── Anchored positioning (CSS anchor()) ─────────────────────────────
|
|
401
|
+
_applyAnchor() {
|
|
402
|
+
const pop = this._popover;
|
|
403
|
+
if (!pop)
|
|
404
|
+
return;
|
|
405
|
+
pop.dataset["placement"] = this.placement;
|
|
406
|
+
if (!this.anchor)
|
|
407
|
+
return;
|
|
408
|
+
// CSS anchor-name is TREE-SCOPED: a popover in this overlay's shadow root
|
|
409
|
+
// can only resolve an anchor-name set on an element in the SAME tree. When
|
|
410
|
+
// the consumer's trigger lives in another shadow root (the common case),
|
|
411
|
+
// native anchor() can't see it — so we pin a fixed top/left computed from
|
|
412
|
+
// the trigger's bounding rect (no dependency). Same-root anchors keep the
|
|
413
|
+
// pure-CSS anchor() path.
|
|
414
|
+
if (this._sameTreeScope(this.anchor)) {
|
|
415
|
+
pop.classList.remove("overlay__popover--pinned");
|
|
416
|
+
this.anchor.style.setProperty("anchor-name", this._anchorName);
|
|
417
|
+
pop.style.setProperty("--xm-overlay-anchor", this._anchorName);
|
|
418
|
+
}
|
|
419
|
+
else {
|
|
420
|
+
pop.classList.add("overlay__popover--pinned");
|
|
421
|
+
this._pinToAnchor();
|
|
422
|
+
// Keep the pin glued to the trigger while open (scroll/resize).
|
|
423
|
+
window.addEventListener("scroll", this._reposition, true);
|
|
424
|
+
window.addEventListener("resize", this._reposition);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
// The popover lives in this element's shadow root; an anchor is in the same
|
|
428
|
+
// tree scope only if its root node is this shadow root (it never is for a
|
|
429
|
+
// consumer trigger) — so in practice this is the consumer-supplied-same-root
|
|
430
|
+
// escape valve. Kept as an explicit check so the native path is taken
|
|
431
|
+
// whenever it actually can resolve.
|
|
432
|
+
_sameTreeScope(anchor) {
|
|
433
|
+
return anchor.getRootNode() === this.renderRoot;
|
|
434
|
+
}
|
|
435
|
+
_pinToAnchor() {
|
|
436
|
+
const pop = this._popover;
|
|
437
|
+
if (!pop || !this.anchor)
|
|
438
|
+
return;
|
|
439
|
+
const a = this.anchor.getBoundingClientRect();
|
|
440
|
+
// A display:none / zero-size anchor returns an all-zero rect — pinning to it
|
|
441
|
+
// would land the popover in the top-left corner. Hide instead of mispinning.
|
|
442
|
+
if (a.width === 0 && a.height === 0) {
|
|
443
|
+
pop.style.setProperty("visibility", "hidden");
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
// The anchor is fully off-screen — clamping would detach the popover and
|
|
447
|
+
// leave it floating at a viewport edge with no trigger. Hide it; a later
|
|
448
|
+
// scroll/resize re-runs this and reveals it once the anchor returns.
|
|
449
|
+
if (a.bottom <= 0 ||
|
|
450
|
+
a.top >= window.innerHeight ||
|
|
451
|
+
a.right <= 0 ||
|
|
452
|
+
a.left >= window.innerWidth) {
|
|
453
|
+
pop.style.setProperty("visibility", "hidden");
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
pop.style.removeProperty("visibility");
|
|
457
|
+
const gap = 4;
|
|
458
|
+
const w = pop.offsetWidth;
|
|
459
|
+
const h = pop.offsetHeight;
|
|
460
|
+
const place = this.placement;
|
|
461
|
+
const main = place.split("-")[0];
|
|
462
|
+
const align = place.includes("-") ? place.split("-")[1] : "center";
|
|
463
|
+
let top;
|
|
464
|
+
let left;
|
|
465
|
+
// Cross-axis alignment for a given edge length: start | end | center.
|
|
466
|
+
const alignAxis = (start, end, size) => align === "end" ? end - size : align === "start" ? start : start + (end - start) / 2 - size / 2;
|
|
467
|
+
if (main === "top") {
|
|
468
|
+
top = a.top - h - gap;
|
|
469
|
+
left = alignAxis(a.left, a.right, w);
|
|
470
|
+
}
|
|
471
|
+
else if (main === "left") {
|
|
472
|
+
left = a.left - w - gap;
|
|
473
|
+
top = alignAxis(a.top, a.bottom, h);
|
|
474
|
+
}
|
|
475
|
+
else if (main === "right") {
|
|
476
|
+
left = a.right + gap;
|
|
477
|
+
top = alignAxis(a.top, a.bottom, h);
|
|
478
|
+
}
|
|
479
|
+
else {
|
|
480
|
+
// bottom (default)
|
|
481
|
+
top = a.bottom + gap;
|
|
482
|
+
left = alignAxis(a.left, a.right, w);
|
|
483
|
+
}
|
|
484
|
+
// Clamp into the viewport so a popover near an edge stays visible.
|
|
485
|
+
const margin = 8;
|
|
486
|
+
left = Math.max(margin, Math.min(left, window.innerWidth - w - margin));
|
|
487
|
+
top = Math.max(margin, Math.min(top, window.innerHeight - h - margin));
|
|
488
|
+
pop.style.setProperty("--xm-overlay-pin-top", `${Math.round(top)}px`);
|
|
489
|
+
pop.style.setProperty("--xm-overlay-pin-left", `${Math.round(left)}px`);
|
|
490
|
+
}
|
|
491
|
+
// ── Scroll lock (native, no library) ────────────────────────────────
|
|
492
|
+
_lockScroll() {
|
|
493
|
+
if (this._scrollLocked)
|
|
494
|
+
return;
|
|
495
|
+
this._scrollLocked = true;
|
|
496
|
+
document.documentElement.style.setProperty("--xm-overlay-scroll-lock", "hidden");
|
|
497
|
+
document.documentElement.style.overflow = "hidden";
|
|
498
|
+
}
|
|
499
|
+
_unlockScroll() {
|
|
500
|
+
if (!this._scrollLocked)
|
|
501
|
+
return;
|
|
502
|
+
this._scrollLocked = false;
|
|
503
|
+
// Only release if no other modal still holds the lock.
|
|
504
|
+
const stillModal = OPEN_STACK.some((o) => o.mode === "modal");
|
|
505
|
+
if (!stillModal) {
|
|
506
|
+
document.documentElement.style.removeProperty("overflow");
|
|
507
|
+
document.documentElement.style.removeProperty("--xm-overlay-scroll-lock");
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
// ── Focus helpers (cross-shadow-root) ───────────────────────────────
|
|
511
|
+
_restoreFocus() {
|
|
512
|
+
const target = this._restoreFocusTo;
|
|
513
|
+
this._restoreFocusTo = null;
|
|
514
|
+
if (target && typeof target.focus === "function" && target.isConnected) {
|
|
515
|
+
target.focus();
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
// Pierce shadow roots to find the truly focused element so restore works
|
|
519
|
+
// when the trigger sits in a nested shadow tree.
|
|
520
|
+
_deepActiveElement() {
|
|
521
|
+
let active = document.activeElement;
|
|
522
|
+
while (active?.shadowRoot?.activeElement) {
|
|
523
|
+
active = active.shadowRoot.activeElement;
|
|
524
|
+
}
|
|
525
|
+
return active instanceof HTMLElement ? active : null;
|
|
526
|
+
}
|
|
527
|
+
_ownsOpener(other) {
|
|
528
|
+
const op = other.opener ?? other.anchor;
|
|
529
|
+
return op != null && this.contains(op);
|
|
530
|
+
}
|
|
531
|
+
_tierZVar() {
|
|
532
|
+
// Tier → z-index custom property. Modal <dialog>s ride the native top
|
|
533
|
+
// layer above all popovers; this var orders popovers among themselves so
|
|
534
|
+
// a menu sits above a tooltip. A FUI-positioned overlay reuses these.
|
|
535
|
+
return this.tier === "tooltip"
|
|
536
|
+
? "--xm-overlay-z-tooltip"
|
|
537
|
+
: this.tier === "menu"
|
|
538
|
+
? "--xm-overlay-z-menu"
|
|
539
|
+
: "--xm-overlay-z-dialog";
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
__decorate([
|
|
543
|
+
property({ type: Boolean, reflect: true })
|
|
544
|
+
], XmOverlay.prototype, "open", void 0);
|
|
545
|
+
__decorate([
|
|
546
|
+
property({ type: String, reflect: true })
|
|
547
|
+
], XmOverlay.prototype, "mode", void 0);
|
|
548
|
+
__decorate([
|
|
549
|
+
property({ type: String, reflect: true })
|
|
550
|
+
], XmOverlay.prototype, "tier", void 0);
|
|
551
|
+
__decorate([
|
|
552
|
+
property({ type: String, reflect: true })
|
|
553
|
+
], XmOverlay.prototype, "placement", void 0);
|
|
554
|
+
__decorate([
|
|
555
|
+
property({ type: String })
|
|
556
|
+
], XmOverlay.prototype, "label", void 0);
|
|
557
|
+
__decorate([
|
|
558
|
+
query(".overlay__dialog")
|
|
559
|
+
], XmOverlay.prototype, "_dialog", void 0);
|
|
560
|
+
__decorate([
|
|
561
|
+
query(".overlay__popover")
|
|
562
|
+
], XmOverlay.prototype, "_popover", void 0);
|
|
563
|
+
XmOverlay = __decorate([
|
|
564
|
+
customElement("xm-overlay")
|
|
565
|
+
], XmOverlay);
|
|
566
|
+
export { XmOverlay };
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/* ============================================
|
|
2
|
+
<xm-pagination> — numbered page controls.
|
|
3
|
+
|
|
4
|
+
Surface / ink (AD-13): a transparent control row tracing its host surface.
|
|
5
|
+
Idle controls take var(--md-sys-color-on-surface-variant) ink; the CURRENT
|
|
6
|
+
page is a coral chip — var(--md-sys-color-primary) fill + var(--md-sys-color-on-primary)
|
|
7
|
+
ink (matched to the coral fill). Hairline 1px borders only (2px is reserved
|
|
8
|
+
for drag-active, not used here). Disabled edges use the shared reduced-
|
|
9
|
+
emphasis treatment — no error color.
|
|
10
|
+
|
|
11
|
+
BEM: block `pagination`; elements `__item` `__page` `__prev` `__next`
|
|
12
|
+
`__ellipsis`; modifiers `__page--current`, size `--xs|sm|md|lg`.
|
|
13
|
+
============================================ */
|
|
14
|
+
|
|
15
|
+
.pagination {
|
|
16
|
+
display: inline-flex;
|
|
17
|
+
align-items: center;
|
|
18
|
+
gap: var(--s-1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/* ---------- Shared control chrome ---------- */
|
|
22
|
+
.pagination__item {
|
|
23
|
+
appearance: none;
|
|
24
|
+
display: inline-flex;
|
|
25
|
+
align-items: center;
|
|
26
|
+
justify-content: center;
|
|
27
|
+
box-sizing: border-box;
|
|
28
|
+
border: 1px solid var(--md-sys-color-outline-variant);
|
|
29
|
+
border-radius: var(--md-sys-shape-corner-button);
|
|
30
|
+
background: transparent;
|
|
31
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
32
|
+
font-family: var(--md-sys-typescale-label-large-font);
|
|
33
|
+
font-weight: 600;
|
|
34
|
+
cursor: pointer;
|
|
35
|
+
user-select: none;
|
|
36
|
+
transition:
|
|
37
|
+
background var(--md-sys-motion-duration-short3) var(--md-sys-motion-easing-standard),
|
|
38
|
+
color var(--md-sys-motion-duration-short3) var(--md-sys-motion-easing-standard),
|
|
39
|
+
border-color var(--md-sys-motion-duration-short3) var(--md-sys-motion-easing-standard),
|
|
40
|
+
box-shadow var(--md-sys-motion-duration-short3) var(--md-sys-motion-easing-standard);
|
|
41
|
+
}
|
|
42
|
+
.pagination__item:hover {
|
|
43
|
+
background: color-mix(
|
|
44
|
+
in oklab,
|
|
45
|
+
var(--md-sys-color-on-surface) var(--md-sys-state-hover-state-layer-opacity),
|
|
46
|
+
transparent
|
|
47
|
+
);
|
|
48
|
+
color: var(--md-sys-color-on-surface);
|
|
49
|
+
border-color: var(--md-sys-color-outline);
|
|
50
|
+
}
|
|
51
|
+
.pagination__item:focus {
|
|
52
|
+
outline: none;
|
|
53
|
+
}
|
|
54
|
+
.pagination__item:focus-visible {
|
|
55
|
+
outline: none;
|
|
56
|
+
box-shadow: var(--xm-state-focus-ring);
|
|
57
|
+
}
|
|
58
|
+
.pagination__item svg {
|
|
59
|
+
flex-shrink: 0;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* ---------- Sizes — one shared control height per size ---------- */
|
|
63
|
+
.pagination--xs .pagination__item { min-width: 22px; height: 22px; padding: 0 var(--s-1); font-size: var(--md-sys-typescale-label-small-size); }
|
|
64
|
+
.pagination--sm .pagination__item { min-width: 26px; height: 26px; padding: 0 var(--s-2); font-size: var(--md-sys-typescale-body-small-size); }
|
|
65
|
+
.pagination--md .pagination__item { min-width: 32px; height: 32px; padding: 0 var(--s-2); font-size: var(--md-sys-typescale-body-medium-size); }
|
|
66
|
+
.pagination--lg .pagination__item { min-width: 42px; height: 42px; padding: 0 var(--s-3); font-size: var(--md-sys-typescale-title-small-size); }
|
|
67
|
+
|
|
68
|
+
/* ---------- Current page — coral chip ----------
|
|
69
|
+
The page digit is text on the coral fill, so it uses --xm-color-primary-fill
|
|
70
|
+
(the darker coral) for WCAG AA, not the lighter --md-sys-color-primary. */
|
|
71
|
+
.pagination__page--current {
|
|
72
|
+
background: var(--xm-color-primary-fill);
|
|
73
|
+
border-color: var(--xm-color-primary-fill);
|
|
74
|
+
color: var(--md-sys-color-on-primary);
|
|
75
|
+
}
|
|
76
|
+
.pagination__page--current:hover {
|
|
77
|
+
background: var(--xm-color-primary-fill);
|
|
78
|
+
border-color: var(--xm-color-primary-fill);
|
|
79
|
+
color: var(--md-sys-color-on-primary);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/* ---------- Ellipsis ---------- */
|
|
83
|
+
.pagination__ellipsis {
|
|
84
|
+
display: inline-flex;
|
|
85
|
+
align-items: center;
|
|
86
|
+
justify-content: center;
|
|
87
|
+
min-width: 24px;
|
|
88
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
89
|
+
user-select: none;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/* ---------- Disabled edges — shared reduced emphasis ---------- */
|
|
93
|
+
.pagination__item[disabled] {
|
|
94
|
+
cursor: not-allowed;
|
|
95
|
+
color: var(--xm-color-on-surface-disabled);
|
|
96
|
+
box-shadow: none;
|
|
97
|
+
}
|
|
98
|
+
.pagination__item[disabled]:hover {
|
|
99
|
+
background: transparent;
|
|
100
|
+
color: var(--xm-color-on-surface-disabled);
|
|
101
|
+
border-color: var(--md-sys-color-outline-variant);
|
|
102
|
+
}
|