@keenmate/pure-admin-core 2.9.0-rc03 → 2.9.0-rc05

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.
Files changed (35) hide show
  1. package/README.md +55 -13
  2. package/dist/css/main.css +554 -7
  3. package/package.json +3 -1
  4. package/snippets/buttons.html +51 -0
  5. package/snippets/cards.html +132 -47
  6. package/snippets/comparison.html +26 -22
  7. package/snippets/manifest.json +180 -115
  8. package/snippets/range-group.html +125 -0
  9. package/snippets/splitter.html +44 -38
  10. package/snippets/statistics.html +31 -0
  11. package/src/js/btn-split-auto-absorb.js +327 -0
  12. package/src/js/command-palette.js +472 -0
  13. package/src/js/file-selector.js +1275 -0
  14. package/src/js/internal/logging.js +121 -0
  15. package/src/js/logic-tree-renderer.js +303 -0
  16. package/src/js/modal-dialogs.js +460 -0
  17. package/src/js/overflow.js +371 -0
  18. package/src/js/pa-stat-fit.js +184 -0
  19. package/src/js/range-group.js +663 -0
  20. package/src/js/search-autocomplete-v2.js +907 -0
  21. package/src/js/search-autocomplete.js +434 -0
  22. package/src/js/settings-panel.js +245 -0
  23. package/src/js/split-button.js +141 -0
  24. package/src/js/splitter.js +1323 -0
  25. package/src/js/toast-service.js +302 -0
  26. package/src/js/tooltips-popovers.js +275 -0
  27. package/src/js/virtual-scroll.js +143 -0
  28. package/src/js/virtual-textbox.js +803 -0
  29. package/src/scss/_core.scss +7 -0
  30. package/src/scss/core-components/_buttons.scss +44 -0
  31. package/src/scss/core-components/_cards.scss +95 -6
  32. package/src/scss/core-components/_overflow.scss +50 -0
  33. package/src/scss/core-components/_range-group.scss +474 -0
  34. package/src/scss/core-components/_statistics.scss +163 -0
  35. package/src/scss/variables/_components.scss +41 -2
