@apliteni/apliteni-ui 0.131.0 → 0.133.0

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/src/index.d.ts CHANGED
@@ -95,6 +95,38 @@ export declare function dropdownHeightFit(
95
95
  dd: Element | null | undefined,
96
96
  panel: Element | null | undefined,
97
97
  ): number | null;
98
+ /** Stop an open panel's height where it cuts a row through the middle, so the list
99
+ * reads as going on: the smaller of its cap and its measured room. Call it with the
100
+ * panel laid out and its room written.
101
+ * why: docs/components.md#the-dropdown-panel */
102
+ export declare function dropdownSnap(
103
+ dd: Element | null | undefined,
104
+ panel: Element | null | undefined,
105
+ ): void;
106
+ /** Lift an open panel out of a Modal or Drawer body too short to hold its field and
107
+ * the panel's floor, so it opens over the body the way a portalled panel does, or put a
108
+ * lifted one back. Returns whether the panel is lifted.
109
+ * why: docs/components.md#the-dropdown-panel */
110
+ export declare function dropdownLift(
111
+ dd: Element | null | undefined,
112
+ panel: Element | null | undefined,
113
+ ): boolean;
114
+ /** Scroll the body an open panel sits in the least distance that shows the whole
115
+ * panel and its field's label, with the room under the panel kept. Returns the distance scrolled, 0 when
116
+ * nothing moved. why: docs/components.md#the-dropdown-panel */
117
+ export declare function dropdownReveal(
118
+ dd: Element | null | undefined,
119
+ panel: Element | null | undefined,
120
+ ): number;
121
+ /** Give an open panel its box, and take the box away from a shut one once its fade is
122
+ * over, so a shut panel adds no scroll room. Returns a function that cancels a
123
+ * collapse still waiting. why: docs/components.md#the-dropdown-panel */
124
+ export declare function dropdownRest(
125
+ dd: Element | null | undefined,
126
+ panel: Element | null | undefined,
127
+ open: boolean,
128
+ wait?: number,
129
+ ): () => void;
98
130
  export declare function backLink(opts?: Record<string, unknown>): string;
99
131
  export declare function statBand(opts?: Record<string, unknown>): string;
100
132
  export declare function commandPalette(opts?: Record<string, unknown>): string;
@@ -37,3 +37,25 @@ export const dropdownAvail = ({ anchorTop, anchorBottom, viewport, gap, inset, m
37
37
  const room = Math.max(up ? anchorTop - gap : viewport - anchorBottom - gap, 0);
38
38
  return room < min ? Infinity : Math.max(room - inset, min);
39
39
  };
40
+
41
+ /**
42
+ * Where a capped panel stops so that its last row is cut through the middle: the
43
+ * list then reads as going on, and a half row is the least that says so. `rows` are
44
+ * the rows' tops and heights from the panel's top edge, scrolled to its start;
45
+ * `below` is the panel's box under the part that scrolls; `content` is the panel's
46
+ * full height; `ceiling` is the tallest it may be. `reach`, past the ceiling, is room
47
+ * that may be spent to stop at the next half row instead of a whole row short: the
48
+ * least of it that does so is taken. Returns the height, or `null` where the panel fits
49
+ * whole under the ceiling or no row's middle fits under it.
50
+ */
51
+ export const dropdownStop = ({ rows, below, content, ceiling, reach = ceiling }) => {
52
+ if (!(content > ceiling)) return null;
53
+ let stop = null;
54
+ let over = null;
55
+ for (const { top, height } of rows) {
56
+ const cut = Math.floor(top + height / 2 + below);
57
+ if (cut <= ceiling && (stop === null || cut > stop)) stop = cut;
58
+ else if (cut > ceiling && cut <= reach && (over === null || cut < over)) over = cut;
59
+ }
60
+ return over ?? stop;
61
+ };
@@ -93,6 +93,10 @@
93
93
  /* Title to description: the step, not the 5px it was. why: docs/foundations.md#spacing-and-rhythm */
94
94
  margin: 0 0 var(--space-2);
95
95
  }
