@apliteni/apliteni-ui 0.98.0 → 0.103.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apliteni/apliteni-ui",
3
- "version": "0.98.0",
3
+ "version": "0.103.0",
4
4
  "workspaces": [
5
5
  "react"
6
6
  ],
@@ -36,10 +36,10 @@
36
36
  --tap-clear-y: 0px;
37
37
  }
38
38
  @media (max-width: 560px) and (pointer: coarse) {
39
- :where(.ui-seg button, .ui-tabs__tab, .ui-dropdown__trigger, .ui-toast__action, .ui-drawer__close, .ui-snippet__copy, .ui-footer__social, .ui-datepicker__opt) {
39
+ :where(.ui-seg button, .ui-tabs__tab, .ui-dropdown__trigger, .ui-toast__action, .ui-drawer__close, .ui-snippet__copy, .ui-footer__social, .ui-app__search, .toggle, .ui-datepicker__opt) {
40
40
  position: relative;
41
41
  }
42
- :where(.ui-btn, .ui-seg button, .ui-tabs__tab, .ui-dropdown__trigger, .ui-toast__action, .ui-drawer__close, .ui-snippet__copy, .ui-footer__social, .ui-switch, .ui-datepicker__opt)::after {
42
+ :where(.ui-btn, .ui-seg button, .ui-tabs__tab, .ui-dropdown__trigger, .ui-toast__action, .ui-drawer__close, .ui-snippet__copy, .ui-footer__social, .ui-switch, .ui-datepicker__opt, .ui-app__search, .toggle)::after {
43
43
  content: "";
44
44
  position: absolute;
45
45
  left: 50%;
@@ -133,6 +133,11 @@
133
133
  --tap-clear-x: var(--tap-gap);
134
134
  --tap-clear-y: var(--tap-gap);
135
135
  }
136
+ .ui-app__bar,
137
+ .topbar__in {
138
+ --tap-clear-x: var(--space-3, 12px);
139
+ --tap-clear-y: var(--space-3, 12px);
140
+ }
136
141
  }
137
142
 
138
143
  /* ../src/styles/tooltip.css */
@@ -820,7 +825,7 @@
820
825
  .rx-toast-stack {
821
826
  position: fixed;
822
827
  right: var(--space-4);
823
- bottom: var(--space-4);
828
+ bottom: calc(var(--space-4) + var(--ui-app-bottom-clearance, 0px));
824
829
  max-width: calc(100% - 2 * var(--space-4));
825
830
  z-index: var(--z-toast);
826
831
  pointer-events: none;
@@ -846,23 +851,33 @@
846
851
  .ui-react-app__bottom .ui-nav__item {
847
852
  min-height: 48px;
848
853
  }
854
+ .ui-react-app:not(:has(> .ui-app__rail)) {
855
+ grid-template-columns: minmax(0, 1fr);
856
+ }
857
+ .ui-react-app .ui-app__bar > .ui-app__brand {
858
+ flex: none;
859
+ margin-inline-end: var(--space-2);
860
+ }
849
861
  @media (max-width: 560px) {
862
+ :root:has(.ui-react-app__bottom) {
863
+ --ui-app-bottom-clearance: calc(96px + env(safe-area-inset-bottom));
864
+ }
865
+ :root:has(.ui-react-app) {
866
+ scroll-padding-bottom: calc(var(--space-5) + var(--ui-app-bottom-clearance, 0px));
867
+ }
850
868
  .ui-react-app {
851
869
  display: block;
852
870
  }
853
871
  .ui-react-app > .ui-app__rail {
854
872
  display: none;
855
873
  }
856
- .ui-react-app .ui-app__main {
857
- padding-bottom: calc(96px + env(safe-area-inset-bottom));
874
+ :root:has(.ui-react-app__bottom) .ui-react-app .ui-app__main {
875
+ padding-bottom: var(--ui-app-bottom-clearance, 0px);
858
876
  }
859
877
  .ui-react-app .ui-app__bar {
860
878
  padding-inline: var(--space-4);
861
879
  }
862
- .ui-react-app .ui-app__search {
863
- min-width: 0;
864
- }
865
- .ui-react-app .ui-app__search kbd {
880
+ .ui-react-app .ui-app__bar > .ui-app__brand {
866
881
  display: none;
867
882
  }
868
883
  .ui-react-app__bottom {
@@ -776,6 +776,10 @@ type AppShellProps = {
776
776
  lede?: string;
777
777
  actions?: ReactNode;
778
778
  bandControl?: ReactNode;
779
+ /** The band trigger's words. Below 560px the shell draws `searchShortLabel` instead;
780
+ * both stay the button's name, which does not change with the viewport. */
781
+ searchLabel?: string;
782
+ searchShortLabel?: string;
779
783
  account: {
780
784
  name: string;
781
785
  email: string;
@@ -787,7 +791,7 @@ type AppShellProps = {
787
791
  /** Spread linkProps onto the router link, including children and onClick. */
788
792
  renderLink?: (section: AppShellSection, linkProps: ComponentPropsWithoutRef<'a'>) => ReactNode;
789
793
  };
790
- declare function AppShell({ sections, pathname, title, children, word, brand, brandHref, width, back, lede, actions, bandControl, account, onSignOut, defaultCollapsed, palette, renderLink }: AppShellProps): react.JSX.Element;
794
+ declare function AppShell({ sections, pathname, title, children, word, brand, brandHref, width, back, lede, actions, bandControl, account, onSignOut, searchLabel, searchShortLabel, defaultCollapsed, palette, renderLink }: AppShellProps): react.JSX.Element;
791
795
 
792
796
  type EmptyStateVariant = 'first-run' | 'no-matches' | 'not-found' | 'not-yet-built';
793
797
  type EmptyStateProps = {
@@ -3407,6 +3407,8 @@ function AppShell({
3407
3407
  bandControl,
3408
3408
  account,
3409
3409
  onSignOut,
3410
+ searchLabel = "Search or run a command\u2026",
3411
+ searchShortLabel = "Search",
3410
3412
  defaultCollapsed = false,
3411
3413
  palette,
3412
3414
  renderLink
@@ -3488,15 +3490,17 @@ function AppShell({
3488
3490
  }
3489
3491
  );
3490
3492
  const foldLabel = collapsed ? "Expand sidebar" : "Collapse sidebar";
3493
+ const navigable = sections.length > 1 || Boolean(back);
3491
3494
  const visible = sections.length > 4 ? sections.slice(0, 3) : sections;
3492
3495
  const remaining = sections.slice(visible.length);
3493
3496
  const initials = account.name.trim().split(/\s+/).map((part) => part[0]).slice(0, 2).join("");
3497
+ const lockup = /* @__PURE__ */ jsxs29("a", { className: "ui-app__brand", href: brandHref, "aria-label": word, children: [
3498
+ brand ?? /* @__PURE__ */ jsx34("i", { "aria-hidden": "true", className: "ui-react-app__mark", dangerouslySetInnerHTML: { __html: prism(uid, 24) } }),
3499
+ /* @__PURE__ */ jsx34("span", { children: word })
3500
+ ] });
3494
3501
  return /* @__PURE__ */ jsxs29("div", { className: `ui-app ui-app--topbar ui-react-app${collapsed ? " is-collapsed" : ""}`, children: [
3495
- /* @__PURE__ */ jsxs29("div", { className: "ui-app__rail", children: [
3496
- /* @__PURE__ */ jsx34("div", { className: "ui-app__head", children: /* @__PURE__ */ jsxs29("a", { className: "ui-app__brand", href: brandHref, "aria-label": word, children: [
3497
- brand ?? /* @__PURE__ */ jsx34("i", { "aria-hidden": "true", className: "ui-react-app__mark", dangerouslySetInnerHTML: { __html: prism(uid, 24) } }),
3498
- /* @__PURE__ */ jsx34("span", { children: word })
3499
- ] }) }),
3502
+ navigable && /* @__PURE__ */ jsxs29("div", { className: "ui-app__rail", children: [
3503
+ /* @__PURE__ */ jsx34("div", { className: "ui-app__head", children: lockup }),
3500
3504
  sidebar(sections, "Sections"),
3501
3505
  /* @__PURE__ */ jsx34("div", { className: "ui-app__foot", children: /* @__PURE__ */ jsx34("div", { className: "ui-app__fold-row", children: /* @__PURE__ */ jsxs29("button", { type: "button", className: "ui-nav__item ui-app__fold", "aria-expanded": !collapsed, "aria-label": foldLabel, onClick: fold, children: [
3502
3506
  /* @__PURE__ */ jsx34("span", { className: "ui-nav__ic", children: /* @__PURE__ */ jsxs29("svg", { className: "ui-icon", viewBox: "0 0 24 24", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
@@ -3509,9 +3513,11 @@ function AppShell({
3509
3513
  ] }),
3510
3514
  /* @__PURE__ */ jsxs29("div", { className: "ui-app__well", children: [
3511
3515
  /* @__PURE__ */ jsxs29("header", { className: "ui-app__bar", children: [
3516
+ !navigable && lockup,
3512
3517
  /* @__PURE__ */ jsxs29("button", { type: "button", className: "ui-app__search", "aria-haspopup": "dialog", onClick: () => setSearch(true), children: [
3513
3518
  /* @__PURE__ */ jsx34("span", { className: "ui-app__search-ic", children: /* @__PURE__ */ jsx34(Icon, { name: "search" }) }),
3514
- /* @__PURE__ */ jsx34("span", { className: "ui-app__search-txt", children: "Search or run a command\u2026" }),
3519
+ /* @__PURE__ */ jsx34("span", { className: "ui-app__search-txt", children: searchLabel }),
3520
+ /* @__PURE__ */ jsx34("span", { className: "ui-app__search-short", "aria-hidden": "true", children: searchShortLabel }),
3515
3521
  /* @__PURE__ */ jsx34("kbd", { className: "ui-cmdk__key", children: paletteHotkey() })
3516
3522
  ] }),
3517
3523
  /* @__PURE__ */ jsx34("span", { className: "ui-app__bar-gap" }),
@@ -3544,7 +3550,7 @@ function AppShell({
3544
3550
  /* @__PURE__ */ jsx34("div", { className: "ui-app__body", children })
3545
3551
  ] })
3546
3552
  ] }),
3547
- /* @__PURE__ */ jsx34("nav", { className: "ui-nav ui-react-app__bottom", "aria-label": "Sections on mobile", children: /* @__PURE__ */ jsxs29("ul", { className: "ui-nav__list", children: [
3553
+ navigable && /* @__PURE__ */ jsx34("nav", { className: "ui-nav ui-react-app__bottom", "aria-label": "Sections on mobile", children: /* @__PURE__ */ jsxs29("ul", { className: "ui-nav__list", children: [
3548
3554
  links(visible),
3549
3555
  remaining.length > 0 && /* @__PURE__ */ jsx34("li", { children: /* @__PURE__ */ jsxs29(
3550
3556
  "button",
@@ -91,12 +91,17 @@ const toWidth = (v) => (v === 'wide' ? 'wide' : 'centered');
91
91
 
92
92
  // `search: 'palette-id'`, or the same id in `{ palette }` with a placeholder beside
93
93
  // it. No palette to open, no field — the argument `signOutHref` takes.
94
- // why: docs/components.md#the-page-shell
94
+ // `shortLabel` is the word the band draws below 560px, where the sentence used to
95
+ // truncate mid-word. why: docs/components.md#the-page-shell
95
96
  const toSearch = (v) => {
96
97
  const given = typeof v === 'string' ? { palette: v } : (isRecord(v) && !Array.isArray(v) ? v : null);
97
98
  const palette = given && typeof given.palette === 'string' ? given.palette.trim() : '';
98
99
  if (!palette) return null;
99
- return { palette, placeholder: str(given.placeholder) || 'Search or run a command…' };
100
+ return {
101
+ palette,
102
+ placeholder: str(given.placeholder) || 'Search or run a command…',
103
+ shortLabel: str(given.shortLabel) || 'Search',
104
+ };
100
105
  };
101
106
 
102
107
  // `maxWidth` lands inside a style attribute, so a length is all this accepts — a number
@@ -236,10 +241,13 @@ function readerBlock({ name, email }, signOutHref, { band = false } = {}) {
236
241
  // A field to look at, a button to press. `data-cmdk-open` and the cap's class are the
237
242
  // palette's own, and the cap is NOT aria-hidden: it is half the button's name.
238
243
  // why: docs/components.md#the-page-shell
239
- const searchField = ({ palette, placeholder }) =>
244
+ // The short word repeats the sentence, so it is `aria-hidden`: the sentence and the
245
+ // cap are the name at every width, and layout.css clips rather than drops them.
246
+ const searchField = ({ palette, placeholder, shortLabel }) =>
240
247
  `<button type="button" class="ui-app__search" data-cmdk-open="${esc(palette)}" aria-haspopup="dialog">`
241
248
  + `<span class="ui-app__search-ic" aria-hidden="true">${icon('search')}</span>`
242
249
  + `<span class="ui-app__search-txt">${esc(placeholder)}</span>`
250
+ + `<span class="ui-app__search-short" aria-hidden="true">${esc(shortLabel)}</span>`
243
251
  + `<kbd class="ui-cmdk__key" data-palette-hotkey>${esc(paletteHotkey())}</kbd>`
244
252
  + `</button>`;
245
253
 
@@ -119,7 +119,7 @@
119
119
  resting step. */
120
120
  /* Re-tuned at #295, which took the light rail down a step with the page. Below 720px
121
121
  the glyph is the whole row, so it is held at text grade rather than at 1.4.11's bar —
122
- stories/apps/shell-states.test.js:1597 `a resting glyph is dimmer but still legible`. */
122
+ stories/apps/shell-states.test.js:1704 `a resting glyph is dimmer but still legible`. */
123
123
  .ui-app__rail .ui-nav__ic svg { opacity: 0.66; }
124
124
 
125
125
  /* A destructive row of the rail is a navigation row and rests like one; the danger
@@ -404,11 +404,26 @@
404
404
  .ui-app__search-txt { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
405
405
  /* The other half of the name, so it never gives way. */
406
406
  .ui-app__search kbd { flex: none; }
407
+ /* The one word the phone band draws instead of the sentence. Above 560px the
408
+ sentence fits and this is a second copy of it, so it is not drawn.
409
+ why: docs/components.md#the-page-shell */
410
+ .ui-app__search-short { display: none; }
407
411
 
408
412
  /* The mark alone — no room for two lines on a 52px row. No rule over it, since the
409
413
  band's hairline is under the whole of it, and no column, since this is not the rail.
410
414
  why: docs/components.md#the-page-shell */
411
415
  .ui-app__user--bar { flex: none; padding-top: 0; border-top: 0; }
416
+ /* The switch is a 34px square and stays one: the trigger above is the thing on this
417
+ band that gives way, and a flex item with the default shrink does not wait its
418
+ turn. Measured at 390 with a control beside the trigger, the React band's switch
419
+ drew 19x34 and the strategy topbar's drew 28.47x34 — a square glyph plate squashed,
420
+ and a tap zone the sheet could only clamp to 29 or 39.
421
+ Pinning the strategy topbar's half of this rule does not make its row narrower: it
422
+ moves a 5.53px shortfall onto the account block's trailing gutter instead, because
423
+ nothing before it in that row has width to spare. topbar.css's --space-2 comment
424
+ names that choice and the gutter it leaves.
425
+ why: docs/components.md#the-page-shell */
426
+ .ui-app__bar .toggle, .topbar__in .toggle { flex: none; }
412
427
  .ui-app__user--bar .ui-app__av { margin-inline-start: 0; }
413
428
  /* Crumb, then the title block. At --space-2 the crumb sat 8px off the h1 and
414
429
  the h1 8px off its subtitle, so all three ranks read as one. */
@@ -422,7 +437,10 @@
422
437
  letter-spacing: var(--tracking-tight);
423
438
  margin-bottom: 8px;
424
439
  }
425
- .ui-app__sub { color: var(--text); font-size: calc(var(--text-base) + 0.5px); line-height: 1.6; max-width: var(--prose-body); margin-bottom: 32px; }
440
+ /* The head keeps the kit's second step below it whether or not a lede is there to
441
+ carry it: with no lede the title's own margin takes the step instead. */
442
+ .ui-app__main h1:not(.ui-card h1):not(:has(+ .ui-app__sub)) { margin-bottom: var(--space-8); }
443
+ .ui-app__sub { color: var(--text); font-size: calc(var(--text-base) + 0.5px); line-height: 1.6; max-width: var(--prose-body); margin-bottom: var(--space-8); }
426
444
  .ui-app__body { display: flex; flex-direction: column; gap: var(--space-5); }
427
445
  /* A flex child's min-width is its content by default, so one wide block — a
428
446
  ledger, a code sample — pushes the whole page sideways instead of scrolling
@@ -515,6 +533,31 @@
515
533
  .ui-app__main .ui-card__row { flex-wrap: wrap; gap: var(--space-3); }
516
534
  }
517
535
 
536
+ /* -- The band's search at one column --------------------------------------- */
537
+ /* The band holds the trigger, the reader's mark and whatever the page puts between
538
+ them, so the sentence broke mid-word — "Search or run a co…". The short word takes
539
+ its place. Neither the sentence nor the cap is `display: none`: both are inside
540
+ the button's accessible name — the cap by Artur's call on #308 — so they are
541
+ clipped the way `.ui-sr` in loading.css clips, and the name holds at every width.
542
+ why: docs/components.md#the-page-shell */
543
+ @media (max-width: 560px) {
544
+ .ui-app__search-txt,
545
+ .ui-app__search kbd {
546
+ position: absolute;
547
+ width: 1px;
548
+ height: 1px;
549
+ margin: -1px;
550
+ padding: 0;
551
+ overflow: hidden;
552
+ clip-path: inset(50%);
553
+ white-space: nowrap;
554
+ border: 0;
555
+ }
556
+ /* It gives way before the band does: at a large font scale the word ellipsises
557
+ rather than pushing the reader's mark off the row. */
558
+ .ui-app__search-short { display: block; flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
559
+ }
560
+
518
561
  /* -- Auth card (sign-in / consent) — centered, glowing --------------------- */
519
562
  .ui-auth {
520
563
  min-height: 100vh;
@@ -204,7 +204,35 @@
204
204
  .ui-table--compact th { padding: var(--space-2) var(--space-3); }
205
205
  .ui-table--compact th:last-child,
206
206
  .ui-table--compact td:last-child { padding-right: var(--space-3); }
207
- .ui-table-scroll { overflow: auto; max-height: var(--ui-table-height, 70vh); padding: 0 var(--space-1); }
207
+ /* Columns past the box's edge, said in paint. Two shades, one per side, under two
208
+ cover layers that ride the content: `local` moves a cover with the scroll, so
209
+ reaching an end slides that cover over its shade. A cover is solid for its first
210
+ half, so at 24px it hides a 12px shade outright — which is why the two sizes are
211
+ not one. why: guidelines/dense-tables.md */
212
+ .ui-table-scroll {
213
+ overflow: auto; max-height: var(--ui-table-height, 70vh); padding: 0 var(--space-1);
214
+ background-color: var(--table-bg);
215
+ background-image:
216
+ linear-gradient(to right, var(--table-bg) 50%, transparent),
217
+ linear-gradient(to left, var(--table-bg) 50%, transparent),
218
+ radial-gradient(farthest-side at 0 50%, var(--table-edge), transparent),
219
+ radial-gradient(farthest-side at 100% 50%, var(--table-edge), transparent);
220
+ background-position: 0 0, 100% 0, 0 0, 100% 0;
221
+ background-size: var(--space-6) 100%, var(--space-6) 100%, var(--space-3) 100%, var(--space-3) 100%;
222
+ background-repeat: no-repeat;
223
+ background-attachment: local, local, scroll, scroll;
224
+ }
225
+ /* The edge darkens on a light ground and lightens on a dark one: ink has nowhere to
226
+ go on a near-black page, which is why --elev-drop splits its spend and a raised
227
+ dark surface takes a light --elev-edge. */
228
+ .ui-table-scroll { --table-edge: color-mix(in srgb, var(--sheen) 12%, transparent); }
229
+ :root[data-theme="light"] .ui-table-scroll { --table-edge: color-mix(in srgb, var(--shadow-ink) 13%, transparent); }
230
+ /* The surface belongs to the box that scrolls, so the shades land under the cells
231
+ rather than behind them; the row fills repeat that same surface and step aside.
232
+ The sticky header and pinned column keep their paint: they hide what scrolls
233
+ beneath them. */
234
+ .ui-table-scroll > .ui-table,
235
+ .ui-table-scroll > .ui-table tbody td:not(.ui-table__identity, .ui-table__selection) { background-color: transparent; }
208
236
  /* The kit's first ringed scroller, and a scroll region inside a card like the rest, so
209
237
  it draws the band inward too rather than spending a halo across the card around it.
210
238
  It had no room of its own to draw in — the padding above is horizontal — which is the
@@ -216,6 +244,45 @@
216
244
  .ui-table--sticky thead th { position: sticky; top: 0; z-index: var(--z-sticky); background: var(--table-bg); padding-top: var(--space-2); }
217
245
  .ui-table--pinned .ui-table__identity, .ui-table--pinned .ui-table__selection { position: sticky; left: 0; z-index: var(--z-base); background: var(--table-bg); border-right: 1px solid var(--border); }
218
246
  .ui-table--pinned thead .ui-table__identity, .ui-table--pinned thead .ui-table__selection { z-index: calc(var(--z-sticky) + 1); }
247
+ /* The divider is the pin's edge: it says the column stayed while the rest moved under it.
248
+ Columns that all fit never move, so the line divides nothing and reads as an arbitrary
249
+ rule. The resting rule keeps it, so an engine that cannot answer the query is unchanged;
250
+ scroll-state makes a query container, not a size one. why: guidelines/dense-tables.md */
251
+ .ui-table-scroll { container-type: scroll-state; }
252
+ /* A shared grid cell puts the edge shade over the opaque sticky header, on both
253
+ sides: the radial-gradient shades below answer only the table's own background
254
+ and fall to nothing where the sticky header paints over them, so an engine that
255
+ can ask whether the scrollport still has room sticks a shade of its own above
256
+ it instead and the base radial pair is sized to 0 here. #434 review round 2 */
257
+ @supports (container-type: scroll-state) {
258
+ .ui-table-scroll {
259
+ display: grid;
260
+ grid-template-columns: minmax(0, 1fr);
261
+ background-size: var(--space-6) 100%, var(--space-6) 100%, 0 100%, 0 100%;
262
+ }
263
+ .ui-table-scroll > .ui-table { grid-area: 1 / 1; }
264
+ .ui-table-scroll::before,
265
+ .ui-table-scroll::after {
266
+ content: '';
267
+ grid-area: 1 / 1;
268
+ position: sticky;
269
+ top: 0;
270
+ width: var(--space-3);
271
+ z-index: calc(var(--z-sticky) + 2);
272
+ pointer-events: none;
273
+ }
274
+ .ui-table-scroll::before { left: 0; right: calc(100% - var(--space-3)); justify-self: start; }
275
+ .ui-table-scroll::after { left: calc(100% - var(--space-3)); right: 0; justify-self: end; }
276
+ @container scroll-state(scrollable: left) {
277
+ .ui-table-scroll::before { background: linear-gradient(to right, var(--table-edge), transparent); }
278
+ }
279
+ @container scroll-state(scrollable: right) {
280
+ .ui-table-scroll::after { background: linear-gradient(to left, var(--table-edge), transparent); }
281
+ }
282
+ }
283
+ @container not scroll-state(scrollable: x) {
284
+ .ui-table--pinned :is(.ui-table__identity, .ui-table__selection) { border-right-color: transparent; }
285
+ }
219
286
  .ui-value, .ui-delta { white-space: nowrap; font-variant-numeric: tabular-nums; }
220
287
  .ui-value__unit { margin-left: var(--space-1); font-size: var(--text-xs); color: var(--text); }
221
288
  .ui-delta--success { color: var(--chip-success-ink); }
@@ -56,6 +56,8 @@
56
56
  .ui-drawer__close,
57
57
  .ui-snippet__copy,
58
58
  .ui-footer__social,
59
+ .ui-app__search,
60
+ .toggle,
59
61
  /* Already relative in react/src/DatePicker.css — restated because this
60
62
  gate reads the kit's own sheets and cannot see that one, and a carrier
61
63
  whose containing block it cannot find is a layer that measures against
@@ -75,7 +77,9 @@
75
77
  .ui-snippet__copy,
76
78
  .ui-footer__social,
77
79
  .ui-switch,
78
- .ui-datepicker__opt
80
+ .ui-datepicker__opt,
81
+ .ui-app__search,
82
+ .toggle
79
83
  )::after {
80
84
  content: "";
81
85
  position: absolute;
@@ -215,4 +219,24 @@
215
219
  --tap-clear-x: var(--tap-gap);
216
220
  --tap-clear-y: var(--tap-gap);
217
221
  }
222
+
223
+ /* Two 52px rows, each holding a control the kit draws under the floor: the
224
+ shell band's palette trigger, 37.05 tall once the phone band swaps its
225
+ sentence for the short word, and the theme switch at 34 square on both.
226
+ Neither row opens anything — the room is already in it, which is why these
227
+ two declare a clearance and no gap. Across, 6px a side against the band's
228
+ own 12px gap and the topbar's 8 below 560px, where this layer exists — not
229
+ the topbar's 14 above that step, where it does not. 6px a side crosses the
230
+ topbar's 4px midline by 1px, which does not mis-tap: neither neighbour on
231
+ that row carries a layer of its own. Down, the row's leftover height is 7.4px
232
+ above and below the trigger and 9 around the switch, where a 44px layer
233
+ needs 3.5 and 5. The rows keep their height, their gaps and their paint.
234
+ #486
235
+
236
+ --space-3 rather than --tap-gap, which is what every row above declares:
237
+ 20 claims 10px a side and the trigger has 7.4. The clamp would still stop
238
+ at 44 today and be a number that is not true, which is what the browser
239
+ half fails a container for. */
240
+ .ui-app__bar,
241
+ .topbar__in { --tap-clear-x: var(--space-3, 12px); --tap-clear-y: var(--space-3, 12px); }
218
242
  }
@@ -246,12 +246,31 @@
246
246
  /* One column, and the band is at its tightest. The row is already full at 390,
247
247
  and at the gap above this one the account avatar stood from 365 to 397 and was
248
248
  cut at the screen edge — the one control on this screen that opens the account
249
- area. Packed to --space-2 the band needs 352 of the 362 it has there, so the
250
- avatar is whole with the band's own gutter beside it. 320 is still short of the
251
- room these controls want, which stays #558.
249
+ area. Packed to --space-2 and a flat 14px trailing gutter the band needs 367.53
250
+ of the 362px it has there, a 5.53px shortfall, because layout.css now pins the
251
+ theme switch to a full 34px square so it keeps the tap floor it was given.
252
+ The wordmark, the Deck/Text switch and the version label are each already at
253
+ the width their own text needs, and the switch must not shrink again, so the
254
+ account block's trailing gutter is what gives — but a FLAT 14px gutter has no
255
+ fallback below the width it was sized for: at 375 it has already given every
256
+ one of its 14px and the row is still 6.53px short, so the avatar was cut and
257
+ the page gained 7px of horizontal scroll (#486 review round 2), worse again
258
+ at 320.
259
+ Both carry a share now, continuously, rather than the gutter alone holding a
260
+ fixed budget: the gutter still narrows first, from 14px at 390 down to 0 by
261
+ 381.53px of available row width, and once it is spent the inter-control gap
262
+ takes the rest, from --space-2 down to its own floor of --space-1. 381.53px is
263
+ this row's own fixed width minus its gutter's full 14px — not a general
264
+ formula, a number that belongs to this composition's own wordmark, switch and
265
+ version label. The avatar is whole and the page carries no horizontal scroll
266
+ down to 375px. 320 still falls 41.53px short with both all the way down,
267
+ which stays #558; it is 5px better than main's own 46.53px there, not solved.
252
268
  why: docs/foundations.md#breakpoints */
253
269
  @media (max-width: 560px) {
254
- .topbar__in { gap: var(--space-2); }
270
+ .topbar__in {
271
+ gap: clamp(var(--space-1), calc(var(--space-2) - (381.53px - 100vw) / 5), var(--space-2));
272
+ padding-right: clamp(0px, calc(100vw - 381.53px), 14px);
273
+ }
255
274
  }
256
275
 
257
276
  /* Account — avatar + dropdown. Gated: hidden until the app confirms a session
@@ -534,6 +534,7 @@
534
534
  .ui-snippet--reveal,
535
535
  .ui-sx,
536
536
  .ui-table,
537
+ .ui-table-scroll,
537
538
  .ui-table--sticky thead th,
538
539
  .ui-table--pinned .ui-table__identity, .ui-table--pinned .ui-table__selection,
539
540
  .ui-table--hover:not(.ui-table--zebra) tbody tr:hover td,
@@ -557,6 +558,7 @@
557
558
  * toward the page and leaves the page 1.02 away, while the card it is a variant of stays
558
559
  * 1.07-1.14 off in both themes. Measured in stories/code-chip.test.js. */
559
560
  .ui-table,
561
+ .ui-table-scroll,
560
562
  .ui-table--sticky thead th,
561
563
  .ui-table--pinned .ui-table__identity, .ui-table--pinned .ui-table__selection,
562
564
  .ui-table--hover:not(.ui-table--zebra) tbody tr:hover td,