@@ -0,0 +1,474 @@
1
+ /* ========================================
2
+ Range Group
3
+ A compact multi-range filter: one toggle that summarises several
4
+ numeric range filters (e.g. "Age / Salary / Children") inline, and
5
+ expands into a floating panel with one slider per dimension.
6
+
7
+ Two blocks live here, on purpose:
8
+
9
+ - .pa-range — the slider primitive. Single- OR dual-thumb,
10
+ driven entirely by CSS custom properties that JS
11
+ sets (--_pos on each thumb, --_fill-start /
12
+ --_fill-end on the fill). Positioning uses LOGICAL
13
+ properties (inset-inline-*) so RTL mirrors for
14
+ free — JS only ever deals in 0–100 percentages.
15
+ - .pa-range-group — the compact control: a __toggle summarising the
16
+ rows, and a floating __panel of .pa-range rows.
17
+ The panel is anchored with Floating UI (see the
18
+ demo's range-group.js, mirroring split-button.js).
19
+
20
+ The group is meant to drop into a filter-card __filters row as a
21
+ single control that stands in for N sliders. See snippets/range-group.html.
22
+ ======================================== */
23
+
24
+ @use '../variables' as *;
25
+
26
+ /* ----------------------------------------
27
+ Slider primitive — pa-range
28
+ ---------------------------------------- */
29
+
30
+ // SCSS values are the compile-time DEFAULTS; every one is also exposed as a
31
+ // runtime --pa-range-* token (consumed via `var(--pa-range-x, <default>)`), so
32
+ // a theme or a per-instance inline style can retint/resize a slider without a
33
+ // recompile and without touching the global emit mixin. Colour tokens fall
34
+ // back to the shared framework cascade; dimension tokens fall back to these
35
+ // SCSS values. Overridable at :root, on .pa-mode-*, or on any .pa-range /
36
+ // .pa-range-group ancestor. Runtime tokens:
37
+ // --pa-range-track track colour (→ --pa-surface-track)
38
+ // --pa-range-fill selected-range fill (→ --pa-accent)
39
+ // --pa-range-thumb-bg handle interior (→ --pa-card-bg)
40
+ // --pa-range-thumb-border handle ring/bar/chevron (→ --pa-accent)
41
+ // --pa-range-thumb-border-hover handle on hover (→ --pa-accent-hover)
42
+ // --pa-range-focus-ring thumb focus/active ring (→ --pa-accent-light)
43
+ // --pa-range-tick minor tick-mark colour (→ --pa-border-color)
44
+ // --pa-range-tick-major major tick-mark colour (→ --pa-text-tertiary)
45
+ // --pa-range-track-height track/fill thickness (→ 0.4rem)
46
+ // --pa-range-thumb-size default round handle Ø (→ 1.6rem)
47
+ // --pa-range-group-panel-min-width floating panel width (→ 32rem)
48
+ // NOTE: the __panel is reparented to <body> while open (to escape overflow
49
+ // clipping), so a per-instance override must sit on the __panel or the
50
+ // .pa-range rows (they travel with it) — or at :root / theme level. Tokens
51
+ // set on the .pa-range-group root reach the closed summary but not the open
52
+ // panel.
53
+ $range-track-height: 0.4rem;
54
+ $range-thumb-size: 1.6rem;
55
+ $range-thumb-border: 0.2rem;
56
+ $range-fill-height: $range-track-height;
57
+
58
+ // Tallest handle shape (the bar) drives the reserved height so no shape
59
+ // overflows the row.
60
+ $range-thumb-tall: 2.2rem;
61
+
62
+ // Tick marks. A tick is a short line centred on the track and drawn BEHIND
63
+ // it, so only the ends protrude above/below — major ticks reach further out
64
+ // than minor ones. Sitting behind the track/fill sidesteps any contrast
65
+ // problem over the accent fill. Labels (opt-in) sit in a reserved band below.
66
+ $range-tick-width: 0.1rem;
67
+ $range-tick-major-width: 0.2rem;
68
+ $range-tick-minor-height: 0.9rem; // 0.25rem protrudes each side of a 0.4rem track
69
+ $range-tick-major-height: 1.4rem; // 0.5rem protrudes each side
70
+ $range-tick-label-gap: 0.3rem; // track bottom → label top
71
+ $range-tick-label-space: 1.6rem; // extra row height reserved when labelled
72
+
73
+ .pa-range {
74
+ position: relative;
75
+ // Reserve the tallest handle's height so absolutely-positioned thumbs
76
+ // never clip against the row above/below. The rail centres itself, so
77
+ // circle handles stay centred regardless.
78
+ height: $range-thumb-tall;
79
+ touch-action: none; // let JS own the drag gesture
80
+ user-select: none;
81
+ // The track is click-to-seek (range-group.js moves the nearest thumb), so
82
+ // the whole row reads as clickable; thumbs override with grab/grabbing.
83
+ cursor: pointer;
84
+ }
85
+
86
+ // Labelled ticks reserve a band below the track. Grow the row and keep the
87
+ // rail anchored to the top so the labels drop into the reserved space rather
88
+ // than pushing the track off-centre from its sibling rows.
89
+ .pa-range--ticks-labeled {
90
+ height: calc(#{$range-thumb-tall} + #{$range-tick-label-space});
91
+
92
+ .pa-range__rail {
93
+ top: calc(#{$range-thumb-tall} / 2);
94
+ }
95
+ }
96
+
97
+ // The inner rail the thumbs travel along. Inset half a thumb on each inline
98
+ // side so a thumb centred at 0% / 100% sits fully on the track without
99
+ // overhanging the ends. Everything (track, fill, thumbs) positions against
100
+ // this, so JS only ever works in 0–100% of the rail width.
101
+ .pa-range__rail {
102
+ position: absolute;
103
+ // Inset tracks the handle size so extreme-position thumbs stay on the track
104
+ // even if --pa-range-thumb-size is overridden.
105
+ inset-inline: calc(var(--pa-range-thumb-size, #{$range-thumb-size}) / 2);
106
+ top: 50%;
107
+ transform: translateY(-50%);
108
+ height: var(--pa-range-track-height, #{$range-track-height});
109
+ }
110
+
111
+ .pa-range__track {
112
+ position: absolute;
113
+ inset: 0;
114
+ border-radius: 999px;
115
+ background-color: var(--pa-range-track, var(--pa-surface-track));
116
+ }
117
+
118
+ .pa-range__fill {
119
+ position: absolute;
120
+ top: 0;
121
+ bottom: 0;
122
+ height: var(--pa-range-track-height, #{$range-fill-height});
123
+ border-radius: 999px;
124
+ background-color: var(--pa-range-fill, var(--pa-accent));
125
+ // JS sets these as 0–100% — logical, so RTL flips automatically.
126
+ inset-inline-start: var(--_fill-start, 0%);
127
+ inset-inline-end: var(--_fill-end, 0%);
128
+ }
129
+
130
+ /* ---- Tick marks ----
131
+ Built by range-group.js when a row has data-ticks. The container lives
132
+ INSIDE the rail (so 0–100% maps to the same travel as the thumbs) and is
133
+ the rail's first child — painted behind the track/fill, so each tick shows
134
+ only as protruding ends. Marks are pointer-events:none, so a track click
135
+ still reaches the .pa-range click-to-seek handler. */
136
+ .pa-range__ticks {
137
+ position: absolute;
138
+ inset: 0;
139
+ pointer-events: none;
140
+ }
141
+
142
+ .pa-range__tick {
143
+ position: absolute;
144
+ top: 50%;
145
+ // --_pos is a 0–100% offset along the rail, set inline by JS. Logical
146
+ // inset + logical negative margin centres the mark, RTL-correct.
147
+ inset-inline-start: var(--_pos, 0%);
148
+ width: $range-tick-width;
149
+ height: $range-tick-minor-height;
150
+ margin-inline-start: calc(#{$range-tick-width} / -2);
151
+ transform: translateY(-50%);
152
+ border-radius: 999px;
153
+ background-color: var(--pa-range-tick, var(--pa-border-color));
154
+ }
155
+
156
+ .pa-range__tick--major {
157
+ width: $range-tick-major-width;
158
+ height: $range-tick-major-height;
159
+ margin-inline-start: calc(#{$range-tick-major-width} / -2);
160
+ background-color: var(--pa-range-tick-major, var(--pa-text-tertiary));
161
+ }
162
+
163
+ // Labels (opt-in via data-tick-labels). Sit in a band below the track; JS
164
+ // adds .pa-range--ticks-labeled to the row to reserve the height.
165
+ .pa-range__tick-labels {
166
+ position: absolute;
167
+ inset-inline: 0;
168
+ top: calc(100% + #{$range-tick-label-gap});
169
+ height: 0;
170
+ pointer-events: none;
171
+ }
172
+
173
+ .pa-range__tick-label {
174
+ position: absolute;
175
+ inset-inline-start: var(--_pos, 0%);
176
+ transform: translateX(-50%); // symmetric box centring; position flips via inset
177
+ font-size: $font-size-2xs;
178
+ color: var(--pa-text-tertiary);
179
+ white-space: nowrap;
180
+ font-variant-numeric: tabular-nums;
181
+ }
182
+
183
+ .pa-range__thumb {
184
+ // Handle dimensions come from private props so the centring margin (and
185
+ // the shape modifiers below) stay in one place. Default = round handle.
186
+ --_thumb-w: var(--pa-range-thumb-size, #{$range-thumb-size});
187
+ --_thumb-h: var(--pa-range-thumb-size, #{$range-thumb-size});
188
+
189
+ position: absolute;
190
+ top: 50%;
191
+ // --_pos is the thumb centre as a 0–100% offset along the rail. Logical
192
+ // inset + a logical negative margin of half the handle keeps it centred
193
+ // and RTL-correct without any transform flip.
194
+ inset-inline-start: var(--_pos, 0%);
195
+ margin-inline-start: calc(var(--_thumb-w) / -2);
196
+ transform: translateY(-50%);
197
+
198
+ width: var(--_thumb-w);
199
+ height: var(--_thumb-h);
200
+ padding: 0;
201
+ border: $range-thumb-border solid var(--pa-range-thumb-border, var(--pa-accent));
202
+ border-radius: 50%;
203
+ background-color: var(--pa-range-thumb-bg, var(--pa-card-bg));
204
+ cursor: grab;
205
+ touch-action: none;
206
+ transition: box-shadow 0.15s ease, border-color 0.15s ease;
207
+
208
+ &:hover {
209
+ border-color: var(--pa-range-thumb-border-hover, var(--pa-accent-hover));
210
+ }
211
+
212
+ &:focus-visible {
213
+ outline: none;
214
+ box-shadow: 0 0 0 0.3rem var(--pa-range-focus-ring, var(--pa-accent-light));
215
+ }
216
+
217
+ &:active,
218
+ &.pa-range__thumb--grabbing {
219
+ cursor: grabbing;
220
+ box-shadow: 0 0 0 0.3rem var(--pa-range-focus-ring, var(--pa-accent-light));
221
+ }
222
+ }
223
+
224
+ /* ---- Handle shapes ----
225
+ Modifiers on .pa-range that restyle its thumbs. Purely cosmetic — no JS
226
+ involvement — so a shape can be mixed per row. Each just overrides the
227
+ private size props + border-radius; centring follows automatically. */
228
+
229
+ // Rounded rectangle.
230
+ .pa-range--handle-rect .pa-range__thumb {
231
+ --_thumb-w: 1.2rem;
232
+ --_thumb-h: 1.9rem;
233
+ border-radius: var(--pa-border-radius-sm);
234
+ }
235
+
236
+ // Thin vertical bar ("|") — solid accent, reads as a divider.
237
+ .pa-range--handle-bar .pa-range__thumb {
238
+ --_thumb-w: 0.35rem;
239
+ --_thumb-h: 2.2rem;
240
+ border-radius: 999px;
241
+ background-color: var(--pa-range-thumb-border, var(--pa-accent));
242
+
243
+ &:hover { background-color: var(--pa-range-thumb-border-hover, var(--pa-accent-hover)); }
244
+ }
245
+
246
+ // Chevron handles ("‹ ›"). A rounded-rect grip with a CSS chevron drawn on
247
+ // top via ::before. Handles point outward (drag me to widen the range); the
248
+ // single-thumb variant keeps the max handle only, pointing at its bound.
249
+ // A 2-border corner only inks one half of its box, so after the 45° rotation
250
+ // the "›" sits toward its point. Nudge it back toward the handle centre by
251
+ // half its post-rotation reach (size / (2·√2)). The nudge is the OUTERMOST
252
+ // transform so it's a screen-space shift, unaffected by the inner rotate.
253
+ $range-chevron-size: 0.5rem;
254
+ $range-chevron-nudge: $range-chevron-size * 0.354; // ≈ size / (2√2)
255
+
256
+ .pa-range--handle-arrow .pa-range__thumb {
257
+ --_thumb-w: 1.6rem;
258
+ --_thumb-h: 2rem;
259
+ border-radius: var(--pa-border-radius-sm);
260
+
261
+ &::before {
262
+ content: '';
263
+ position: absolute;
264
+ top: 50%;
265
+ inset-inline-start: 50%;
266
+ width: $range-chevron-size;
267
+ height: $range-chevron-size;
268
+ // top+end borders rotated 45° form a "›" (points toward inline-end).
269
+ border-block-start: $range-thumb-border solid var(--pa-range-thumb-border, var(--pa-accent));
270
+ border-inline-end: $range-thumb-border solid var(--pa-range-thumb-border, var(--pa-accent));
271
+ transform: translateX(#{-$range-chevron-nudge}) translate(-50%, -50%) rotate(45deg);
272
+ }
273
+ }
274
+ // Min handle points the other way ("‹") — nudge the opposite direction.
275
+ .pa-range--handle-arrow .pa-range__thumb--min::before {
276
+ transform: translateX(#{$range-chevron-nudge}) translate(-50%, -50%) rotate(-135deg);
277
+ }
278
+
279
+ // Needle ("▽") — a downward triangle balancing its point on the track, like a
280
+ // gauge needle marking the value. The thumb box goes invisible (it stays as the
281
+ // grab/focus target, full row height) and a filled triangle is drawn via
282
+ // ::before with clip-path — apex at the track centre, body rising above. Pure
283
+ // CSS, no glyph/SVG, matching the other shapes.
284
+ $range-needle-width: 1.2rem;
285
+ $range-needle-height: 1.1rem; // fits above the track centre within the row
286
+
287
+ .pa-range--handle-needle .pa-range__thumb {
288
+ --_thumb-w: #{$range-needle-width};
289
+ --_thumb-h: #{$range-thumb-tall};
290
+ background-color: transparent;
291
+ border-color: transparent;
292
+ border-radius: var(--pa-border-radius-sm); // shapes the focus halo only
293
+
294
+ &::before {
295
+ content: '';
296
+ position: absolute;
297
+ inset-inline-start: 50%;
298
+ bottom: 50%; // apex rests on the track centre
299
+ width: $range-needle-width;
300
+ height: $range-needle-height;
301
+ margin-inline-start: calc(#{$range-needle-width} / -2);
302
+ background-color: var(--pa-range-thumb-border, var(--pa-accent));
303
+ clip-path: polygon(50% 100%, 0 0, 100% 0); // flat top, point down
304
+ transition: background-color 0.15s ease;
305
+ }
306
+
307
+ &:hover::before {
308
+ background-color: var(--pa-range-thumb-border-hover, var(--pa-accent-hover));
309
+ }
310
+ }
311
+
312
+ // Single-thumb variant — hide the unused min thumb. JS reads the modifier
313
+ // to decide how many handles to wire up and how the fill is anchored.
314
+ .pa-range--single {
315
+ .pa-range__thumb--min {
316
+ display: none;
317
+ }
318
+ }
319
+
320
+ .pa-range--disabled {
321
+ opacity: 0.5;
322
+ pointer-events: none;
323
+ }
324
+
325
+ /* ----------------------------------------
326
+ Compact control — pa-range-group
327
+ ---------------------------------------- */
328
+
329
+ $range-group-toggle-padding-v: $spacing-sm;
330
+ $range-group-toggle-padding-h: $spacing-md;
331
+ $range-group-panel-min-width: 32rem;
332
+ $range-group-panel-padding: $spacing-base;
333
+ $range-group-panel-row-gap: $spacing-lg;
334
+
335
+ .pa-range-group {
336
+ position: relative;
337
+ display: inline-flex;
338
+ }
339
+
340
+ .pa-range-group__toggle {
341
+ display: inline-flex;
342
+ align-items: center;
343
+ gap: $spacing-md;
344
+ width: 100%;
345
+ padding: $range-group-toggle-padding-v $range-group-toggle-padding-h;
346
+ border: $border-width-base solid var(--pa-border-color);
347
+ border-radius: var(--pa-border-radius);
348
+ background-color: var(--pa-input-bg);
349
+ color: var(--pa-text-color-1);
350
+ font-size: $font-size-sm;
351
+ line-height: 1.3;
352
+ text-align: start;
353
+ cursor: pointer;
354
+ transition: border-color 0.15s ease, box-shadow 0.15s ease;
355
+
356
+ &:hover {
357
+ border-color: var(--pa-accent-hover);
358
+ }
359
+
360
+ &:focus-visible {
361
+ outline: none;
362
+ border-color: var(--pa-accent);
363
+ box-shadow: 0 0 0 0.3rem var(--pa-accent-light);
364
+ }
365
+ }
366
+
367
+ // Single line: "LABEL value / LABEL value / …". Keeps the toggle the same
368
+ // height as sibling inputs; truncates with an ellipsis when it runs out of
369
+ // room. JS builds the segments (see updateSummary in range-group.js).
370
+ .pa-range-group__summary {
371
+ flex: 1;
372
+ min-width: 0;
373
+ font-size: $font-size-sm;
374
+ white-space: nowrap;
375
+ overflow: hidden;
376
+ text-overflow: ellipsis;
377
+ }
378
+
379
+ // One dimension's "LABEL value" pair.
380
+ .pa-range-group__seg-label {
381
+ font-size: $font-size-2xs;
382
+ font-weight: $font-weight-semibold;
383
+ letter-spacing: 0.02em;
384
+ text-transform: uppercase;
385
+ color: var(--pa-text-tertiary);
386
+ }
387
+
388
+ .pa-range-group__seg-value {
389
+ font-weight: $font-weight-medium;
390
+ color: var(--pa-text-color-1);
391
+ font-variant-numeric: tabular-nums;
392
+ }
393
+
394
+ // "Any" placeholder value — muted so active dimensions read as the signal.
395
+ .pa-range-group__seg-value--empty {
396
+ color: var(--pa-text-tertiary);
397
+ font-weight: $font-weight-normal;
398
+ }
399
+
400
+ // The " / " separators between segments.
401
+ .pa-range-group__seg-sep {
402
+ color: var(--pa-text-tertiary);
403
+ margin-inline: 0.4rem;
404
+ }
405
+
406
+ .pa-range-group__caret {
407
+ flex-shrink: 0;
408
+ font-size: $font-size-xs;
409
+ color: var(--pa-text-secondary);
410
+ transition: transform 0.2s ease;
411
+ }
412
+
413
+ .pa-range-group--open .pa-range-group__caret {
414
+ transform: rotate(180deg);
415
+ }
416
+
417
+ /* ---- Floating panel ---- */
418
+
419
+ .pa-range-group__panel {
420
+ display: none;
421
+ flex-direction: column;
422
+ gap: $range-group-panel-row-gap;
423
+ min-width: var(--pa-range-group-panel-min-width, #{$range-group-panel-min-width});
424
+ padding: $range-group-panel-padding;
425
+ border: $border-width-base solid var(--pa-border-color);
426
+ border-radius: var(--pa-border-radius-lg);
427
+ background-color: var(--pa-card-bg);
428
+ box-shadow: var(--pa-detail-shadow);
429
+ z-index: $z-index-dropdown;
430
+
431
+ &.pa-range-group__panel--open {
432
+ display: flex;
433
+ }
434
+ }
435
+
436
+ .pa-range-group__row {
437
+ display: flex;
438
+ flex-direction: column;
439
+ gap: $spacing-sm;
440
+ }
441
+
442
+ .pa-range-group__row-head {
443
+ display: flex;
444
+ align-items: baseline;
445
+ justify-content: space-between;
446
+ gap: $spacing-sm;
447
+ }
448
+
449
+ .pa-range-group__row-label {
450
+ font-size: $font-size-sm;
451
+ font-weight: $font-weight-semibold;
452
+ color: var(--pa-text-color-1);
453
+ }
454
+
455
+ .pa-range-group__row-value {
456
+ font-size: $font-size-sm;
457
+ font-weight: $font-weight-medium;
458
+ font-variant-numeric: tabular-nums;
459
+ // Tracks the fill colour so retinting the slider retints its readout too.
460
+ color: var(--pa-range-fill, var(--pa-accent));
461
+
462
+ &.pa-range-group__row-value--empty {
463
+ color: var(--pa-text-tertiary);
464
+ }
465
+ }
466
+
467
+ .pa-range-group__actions {
468
+ display: flex;
469
+ justify-content: flex-end;
470
+ gap: $spacing-sm;
471
+ margin-top: $spacing-xs;
472
+ padding-top: $range-group-panel-row-gap;
473
+ border-top: $border-width-base solid var(--pa-border-color);
474
+ }
@@ -118,6 +118,7 @@
118
118
  padding: $spacing-lg;
119
119
  min-height: $stat-square-min-size;
120
120
  min-width: $stat-square-min-size;
121
+ max-height: $stat-square-max-height; // keep full-width tiles from growing into page-dominating banners
121
122
  position: relative;
122
123
  border-radius: var(--pa-border-radius);
123
124
  box-shadow: $shadow-md;
@@ -183,6 +184,17 @@
183
184
  &.pa-stat--warning {
184
185
  background-color: var(--pa-warning-bg);
185
186
  color: var(--pa-btn-warning-text);
187
+
188
+ // Warning is the one square variant with DARK text (on a light amber
189
+ // surface). The number's default black text-shadow + drop-shadow stack is
190
+ // tuned for the white-text variants; against a dark glyph it has no
191
+ // contrast and smears into an "ink-blot" halo. Dial it to a whisper and
192
+ // drop the costly drop-shadow layer. Covers classic AND fit mode, since
193
+ // both render the number through .pa-stat__number.
194
+ .pa-stat__number {
195
+ text-shadow: 0 1px 1px rgba(0, 0, 0, $opacity-light);
196
+ filter: none;
197
+ }
186
198
  }
187
199
 
188
200
  &.pa-stat--danger {
@@ -194,6 +206,157 @@
194
206
  background-color: var(--pa-text-color-2);
195
207
  color: var(--pa-btn-primary-text);
196
208
  }
209
+
210
+ // ----------------------------------------------------------------
211
+ // FIT + PROGRESSIVE DISCLOSURE mode (opt-in: [data-pa-stat-fit])
212
+ // The number is sized to fill the tile by pa-stat-fit.js, which also
213
+ // wraps __number + __symbol into a __slot > __group at runtime. Lower-
214
+ // priority rows (label → change → context) reveal as the tile earns
215
+ // both width and height.
216
+ //
217
+ // REQUIRES A HEIGHT SOURCE on the tile (grid cell with a set height,
218
+ // explicit height, or aspect-ratio): container-type: size makes the
219
+ // box size independently of its content, so without a height it falls
220
+ // back to the $stat-square-min-size floor.
221
+ // ----------------------------------------------------------------
222
+ &[data-pa-stat-fit] {
223
+ container-type: size;
224
+ container-name: #{$stat-square-fit-container-name};
225
+ flex-direction: column;
226
+ flex-wrap: nowrap;
227
+ align-content: stretch;
228
+ align-items: stretch;
229
+ max-height: none; // fit height is container-driven; undo the base cap.
230
+ // min-height (the base $stat-square-min-size) stays as
231
+ // the default height when none is set + collapse floor.
232
+
233
+ // __slot grows to take the room not used by the metadata rows; the
234
+ // number is fit into it. ResizeObserver watches this element.
235
+ .pa-stat__slot {
236
+ flex: 1 1 auto;
237
+ min-height: 0;
238
+ display: flex;
239
+ align-items: center;
240
+ justify-content: flex-start;
241
+ }
242
+
243
+ // font-size is set inline (px) on __group by the JS; children scale via em.
244
+ .pa-stat__group {
245
+ display: inline-flex;
246
+ align-items: baseline;
247
+ column-gap: $stat-square-symbol-gap;
248
+ line-height: 1;
249
+ white-space: nowrap;
250
+ font-weight: $font-weight-bold;
251
+
252
+ .pa-stat__number { font-size: 1em; }
253
+ .pa-stat__symbol {
254
+ font-size: #{$stat-square-fit-symbol-ratio}em;
255
+ opacity: $stat-symbol-opacity;
256
+ }
257
+ }
258
+
259
+ // pa-stat-fit.js wraps the metadata rows into this column so the whole
260
+ // group can move beside the number in the horizontal (banner) layout.
261
+ // The number is the HERO; the metadata is subordinate — pa-stat-fit.js sets
262
+ // this column's font-size to a small fraction of the fitted number size
263
+ // (META_RATIO), so the rows below stay "supersmall" relative to the number at
264
+ // every tile size. The rows are em-relative so that one font-size cascades to
265
+ // all three. The value here is only the no-JS fallback.
266
+ .pa-stat__meta {
267
+ display: flex;
268
+ flex-direction: column;
269
+ min-width: 0; // let children ellipsis instead of overflowing
270
+ flex: 0 0 auto; // vertical layout: natural height below the slot
271
+ font-size: $stat-square-fit-meta-size; // JS overrides inline (px); this is the fallback
272
+ }
273
+
274
+ // metadata rows sit below the slot, each on its own line — all em-relative to
275
+ // __meta's (JS-driven) font-size so they scale with the hero number.
276
+ .pa-stat__label {
277
+ flex: 0 0 auto; // undo the base square's flex-basis:100%
278
+ margin-top: 0.35em;
279
+ font-size: 1em; // the meta base — the largest metadata line
280
+ }
281
+ .pa-stat__change {
282
+ flex: 0 0 auto;
283
+ margin-top: 0.35em;
284
+ font-size: 0.9em;
285
+ font-weight: $font-weight-semibold;
286
+ line-height: 1.2;
287
+ color: inherit; // default to the tile's text colour…
288
+ white-space: nowrap;
289
+ overflow: hidden;
290
+ text-overflow: ellipsis;
291
+
292
+ // …opt-in sentiment tints (same scale as the hero variant's __change)
293
+ &--positive { color: var(--pa-positive); }
294
+ &--neutral { color: var(--pa-neutral); }
295
+ &--negative { color: var(--pa-negative); }
296
+ }
297
+ .pa-stat__context {
298
+ flex: 0 0 auto;
299
+ margin-top: 0.35em;
300
+ font-size: 0.8em;
301
+ opacity: 0.7;
302
+ white-space: nowrap;
303
+ overflow: hidden;
304
+ text-overflow: ellipsis;
305
+ }
306
+
307
+ // ---- priority ladder: P0 number always; reveal P1–P4 with space ----
308
+ .pa-stat__symbol,
309
+ .pa-stat__label,
310
+ .pa-stat__change,
311
+ .pa-stat__context { display: none; }
312
+
313
+ @container #{$stat-square-fit-container-name} (min-width: #{$stat-square-fit-symbol-min-w}) and (min-height: #{$stat-square-fit-symbol-min-h}) {
314
+ .pa-stat__symbol { display: inline; }
315
+ }
316
+ @container #{$stat-square-fit-container-name} (min-width: #{$stat-square-fit-label-min-w}) and (min-height: #{$stat-square-fit-label-min-h}) {
317
+ .pa-stat__label { display: block; }
318
+ }
319
+ @container #{$stat-square-fit-container-name} (min-width: #{$stat-square-fit-change-min-w}) and (min-height: #{$stat-square-fit-change-min-h}) {
320
+ .pa-stat__change { display: block; }
321
+ }
322
+ @container #{$stat-square-fit-container-name} (min-width: #{$stat-square-fit-context-min-w}) and (min-height: #{$stat-square-fit-context-min-h}) {
323
+ .pa-stat__context { display: block; }
324
+ }
325
+
326
+ // ---- HORIZONTAL "banner" layout for wide tiles ----
327
+ // Toggled by pa-stat-fit.js (.pa-stat--fit-wide) when the tile is at least
328
+ // $stat-square-fit-horizontal-min-w wide. This CANNOT be a `@container`
329
+ // query: the tile is its own size container, and a container query only
330
+ // restyles a container's DESCENDANTS — never the container element itself,
331
+ // so the tile's own `flex-direction` would never flip. A JS class on the
332
+ // tile sidesteps that.
333
+ //
334
+ // Number on the LEFT (__slot is first in DOM → first flex item), filling
335
+ // the height so it stays big; the metadata column sits to its RIGHT,
336
+ // vertically centred. All meta rows are revealed here regardless of height
337
+ // — stacked beside the number they don't need vertical room — so a short
338
+ // wide banner still shows trend + context.
339
+ &.pa-stat--fit-wide {
340
+ flex-direction: row;
341
+ align-items: stretch;
342
+ column-gap: $spacing-lg;
343
+
344
+ .pa-stat__slot {
345
+ flex: 0 0 #{$stat-square-fit-number-width};
346
+ min-width: 0; // never let the number push past its column
347
+ justify-content: center; // centre the number within its column
348
+ }
349
+ .pa-stat__meta {
350
+ flex: 1 1 auto;
351
+ min-width: 0; // allow the rows to ellipsis
352
+ justify-content: center; // centre the rows vertically beside the number
353
+ }
354
+ .pa-stat__symbol { display: inline; }
355
+ .pa-stat__label,
356
+ .pa-stat__change,
357
+ .pa-stat__context { display: block; }
358
+ }
359
+ }
197
360
  }
198
361
  }
199
362