96
+ /* The title of a card that supports its neighbour, such as a document beside the
97
+ data read from it. The size, weight and rank stay the card title's.
98
+ why: docs/foundations.md#text-ink */
99
+ .ui-card__title--muted { /* muted-ink: chrome — a supporting card ranks below its neighbour; the title is also the card's accessible name */ color: var(--muted); }
96
100
  .ui-card__sub {
97
101
  color: var(--text);
98
102
  font-size: var(--text-base);
@@ -6,6 +6,22 @@
6
6
  * ========================================================================== */
7
7
 
8
8
  .ui-dropdown { position: relative; display: inline-flex; }
9
+ /* Inside a scrolling body the wiring writes --ui-dropdown-reach when it opens the
10
+ panel: the distance from the trigger to one edge inset past the panel. The panel is
11
+ the last thing the body holds, so the body ends where the panel ends; this 1px spot
12
+ lets it scroll that far. It stays through the fade and until the reader has scrolled
13
+ out of it, so closing does not move the field, and it paints nothing.
14
+ why: docs/components.md#the-dropdown-panel */
15
+ .ui-dropdown[style*="--ui-dropdown-reach"]::after {
16
+ content: "";
17
+ position: absolute;
18
+ inset-inline-start: 0;
19
+ top: 100%;
20
+ width: 1px;
21
+ height: 1px;
22
+ margin-top: var(--ui-dropdown-reach);
23
+ pointer-events: none;
24
+ }
9
25
 
10
26
  /* Trigger — pill button: [pre] [value] [chevron] */
11
27
  .ui-dropdown__trigger {
@@ -88,12 +104,26 @@
88
104
  the screen's edge sets it to 0.
89
105
  why: docs/components.md#the-dropdown-panel */
90
106
  --ui-dropdown-edge: 8px;
107
+ /* What a panel that opens down inside a Modal or Drawer body keeps between its end
108
+ and the body's end. The drop shadow fades out over 43px in light and 15px in dark,
109
+ measured from pixels; the body clips whatever is left of it. 48px is the 8px
110
+ step above the light figure. why: docs/components.md#the-dropdown-panel */
111
+ --ui-dropdown-shade: 48px;
91
112
  /* The least height a capped panel keeps before it would rather spend the edge
92
113
  inset above than shrink further — three rows or so. Below it the panel
93
114
  still ends at or before the edge; it never spends room past the edge to
94
115
  reach this floor, because that room does not exist. #489
95
116
  why: docs/components.md#the-dropdown-panel */
96
117
  --ui-dropdown-min: 120px;
118
+ /* The tallest a panel grows before it scrolls inside itself. It is the figure the
119
+ reference systems settle on, 256 to 300px, and the one `scroll` and the search
120
+ list already used. The wiring stops the cap short of it, in the middle of a row
121
+ (--ui-dropdown-fit), so the last row is cut and the list is seen to go on. That
122
+ shows 7 whole rows and half of the eighth in a plain list, 4 and a half with a
123
+ description line, 5 and a half under a search field and 3 and a half with both.
124
+ A consumer asks for another cap with `scroll: <n>`.
125
+ why: docs/components.md#the-dropdown-panel */
126
+ --ui-dropdown-max: 300px;
97
127
  /* Named for the same reason the offset is: a block pinned to an edge has to
98
128
  bleed back through it, and #306 is a page copying the number by hand.
99
129
  why: docs/components.md#the-dropdown-panel */
@@ -142,15 +172,18 @@
142
172
  /* The cap, and the whole of #489: a panel took its content's height until that
143
173
  height ran off the screen, and then it took what was left. Two numbers decide
144
174
  it and the smaller wins — --ui-dropdown-cap is what the consumer asked for
145
- (the `scroll` option writes it), --ui-dropdown-avail is the room between the
146
- trigger and the viewport edge, which the wiring measures on every open and
147
- every scroll. Neither is declared here: an unset cap means the consumer wants
148
- the whole list, and an unset avail means nothing has measured yet, which is a
149
- server-rendered page before hydration — so the fallback promises the same
150
- thing at a coarser grain, the viewport less the edge inset at each end.
175
+ (the `scroll` option writes it, and --ui-dropdown-max stands in when it is
176
+ unset), --ui-dropdown-avail is the room between the trigger and the viewport
177
+ edge, or the body's, which the wiring measures when the panel opens. Between
178
+ them --ui-dropdown-fit is the height the wiring stops the smaller at, in the
179
+ middle of a row. An unset avail
180
+ means nothing has measured yet, which is a server-rendered page before
181
+ hydration — so the fallback promises the same thing at a coarser grain, the
182
+ viewport less the edge inset at each end.
151
183
  why: docs/components.md#the-dropdown-panel */
152
184
  max-height: min(
153
- var(--ui-dropdown-cap, 100dvh),
185
+ var(--ui-dropdown-cap, var(--ui-dropdown-max)),
186
+ var(--ui-dropdown-fit, 100dvh),
154
187
  var(--ui-dropdown-avail, calc(100dvh - 2 * var(--ui-dropdown-edge)))
155
188
  );
156
189
  overflow-y: auto;
@@ -173,21 +206,40 @@
173
206
  z-index: var(--z-dropdown);
174
207
  }
175
208
  .ui-dropdown__panel.is-end { left: auto; right: 0; }
176
- /* `.is-scroll` asks for 300px specifically; the panel above caps every panel at
177
- the room the viewport left either way. #489 */
178
- .ui-dropdown__panel.is-scroll { --ui-dropdown-cap: 300px; }
209
+ /* `.is-scroll` asks for the standard cap by name; the panel above caps every panel
210
+ at the room the viewport left either way. #489 */
211
+ .ui-dropdown__panel.is-scroll { --ui-dropdown-cap: var(--ui-dropdown-max); }
179
212
 
180
- /* The wiring adds this where the floor (above) left the panel uncapped: below
181
- 120px even spending the whole edge inset, there is no usable cap, so the
182
- panel keeps its content height and the page grows to hold it instead —
183
- #641's danger 1. That panel has nothing of its own to scroll, so the
184
- `overflow-y: auto` above still claims a wheel over it for nothing, and
185
- `overscroll-behavior: contain` then stops the gesture from reaching the
186
- page that is now the only way to the rows below the fold. `visible` lets it
187
- through. A panel that also carries `.is-scroll` or a numeric cap keeps
188
- `auto`: that cap is still real room to scroll inside. why: dropdownAvail(),
189
- src/logic/dropdown.js */
190
- .ui-dropdown__panel.is-unbounded { overflow-y: visible; }
213
+ /* The wiring adds this where the room is below the floor: even spending the whole
214
+ edge inset, there is no usable height. The panel keeps its cap — it is never
215
+ taller than 300px — and scrolls inside it. Its room runs past the fold, so the
216
+ page is the way to the rows below it, and `overscroll-behavior: contain` above
217
+ would stop the wheel at the panel's own end instead of passing it on. `auto`
218
+ lets it through. why: dropdownAvail(), src/logic/dropdown.js */
219
+ .ui-dropdown__panel.is-unbounded { overscroll-behavior: auto; }
220
+
221
+ /* The wiring lifts a panel out of a Modal or Drawer body too short to hold the field's
222
+ label and trigger, the floor and the 8px edge under it. Fixed, it opens over the
223
+ body the way a portalled panel does, and the wiring writes its edges from the
224
+ trigger's. The value is the side it opens on. why: docs/components.md#the-dropdown-panel */
225
+ .ui-dropdown__panel[data-dd-lifted] { position: fixed; }
226
+ .ui-dropdown__panel[data-dd-lifted="up"] { transform: translateY(6px); }
227
+
228
+ /* A search list that carries the consumer's own cap keeps it, and the panel is that cap
229
+ and the search field over it. The wiring writes the sum as --ui-dropdown-fit; this
230
+ lifts the standard cap out of its way. why: docs/components.md#the-dropdown-panel */
231
+ .ui-dropdown__panel--search:has(> .ui-dropdown__list[style*="--ui-dropdown-cap"]) { --ui-dropdown-max: 100dvh; }
232
+
233
+ /* A shut panel is still laid out, and a box that is laid out counts towards the
234
+ scroll of the body or page it sits in. The wiring collapses it once its fade is
235
+ over and gives the box back before the next open measures it. The width stays, so
236
+ the horizontal fit reads the same number. why: docs/components.md#the-dropdown-panel */
237
+ .ui-dropdown__panel[data-dd-shut] {
238
+ max-height: 0;
239
+ padding-block: 0;
240
+ border-block-width: 0;
241
+ overflow: hidden;
242
+ }
191
243
 
192
244
  /* Chrome makes a scroll container a keyboard stop on its own — no tabindex, no
193
245
  author rule — so `.is-scroll` took the browser's outline, black in light mode.
@@ -396,7 +448,7 @@
396
448
 
397
449
  /* The rows' own cap, read from the same property the panel reads: the `scroll`
398
450
  option lands on the list when there is a field above it. #489 */
399
- .ui-dropdown__list { max-height: var(--ui-dropdown-cap, 300px); overflow-y: auto; overscroll-behavior: contain; }
451
+ .ui-dropdown__list { max-height: var(--ui-dropdown-cap, var(--ui-dropdown-fit, var(--ui-dropdown-max, 300px))); overflow-y: auto; overscroll-behavior: contain; }
400
452
  /* Rows are `role="option" tabindex="-1"`, so the LIST takes the keyboard stop and
401
453
  answered with the native outline. The band is drawn INWARD, on the list: an outset
402
454
  one 6px inside a --radius-lg corner leaves the panel, and the panel is not the box
@@ -81,3 +81,42 @@
81
81
 
82
82
  .ui-drop.is-disabled .ui-drop__row { /* muted-ink: state — the drop accepts nothing */ color: var(--disabled-ink); }
83
83
  .ui-drop.is-disabled .ui-drop__name { /* muted-ink: state — the drop accepts nothing */ color: var(--disabled-ink); }
84
+
85
+ /* The empty page's drop area: the whole region is the target, drawn as a dashed
86
+ box around the first-run block. A file over it turns the edge to the accent,
87
+ the screen's one accent line, the way .ui-drop__target draws its own. */
88
+ .ui-drop--area { border: 1px dashed var(--border-strong); border-radius: var(--radius-lg); padding: var(--space-4); }
89
+ .ui-drop--area.is-dragging { border-color: var(--accent); }
90
+ @media (max-width: 560px) { .ui-drop--area { padding: var(--space-3); } }
91
+
92
+ /* The whole page as a target, drawn only while a file is dragged over the page. It is the
93
+ empty page's drop area made large: the same dashed accent edge and radius, holding the upload
94
+ glyph, a line that says to drop the files and the accepted types, laid over the page content below
95
+ the topbar. Its ground is the page ground, slightly transparent and blurred, so the table shows
96
+ through softly and no row stays sharp under the words. Where the engine cannot blur the backdrop,
97
+ or the reader asks for less transparency, the ground is solid. The layer takes no pointer events,
98
+ so the drop lands on what is under it. */
99
+ .ui-drop--page {
100
+ position: fixed;
101
+ inset: calc(var(--ui-app-band, 0px) + var(--ui-app-top, 0px) + var(--space-4)) var(--space-4) var(--space-4);
102
+ z-index: var(--z-overlay);
103
+ display: flex;
104
+ align-items: center;
105
+ justify-content: center;
106
+ pointer-events: none;
107
+ background: var(--bg); /* code-bg: inherit — the layer holds no focus target and takes no pointer. */
108
+ }
109
+ @supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
110
+ .ui-drop--page {
111
+ background: color-mix(in srgb, var(--bg) 80%, transparent); /* code-bg: inherit — the layer holds no focus target and takes no pointer. */
112
+ -webkit-backdrop-filter: blur(var(--space-2));
113
+ backdrop-filter: blur(var(--space-2));
114
+ }
115
+ }
116
+ @media (prefers-reduced-transparency: reduce) {
117
+ .ui-drop--page {
118
+ background: var(--bg); /* code-bg: inherit — the layer holds no focus target and takes no pointer. */
119
+ -webkit-backdrop-filter: none;
120
+ backdrop-filter: none;
121
+ }
122
+ }
@@ -332,7 +332,7 @@
332
332
  the three heights below are for. Level, not one continuous stroke: the rail insets
333
333
  its own rule, and in light that half is 1.009:1 on the rail's ground.
334
334
  why: docs/components.md#the-second-layout */
335
- .ui-app { --ui-app-band: 52px; }
335
+ .ui-app--topbar { --ui-app-band: 52px; }
336
336
  .ui-app__well { display: flex; flex-direction: column; min-width: 0; }
337
337
  .ui-app__bar {
338
338
  display: flex;
@@ -516,15 +516,17 @@
516
516
  @media (max-width: 720px) {
517
517
  /* A column, not a grid: the band sticks to the top of the shell, and a sticky grid
518
518
  item sticks only inside its own row. */
519
- .ui-app { display: flex; flex-direction: column; }
519
+ .ui-app { display: flex; flex-direction: column; --ui-app-band: 52px; }
520
520
  .ui-app > .ui-app__rail { display: none; }
521
521
  .ui-app__well { flex: 1 1 auto; }
522
522
  .ui-app > .ui-app__main { flex: 1 1 auto; }
523
523
  .ui-app > .ui-app__bar--phone { display: flex; }
524
524
  .ui-btn.ui-app__menu { display: inline-flex; }
525
525
  .ui-app__brand--band { display: flex; }
526
- /* The menu control stands at the band's start with the band's inset beside it. */
527
- .ui-app__bar { padding-inline: var(--space-2) var(--space-4); }
526
+ /* The first visible item lines up with the page. A leading menu uses the smaller
527
+ inset to keep its phone position in both shells. */
528
+ .ui-app__bar { padding-inline: var(--space-4); }
529
+ .ui-app__bar:has(> .ui-app__menu:first-child) { padding-inline-start: var(--space-2); }
528
530
  /* The drawer paints itself and sits out of the flow, so its wrapper takes no height. */
529
531
  .ui-app__drawer { display: block; }
530
532
  /* The rail's rows are the drawer's rows: the column fills the panel, and the foot
@@ -206,10 +206,10 @@
206
206
  transition: background var(--dur-fast) var(--ease);
207
207
  }
208
208
  /* Menu rows use the reading surface and an edge on hover — see
209
- src/styles/dropdown.css:271 `.ui-dropdown__item:hover`.
209
+ src/styles/dropdown.css:323 `.ui-dropdown__item:hover`.
210
210
  why: docs/foundations.md#elevation */
211
211
  .vopt:hover { outline: 1px solid var(--control-edge); outline-offset: -1px; background: var(--surface); /* code-bg: inherit — control paint; the containing surface is the ground, for a chip and for a band alike. */ }
212
- /* The row is a keyboard stop: src/components/dropdown.js:1191
212
+ /* The row is a keyboard stop: src/components/dropdown.js:1663
213
213
  `items[next < 0 ? 0 : next]?.focus()` moves focus onto a `[data-dd-item]` with
214
214
  the arrow keys, and this is one. It takes the ring on its own 10px radius, the
215
215
  same way `.amenu a` above does. #482 */