@apliteni/apliteni-ui 0.70.0 → 0.72.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.
@@ -14,6 +14,34 @@
14
14
 
15
15
  **Except:** WCAG 2.5.8 permits spacing, equivalent controls, inline text, user-agent sizes and essential presentation. Overlays must not reach neighbours.
16
16
 
17
+ ## Tap target on a phone
18
+
19
+ <!-- rule: tap-zone -->
20
+
21
+ **Rule:** Below the phone step, reach 44x44 with a transparent layer outside the control, never by growing the control.
22
+
23
+ **Why:** A finger covers more than a cursor, and a control that grows on a phone moves the page a reader already knows.
24
+
25
+ **Do:** Centre a transparent layer on the control and let it stop at half the gap to the nearest neighbour.
26
+
27
+ **Don't:** Grow the drawn control to 44 and push out every row that holds one.
28
+
29
+ **Except:** Only a coarse pointer gets the layer, or a cursor lights a control it is not over. Rows that share an edge — a menu, a dense table, a settings list — have nothing to grow into and stay as they are drawn. Fields generate no layer, so a field reaches the floor by its own height or not at all.
30
+
31
+ ## Space between small targets
32
+
33
+ <!-- rule: tap-spacing -->
34
+
35
+ **Rule:** Below the phone step, open a row of small controls to 44 minus their drawn size, so both tap zones fit between the drawn edges.
36
+
37
+ **Why:** A zone reaches half the gap before it crosses its neighbour's edge, so the gap is what decides whether either control reaches the floor.
38
+
39
+ **Do:** Open a row of 24px marks to 20px on a phone, where both zones fit and neither mark changes size.
40
+
41
+ **Don't:** Keep the desktop 8px and let the control written later in the markup win the overlap.
42
+
43
+ **Except:** A row that is already full opens downward only — widening it across makes the controls narrower, which is the one thing the floor must not do. Rows that share an edge — a menu, a dense table, a settings list — have nothing to open and keep their drawn targets. A row that opens nothing grows no zone at all.
44
+
17
45
  ## Focus ring contrast
18
46
 
19
47
  <!-- rule: ring-contrast -->
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apliteni/apliteni-ui",
3
- "version": "0.70.0",
3
+ "version": "0.72.0",
4
4
  "workspaces": [
5
5
  "react"
6
6
  ],
@@ -26,6 +26,89 @@
26
26
  }
27
27
  }
28
28
 
29
+ /* ../src/styles/tap-zone.css */
30
+ :root {
31
+ --tap-min: 44px;
32
+ --tap-aa: 24px;
33
+ --tap-gap: var(--space-5, 20px);
34
+ --tap-clear-x: 0px;
35
+ --tap-clear-y: 0px;
36
+ }
37
+ @media (max-width: 560px) and (pointer: coarse) {
38
+ :where(.ui-seg button, .ui-tabs__tab, .ui-dropdown__trigger, .ui-toast__action, .ui-drawer__close, .ui-snippet__copy, .ui-footer__social) {
39
+ position: relative;
40
+ }
41
+ :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)::after {
42
+ content: "";
43
+ position: absolute;
44
+ left: 50%;
45
+ top: 50%;
46
+ transform: translate(-50%, -50%);
47
+ width: min(max(100%, var(--tap-min)), calc(100% + var(--tap-clear-x)));
48
+ height: min(max(100%, var(--tap-min)), calc(100% + var(--tap-clear-y)));
49
+ }
50
+ .ui-check input::before,
51
+ .ui-toast__close::before {
52
+ width: max(var(--tap-aa), min(max(100%, var(--tap-min)), calc(100% + var(--tap-clear-x))));
53
+ height: max(var(--tap-aa), min(max(100%, var(--tap-min)), calc(100% + var(--tap-clear-y))));
54
+ }
55
+ .ui-seg {
56
+ row-gap: var(--tap-gap) !important;
57
+ --tap-clear-x: var(--space-1, 4px);
58
+ --tap-clear-y: var(--tap-gap);
59
+ }
60
+ .ui-tabs__list {
61
+ --tap-clear-x: 2px;
62
+ --tap-clear-y: var(--tap-gap);
63
+ }
64
+ .ui-pager__steps {
65
+ row-gap: var(--tap-gap) !important;
66
+ --tap-clear-x: var(--space-1, 4px);
67
+ --tap-clear-y: var(--tap-gap);
68
+ }
69
+ .ui-pager {
70
+ row-gap: var(--tap-gap) !important;
71
+ --tap-clear-y: var(--tap-gap);
72
+ }
73
+ .ui-filter-bar {
74
+ gap: var(--tap-gap) !important;
75
+ --tap-clear-x: var(--tap-gap);
76
+ --tap-clear-y: var(--tap-gap);
77
+ }
78
+ .ui-filter-bar__chip {
79
+ --tap-clear-x: 0px;
80
+ --tap-clear-y: 0px;
81
+ }
82
+ .ui-table__act .ui-btn + .ui-btn {
83
+ margin-inline-start: var(--tap-gap) !important;
84
+ }
85
+ .ui-table__act {
86
+ --tap-clear-x: var(--tap-gap);
87
+ --tap-clear-y: 0px;
88
+ }
89
+ .ui-toast {
90
+ row-gap: var(--tap-gap) !important;
91
+ --tap-clear-y: var(--tap-gap);
92
+ }
93
+ .ui-confirm__acts,
94
+ .ui-toolbar,
95
+ .ui-empty__actions,
96
+ .ui-sx__actions,
97
+ .ui-fbc__foot,
98
+ .ui-drawer__footer,
99
+ .ui-snippet__bar,
100
+ .ui-app__main .ui-card__row {
101
+ row-gap: var(--tap-gap) !important;
102
+ --tap-clear-y: var(--tap-gap);
103
+ }
104
+ .ui-footer__socials,
105
+ .ui-drawer__header {
106
+ gap: var(--tap-gap) !important;
107
+ --tap-clear-x: var(--tap-gap);
108
+ --tap-clear-y: var(--tap-gap);
109
+ }
110
+ }
111
+
29
112
  /* ../src/styles/input.css */
30
113
  .ui-field {
31
114
  display: block;
@@ -298,8 +381,8 @@
298
381
  max-width: var(--prose-caption);
299
382
  padding: var(--space-2) var(--space-3);
300
383
  background: var(--ui-tip-bg);
301
- border: 1px solid var(--border-strong);
302
- box-shadow: inset 0 0 0 1px var(--elev-edge, var(--border)), var(--elev-drop);
384
+ border: 1px solid var(--float-edge);
385
+ box-shadow: inset 0 0 0 1px var(--elev-edge, var(--float-edge-inner)), var(--elev-drop);
303
386
  border-radius: var(--radius-sm);
304
387
  font-family: var(--font-sans);
305
388
  font-size: var(--text-xs);
package/src/index.css CHANGED
@@ -44,3 +44,6 @@
44
44
  @import "./styles/feedback.css";
45
45
  @import "./styles/success.css";
46
46
  @import "./styles/loading.css";
47
+ /* Last, and that is the point: the tap zone opens a gap a component sheet set,
48
+ so it has to be read after every one of them. why: specification, the tap floor */
49
+ @import "./styles/tap-zone.css";
package/src/inline.js CHANGED
@@ -30,6 +30,7 @@ export const styles = {
30
30
  base: baseCss,
31
31
  reducedMotion: read('styles/reduced-motion.css'),
32
32
  fieldZoom: read('styles/field-zoom.css'),
33
+ tapZone: read('styles/tap-zone.css'),
33
34
  motion: read('styles/motion.css'),
34
35
  button: read('styles/button.css'),
35
36
  card: read('styles/card.css'),
@@ -92,4 +93,7 @@ export const cssText = [
92
93
  styles.feedback,
93
94
  styles.success,
94
95
  styles.loading,
96
+ // Last in the list for the reason src/index.css imports it last: it opens a
97
+ // gap a component sheet set, so it is read after every one of them.
98
+ styles.tapZone,
95
99
  ].join('\n');
@@ -46,8 +46,8 @@
46
46
  max-width: var(--panel-md);
47
47
  overflow: hidden; /* clips the timer bar to the rounded corners */
48
48
  background: var(--surface); --ring-gap: var(--surface);
49
- border: 1px solid var(--border);
50
- box-shadow: inset 0 0 0 1px var(--elev-edge, var(--border)), var(--elev-drop);
49
+ border: 1px solid var(--float-edge);
50
+ box-shadow: inset 0 0 0 1px var(--elev-edge, var(--float-edge-inner)), var(--elev-drop);
51
51
  animation: ui-toast-in var(--dur-med) var(--ease-out) both;
52
52
  }
53
53
 
@@ -1,13 +1,16 @@
1
1
  /* ============================================================================
2
- * Card — .ui-card (a filled surface one step off the page, edged by a hairline)
2
+ * Card — .ui-card (a filled surface one step off the page: edged in dark, dropped in light)
3
3
  * Rows: .ui-card__row / headers: .ui-card__title / .ui-card__sub
4
4
  * ========================================================================== */
5
5
 
6
- /* Flat since #284 and edged in both themes since #295: the surface is the step, the
7
- line is what makes it legible. why: docs/specification.md#elevation */
6
+ /* The surface is the step; what makes it legible depends on the theme. Dark edges the
7
+ card with the hairline (--card-edge). Light draws no line at all and casts --elev-rest
8
+ instead — a 22px penumbra off a 10px offset, the width Artur's reference uses on a
9
+ card. Chosen by him on #490 round t3. why: docs/specification.md#elevation */
8
10
  .ui-card {
9
11
  background: var(--surface); --ring-gap: var(--surface);
10
- border: 1px solid var(--border);
12
+ border: 1px solid var(--card-edge);
13
+ box-shadow: var(--elev-rest);
11
14
  border-radius: var(--radius-xl);
12
15
  /* Was `var(--space-6) 26px`; 26 is off the scale and broke the column's text edge.
13
16
  why: docs/specification.md#spacing-and-rhythm */
@@ -50,13 +53,14 @@
50
53
  cursor: pointer;
51
54
  /* Written as a <button>, the card would take the browser's frame, centred text and
52
55
  `font: 400 13.3333px Arial` — a shorthand, so `font: inherit` and not the family
53
- longhand. .ui-card states the background and, since #295, the border, so a
54
- `border: 0` here would only delete the hairline. #251 */
56
+ longhand. .ui-card states the background, the border and its rung, so a
57
+ `border: 0` here would only delete the edge dark draws. #251 */
55
58
  text-align: left;
56
59
  font: inherit;
57
60
  transition: transform var(--dur-fast) var(--ease);
58
61
  }
59
- /* The lift alone: the kit casts no shadow anywhere (#295), and cards are flat (#284). */
62
+ /* The lift alone: the card's own rung is what says it is raised, and hover is not a
63
+ change of level. why: docs/specification.md#elevation */
60
64
  .ui-card--interactive:hover { transform: translateY(-2px); }
61
65
  .ui-card--interactive:focus-visible { outline: 2px solid transparent; box-shadow: var(--ring); }
62
66
 
@@ -69,10 +69,12 @@
69
69
  flex-direction: column;
70
70
  font-family: var(--font-sans);
71
71
  background: var(--cmdk-surface); --ring-gap: var(--cmdk-surface);
72
- /* The two-step edge and the drop: --border-strong outside, --border one pixel
73
- inside it, then the broad faint drop. why: docs/specification.md#elevation */
74
- border: 1px solid var(--border-strong);
75
- box-shadow: inset 0 0 0 1px var(--elev-edge, var(--border)), var(--elev-drop);
72
+ /* The two-step edge and the drop, through the level's own names: in DARK that is
73
+ --border-strong outside and --border one pixel inside it, then the drop; in LIGHT
74
+ both lines are transparent and the drop carries the whole separation.
75
+ why: docs/specification.md#elevation */
76
+ border: 1px solid var(--float-edge);
77
+ box-shadow: inset 0 0 0 1px var(--elev-edge, var(--float-edge-inner)), var(--elev-drop);
76
78
  border-radius: var(--radius-lg);
77
79
  overflow: hidden;
78
80
  opacity: 0;
@@ -86,7 +88,7 @@
86
88
  box-shadow list replaces the whole list, so `box-shadow: var(--ring)` alone
87
89
  took the panel's edge and its drop off for as long as it held focus.
88
90
  why: docs/specification.md#elevation */
89
- .ui-cmdk__panel:focus-visible { outline: 2px solid transparent; box-shadow: var(--ring), inset 0 0 0 1px var(--elev-edge, var(--border)), var(--elev-drop); }
91
+ .ui-cmdk__panel:focus-visible { outline: 2px solid transparent; box-shadow: var(--ring), inset 0 0 0 1px var(--elev-edge, var(--float-edge-inner)), var(--elev-drop); }
90
92
 
91
93
  /* -- The text box --------------------------------------------------------- */
92
94
 
@@ -62,10 +62,12 @@
62
62
  gap: var(--space-2);
63
63
  padding: var(--space-5);
64
64
  background: var(--confirm-surface); --ring-gap: var(--confirm-surface);
65
- /* The two-step edge and the drop: --border-strong outside, --border one pixel
66
- inside it, then the broad faint drop. why: docs/specification.md#elevation */
67
- border: 1px solid var(--border-strong);
68
- box-shadow: inset 0 0 0 1px var(--elev-edge, var(--border)), var(--elev-drop);
65
+ /* The two-step edge and the drop, through the level's own names: in DARK that is
66
+ --border-strong outside and --border one pixel inside it, then the drop; in LIGHT
67
+ both lines are transparent and the drop carries the whole separation.
68
+ why: docs/specification.md#elevation */
69
+ border: 1px solid var(--float-edge);
70
+ box-shadow: inset 0 0 0 1px var(--elev-edge, var(--float-edge-inner)), var(--elev-drop);
69
71
  border-radius: var(--radius-lg);
70
72
  opacity: 0;
71
73
  transform: translateY(8px);
@@ -78,7 +80,7 @@
78
80
  box-shadow list replaces the whole list, so `box-shadow: var(--ring)` alone
79
81
  took the panel's edge and its drop off for as long as it held focus.
80
82
  why: docs/specification.md#elevation */
81
- .ui-confirm__panel:focus-visible { outline: 2px solid transparent; box-shadow: var(--ring), inset 0 0 0 1px var(--elev-edge, var(--border)), var(--elev-drop); }
83
+ .ui-confirm__panel:focus-visible { outline: 2px solid transparent; box-shadow: var(--ring), inset 0 0 0 1px var(--elev-edge, var(--float-edge-inner)), var(--elev-drop); }
82
84
 
83
85
  /* -- Slots ----------------------------------------------------------------- */
84
86
  /* The question and the answers keep their size; the consequence between them is
@@ -82,14 +82,14 @@
82
82
  }
83
83
  .ui-drawer--right .ui-drawer__panel {
84
84
  right: 0;
85
- border-left: 1px solid var(--border-strong);
86
- --drawer-line: inset 1px 0 0 var(--border);
85
+ border-left: 1px solid var(--float-edge);
86
+ --drawer-line: inset 1px 0 0 var(--float-edge-inner);
87
87
  transform: translateX(100%);
88
88
  }
89
89
  .ui-drawer--left .ui-drawer__panel {
90
90
  left: 0;
91
- border-right: 1px solid var(--border-strong);
92
- --drawer-line: inset -1px 0 0 var(--border);
91
+ border-right: 1px solid var(--float-edge);
92
+ --drawer-line: inset -1px 0 0 var(--float-edge-inner);
93
93
  transform: translateX(-100%);
94
94
  }
95
95
  .ui-drawer--top .ui-drawer__panel,
@@ -100,14 +100,14 @@
100
100
  }
101
101
  .ui-drawer--top .ui-drawer__panel {
102
102
  top: 0;
103
- border-bottom: 1px solid var(--border-strong);
104
- --drawer-line: inset 0 -1px 0 var(--border);
103
+ border-bottom: 1px solid var(--float-edge);
104
+ --drawer-line: inset 0 -1px 0 var(--float-edge-inner);
105
105
  transform: translateY(-100%);
106
106
  }
107
107
  .ui-drawer--bottom .ui-drawer__panel {
108
108
  bottom: 0;
109
- border-top: 1px solid var(--border-strong);
110
- --drawer-line: inset 0 1px 0 var(--border);
109
+ border-top: 1px solid var(--float-edge);
110
+ --drawer-line: inset 0 1px 0 var(--float-edge-inner);
111
111
  transform: translateY(100%);
112
112
  }
113
113
 
@@ -71,10 +71,12 @@
71
71
  /* The floating step, not the sunken one — a panel held open over a card cannot be
72
72
  painted the colour of an inset. why: docs/specification.md#elevation */
73
73
  background: var(--bg-elevated); --ring-gap: var(--bg-elevated);
74
- /* The two-step edge and the drop: --border-strong outside, --border one pixel
75
- inside it, then the broad faint drop. why: docs/specification.md#elevation */
76
- border: 1px solid var(--border-strong);
77
- box-shadow: inset 0 0 0 1px var(--elev-edge, var(--border)), var(--elev-drop);
74
+ /* The two-step edge and the drop, through the level's own names: in DARK that is
75
+ --border-strong outside and --border one pixel inside it, then the drop; in LIGHT
76
+ both lines are transparent and the drop carries the whole separation.
77
+ why: docs/specification.md#elevation */
78
+ border: 1px solid var(--float-edge);
79
+ box-shadow: inset 0 0 0 1px var(--elev-edge, var(--float-edge-inner)), var(--elev-drop);
78
80
  border-radius: var(--radius-lg);
79
81
  padding: var(--ui-dropdown-pad);
80
82
  opacity: 0;
@@ -102,7 +104,7 @@
102
104
  a panel that takes focus any other way has the same claim on the ring. The
103
105
  ring is COMPOSED with the floating treatment rather than over it: a box-shadow
104
106
  list replaces the whole list. why: docs/specification.md#elevation */
105
- .ui-dropdown__panel:focus-visible { outline: 2px solid transparent; box-shadow: var(--ring), inset 0 0 0 1px var(--elev-edge, var(--border)), var(--elev-drop); }
107
+ .ui-dropdown__panel:focus-visible { outline: 2px solid transparent; box-shadow: var(--ring), inset 0 0 0 1px var(--elev-edge, var(--float-edge-inner)), var(--elev-drop); }
106
108
 
107
109
  /* Opening upward. The kit releases its own `top` here so that nothing outside
108
110
  the kit has to: an absolute box with both edges pinned is stretched between
@@ -67,8 +67,12 @@
67
67
  .ui-fbcomposer {
68
68
  position: fixed; z-index: 71; left: 50%; bottom: 26px; transform: translateX(-50%) translateY(14px);
69
69
  width: min(520px, calc(100vw - 32px)); background: var(--bg-elevated); --ring-gap: var(--bg-elevated); border-radius: 18px;
70
- /* The shadow was its only edge until #295; now it takes the hairline. */
71
- border: 1px solid var(--border); opacity: 0; pointer-events: none;
70
+ /* The shadow was its only edge until #295, which replaced it with the hairline; since
71
+ #490 light is back on the drop and dark keeps the line.
72
+ why: docs/specification.md#elevation */
73
+ border: 1px solid var(--float-edge);
74
+ box-shadow: inset 0 0 0 1px var(--elev-edge, var(--float-edge-inner)), var(--elev-drop);
75
+ opacity: 0; pointer-events: none;
72
76
  transition: opacity var(--dur-med) var(--ease), transform var(--dur-med) var(--ease); overflow: hidden;
73
77
  }
74
78
  .ui-fbcomposer.show { opacity: 1; pointer-events: auto; transform: translateX(-50%) translateY(0); }
@@ -28,7 +28,9 @@
28
28
  .ui-app-page .topbar { position: sticky; top: 0; z-index: var(--z-sticky); }
29
29
 
30
30
  /* The rail uses the card surface so its labels stay on a reading ground.
31
- Its border separates navigation from the page in both themes.
31
+ Dark keeps its edge (--rail-edge); light casts --elev-rail, sideways, because the rail
32
+ is flush to a screen edge. Reopens the light half of #454.
33
+ why: docs/specification.md#elevation
32
34
  overflow-y, because the box is pinned at the viewport's height for the whole
33
35
  scroll of .ui-app — without it a row past that height paints below the fold
34
36
  and no amount of document scrolling brings it back. The rail's own padding is
@@ -38,7 +40,8 @@
38
40
  flex-direction: column;
39
41
  gap: var(--space-5);
40
42
  padding: var(--space-5) var(--space-4);
41
- border-right: var(--ui-rail-rule) solid var(--border);
43
+ border-right: var(--ui-rail-rule) solid var(--rail-edge);
44
+ box-shadow: var(--elev-rail);
42
45
  background: var(--surface); --ring-gap: var(--surface);
43
46
  position: sticky;
44
47
  top: var(--ui-app-top, 0px);
@@ -296,7 +299,7 @@
296
299
  why: docs/specification.md#elevation */
297
300
  :where(.ui-app.is-collapsed) .ui-app__rail .ui-nav__item:is(:hover, :focus-visible) > .ui-nav__label,
298
301
  :where(.ui-app.is-collapsed) .ui-app__rail .ui-app__user-trigger:is(:hover, :focus-visible) > .ui-app__who,
299
- .ui-app__rail .ui-app__fold:is(:hover, :focus-visible) > .ui-nav__label { position: fixed; left: calc(var(--ui-rail-w) + var(--space-2)); z-index: var(--z-dropdown); width: max-content; max-width: 15rem; margin-block: -7px; padding: 6px 10px; border: 1px solid var(--border-strong); box-shadow: inset 0 0 0 1px var(--elev-edge, var(--border)), var(--elev-drop); border-radius: var(--radius-xs); background: var(--surface); /* ring-gap: inherit — decorative or text-only surface has no nested focus target. */ color: var(--strong); opacity: 1; pointer-events: none; }
302
+ .ui-app__rail .ui-app__fold:is(:hover, :focus-visible) > .ui-nav__label { position: fixed; left: calc(var(--ui-rail-w) + var(--space-2)); z-index: var(--z-dropdown); width: max-content; max-width: 15rem; margin-block: -7px; padding: 6px 10px; border: 1px solid var(--float-edge); box-shadow: inset 0 0 0 1px var(--elev-edge, var(--float-edge-inner)), var(--elev-drop); border-radius: var(--radius-xs); background: var(--surface); /* ring-gap: inherit — decorative or text-only surface has no nested focus target. */ color: var(--strong); opacity: 1; pointer-events: none; }
300
303
  @supports (anchor-name: --a) and (anchor-scope: --a) {
301
304
  .ui-app.is-collapsed,
302
305
  .ui-app:has(.ui-app__fold) { anchor-scope: --ui-rail; }
@@ -395,7 +398,7 @@
395
398
  .ui-app__search:focus-visible { outline: 2px solid transparent; box-shadow: var(--ring); border-color: var(--accent); }
396
399
  /* The same glyph the palette draws in its own search box, at the same box and the
397
400
  same stroke: the trigger and the thing it opens are not two search marks.
398
- src/styles/command-palette.css:107 `.ui-cmdk__search-ic svg` */
401
+ src/styles/command-palette.css:109 `.ui-cmdk__search-ic svg` */
399
402
  .ui-app__search-ic { display: flex; flex: none; }
400
403
  .ui-app__search-ic svg { width: 17px; height: 17px; stroke: currentColor; fill: none; stroke-width: 2.2; }
401
404
  /* Part of the button's name, so it is squeezed rather than dropped: an ellipsis
@@ -497,7 +500,7 @@
497
500
  .ui-app__rail .ui-nav__item,
498
501
  .ui-app__rail .ui-app__user-trigger { min-height: 44px; }
499
502
  .ui-app__rail .ui-nav__item:is(:hover, :focus-visible) > .ui-nav__label,
500
- .ui-app__rail .ui-app__user-trigger:is(:hover, :focus-visible) > .ui-app__who { position: fixed; left: calc(var(--ui-rail-w) + var(--space-2)); z-index: var(--z-dropdown); width: max-content; max-width: 15rem; margin-block: -7px; padding: 6px 10px; border: 1px solid var(--border-strong); box-shadow: inset 0 0 0 1px var(--elev-edge, var(--border)), var(--elev-drop); border-radius: var(--radius-xs); background: var(--surface); /* ring-gap: inherit — decorative or text-only surface has no nested focus target. */ color: var(--strong); opacity: 1; pointer-events: none; }
503
+ .ui-app__rail .ui-app__user-trigger:is(:hover, :focus-visible) > .ui-app__who { position: fixed; left: calc(var(--ui-rail-w) + var(--space-2)); z-index: var(--z-dropdown); width: max-content; max-width: 15rem; margin-block: -7px; padding: 6px 10px; border: 1px solid var(--float-edge); box-shadow: inset 0 0 0 1px var(--elev-edge, var(--float-edge-inner)), var(--elev-drop); border-radius: var(--radius-xs); background: var(--surface); /* ring-gap: inherit — decorative or text-only surface has no nested focus target. */ color: var(--strong); opacity: 1; pointer-events: none; }
501
504
  @supports (anchor-name: --a) and (anchor-scope: --a) {
502
505
  .ui-app { anchor-scope: --ui-rail; }
503
506
  .ui-app__rail { anchor-name: --ui-rail; }
@@ -529,10 +532,12 @@
529
532
  z-index: 1;
530
533
  width: 100%;
531
534
  max-width: var(--panel-md);
532
- /* Floats over a spotlit page, so it takes the floating step and the hairline.
535
+ /* Floats over a spotlit page, so it takes the floating step and the floating
536
+ treatment: the two-step edge in dark, the drop alone in light.
533
537
  why: docs/specification.md#elevation */
534
538
  background: var(--bg-elevated); --ring-gap: var(--bg-elevated);
535
- border: 1px solid var(--border);
539
+ border: 1px solid var(--float-edge);
540
+ box-shadow: inset 0 0 0 1px var(--elev-edge, var(--float-edge-inner)), var(--elev-drop);
536
541
  border-radius: 22px;
537
542
  padding: 38px 36px 32px;
538
543
  }
@@ -17,7 +17,10 @@
17
17
  overflow: hidden;
18
18
  border-radius: 20px;
19
19
  background: var(--bg-elevated); --ring-gap: var(--bg-elevated); /* Elevated surface for the confirmation and its actions. */
20
- border: 1px solid var(--border);
20
+ /* The floating treatment, like every other surface on that step.
21
+ why: docs/specification.md#elevation */
22
+ border: 1px solid var(--float-edge);
23
+ box-shadow: inset 0 0 0 1px var(--elev-edge, var(--float-edge-inner)), var(--elev-drop);
21
24
  isolation: isolate;
22
25
  }
23
26
 
@@ -0,0 +1,167 @@
1
+ /* apliteni-ui — the tap zone. Below the phone step, to a coarse pointer, a
2
+ * control that draws smaller than --tap-min carries a transparent layer that
3
+ * reaches the floor, and no control is drawn any bigger. Zones alone do not
4
+ * finish it: a layer takes HALF the clear space its container declares, so
5
+ * where the kit packs small controls closer than that, the GAP opens to
6
+ * --tap-gap. The controls keep their size; the space between them grows.
7
+ *
8
+ * Read LAST, after every component sheet — see src/index.css — because each
9
+ * opening overrides a gap one of them already set.
10
+ *
11
+ * why: docs/specification.md#a-tap-reaches-the-floor-below-the-phone-step
12
+ * Held by stories/tap-zone.test.js, measured in a browser at 390.
13
+ */
14
+
15
+ :root {
16
+ /* WCAG 2.5.5, and where iOS and Android land. Not on the spacing scale,
17
+ which runs 40 then 48, because a fingertip is not spacing. Declared here
18
+ rather than in tokens.css for the reason field-zoom.css carries its own 16:
19
+ react/src/index.ts imports this sheet without the token file, and a var()
20
+ resolving to nothing there would size the layer to `auto` in silence. */
21
+ --tap-min: 44px;
22
+
23
+ /* WCAG 2.5.8 (AA), the target the kit holds at every width: the floor this
24
+ sheet raises FROM and may never drop below. */
25
+ --tap-aa: 24px;
26
+
27
+ /* 44 − 24: two zones fit between two of the smallest marks the kit draws. */
28
+ --tap-gap: var(--space-5, 20px);
29
+
30
+ /* Clear space to the nearest neighbour, declared by the container that knows
31
+ its own gap; a zone takes half on each side.
32
+
33
+ ZERO by default, so a zone grows nowhere until a container has opened the
34
+ room — assuming --tap-gap everywhere was measured handing `Keep the
35
+ workspace` a pixel of its own box that answered for `Delete it
36
+ permanently`. */
37
+ --tap-clear-x: 0px;
38
+ --tap-clear-y: 0px;
39
+ }
40
+
41
+ @media (max-width: 560px) and (pointer: coarse) {
42
+ /* The containing block for every layer below. `.ui-btn` already has it; the
43
+ rest take it only inside this query, because `position: relative` lifts a
44
+ control over the in-flow one beside it and that changes who wins a shared
45
+ pixel. */
46
+ :where(
47
+ .ui-seg button,
48
+ .ui-tabs__tab,
49
+ .ui-dropdown__trigger,
50
+ .ui-toast__action,
51
+ .ui-drawer__close,
52
+ .ui-snippet__copy,
53
+ .ui-footer__social
54
+ ) { position: relative; }
55
+
56
+ /* The layer: centred, transparent, sized to the floor, clamped to half the
57
+ clearance. */
58
+ :where(
59
+ .ui-btn,
60
+ .ui-seg button,
61
+ .ui-tabs__tab,
62
+ .ui-dropdown__trigger,
63
+ .ui-toast__action,
64
+ .ui-drawer__close,
65
+ .ui-snippet__copy,
66
+ .ui-footer__social,
67
+ .ui-switch
68
+ )::after {
69
+ content: "";
70
+ position: absolute;
71
+ left: 50%;
72
+ top: 50%;
73
+ transform: translate(-50%, -50%);
74
+ width: min(max(100%, var(--tap-min)), calc(100% + var(--tap-clear-x)));
75
+ height: min(max(100%, var(--tap-min)), calc(100% + var(--tap-clear-y)));
76
+ }
77
+
78
+ /* ::after on these is spoken for — the tick and the dot, the toast close's
79
+ glyph plate — so the existing ::before grows, floored at --tap-aa. The
80
+ floor is load-bearing: `100%` is the PADDING box, 16 on a 19px checkbox
81
+ with a 1.5px border, so without it the clamp takes this layer DOWN to 22
82
+ and under the kit's own minimum. */
83
+ .ui-check input::before,
84
+ .ui-toast__close::before {
85
+ width: max(var(--tap-aa), min(max(100%, var(--tap-min)), calc(100% + var(--tap-clear-x))));
86
+ height: max(var(--tap-aa), min(max(100%, var(--tap-min)), calc(100% + var(--tap-clear-y))));
87
+ }
88
+
89
+ /* -- Where the kit opens a gap, and where it cannot --------------------
90
+ * `!important` for the reason field-zoom.css takes it: each opening
91
+ * overrides a `gap` shorthand a component sheet set, and the React bundle
92
+ * ships this sheet without those sheets, where order is the consumer's. */
93
+
94
+ /* A strip keeps its pills packed across — that tight track IS the control —
95
+ and opens down, where the room is free until it wraps. */
96
+ .ui-seg {
97
+ row-gap: var(--tap-gap) !important;
98
+ --tap-clear-x: var(--space-1, 4px);
99
+ --tap-clear-y: var(--tap-gap);
100
+ }
101
+
102
+ /* Tabs are wide enough across; down, the panel under them has the room. */
103
+ .ui-tabs__list { --tap-clear-x: 2px; --tap-clear-y: var(--tap-gap); }
104
+
105
+ /* Equal cells read as one strip, so they stay packed across and open down,
106
+ which is where this strip wraps at this width. */
107
+ .ui-pager__steps {
108
+ row-gap: var(--tap-gap) !important;
109
+ --tap-clear-x: var(--space-1, 4px);
110
+ --tap-clear-y: var(--tap-gap);
111
+ }
112
+ .ui-pager { row-gap: var(--tap-gap) !important; --tap-clear-y: var(--tap-gap); }
113
+
114
+ /* Chips open both ways: the row wraps, and the button at the end of it is a
115
+ target like the chips are. */
116
+ .ui-filter-bar { gap: var(--tap-gap) !important; --tap-clear-x: var(--tap-gap); --tap-clear-y: var(--tap-gap); }
117
+
118
+ /* Inside a chip there is nothing to open: parting the value from its remove
119
+ mark would draw two controls where the reader sees one. Named on the
120
+ Accessibility minimums page. */
121
+ .ui-filter-bar__chip { --tap-clear-x: 0px; --tap-clear-y: 0px; }
122
+
123
+ /* Row actions open across — the pair #488 names — and cannot open down:
124
+ a dense table's rows share an edge, and parting them is the row height the
125
+ guideline protects. Down, they get the row's own padding, twice. */
126
+ /* Row actions — the pair #488 names — part ACROSS with a margin: the cell is
127
+ a `table-cell`, where `gap` does nothing, and declaring it anyway left the
128
+ mark's zone 9px inside `Revoke`. Nothing down, because a dense table's
129
+ rows share an edge and its padding is not clearance to spend. */
130
+ .ui-table__act .ui-btn + .ui-btn { margin-inline-start: var(--tap-gap) !important; }
131
+ .ui-table__act { --tap-clear-x: var(--tap-gap); --tap-clear-y: 0px; }
132
+
133
+ /* Down only, like the nine rows below: a toast is a full row at this width,
134
+ and opening it across took 24px out of the message for no reach at all —
135
+ its action already clears the floor on the vertical half. The box does not
136
+ open either: a toast clips to its own rounded corners for the timer bar,
137
+ and widening its padding grows the surface rather than the space between
138
+ controls, measured at four marks for 8px on every toast. */
139
+ .ui-toast { row-gap: var(--tap-gap) !important; --tap-clear-y: var(--tap-gap); }
140
+
141
+ /* The kit's remaining control rows open DOWN only. A row of labelled buttons
142
+ at 390 is often already full, so widening its column gap does not move the
143
+ buttons apart — it makes them narrower, and that is the half #488 rules
144
+ out. Measured on a toolbar of three buttons and a dropdown. These rows
145
+ wrap, which is also where the review measured them colliding. */
146
+ .ui-confirm__acts,
147
+ .ui-toolbar,
148
+ .ui-empty__actions,
149
+ .ui-sx__actions,
150
+ .ui-fbc__foot,
151
+ .ui-drawer__footer,
152
+ .ui-snippet__bar,
153
+ .ui-app__main .ui-card__row {
154
+ row-gap: var(--tap-gap) !important;
155
+ --tap-clear-y: var(--tap-gap);
156
+ }
157
+
158
+ /* Both ways: fixed squares cannot overflow the way labels can, and a drawer
159
+ header spends its column gap on the title. Not the accent picker — its
160
+ swatches are on no carrier list, so opening it bought no reach. */
161
+ .ui-footer__socials,
162
+ .ui-drawer__header {
163
+ gap: var(--tap-gap) !important;
164
+ --tap-clear-x: var(--tap-gap);
165
+ --tap-clear-y: var(--tap-gap);
166
+ }
167
+ }
@@ -26,10 +26,12 @@
26
26
  max-width: var(--prose-caption);
27
27
  padding: var(--space-2) var(--space-3);
28
28
  background: var(--ui-tip-bg); /* ring-gap: inherit — decorative or text-only surface has no nested focus target. */
29
- /* The two-step edge and the drop: --border-strong outside, --border one pixel
30
- inside it, then the broad faint drop. why: docs/specification.md#elevation */
31
- border: 1px solid var(--border-strong);
32
- box-shadow: inset 0 0 0 1px var(--elev-edge, var(--border)), var(--elev-drop);
29
+ /* The two-step edge and the drop, through the level's own names: in DARK that is
30
+ --border-strong outside and --border one pixel inside it, then the drop; in LIGHT
31
+ both lines are transparent and the drop carries the whole separation.
32
+ why: docs/specification.md#elevation */
33
+ border: 1px solid var(--float-edge);
34
+ box-shadow: inset 0 0 0 1px var(--elev-edge, var(--float-edge-inner)), var(--elev-drop);
33
35
  border-radius: var(--radius-sm);
34
36
  font-family: var(--font-sans);
35
37
  font-size: var(--text-xs);
@@ -129,10 +129,12 @@
129
129
  left: 0;
130
130
  min-width: 266px;
131
131
  background: var(--bg-elevated); --ring-gap: var(--bg-elevated); /* the floating step — see dropdown.css. #295 */
132
- /* The two-step edge and the drop: --border-strong outside, --border one pixel
133
- inside it, then the broad faint drop. why: docs/specification.md#elevation */
134
- border: 1px solid var(--border-strong);
135
- box-shadow: inset 0 0 0 1px var(--elev-edge, var(--border)), var(--elev-drop);
132
+ /* The two-step edge and the drop, through the level's own names: in DARK that is
133
+ --border-strong outside and --border one pixel inside it, then the drop; in LIGHT
134
+ both lines are transparent and the drop carries the whole separation.
135
+ why: docs/specification.md#elevation */
136
+ border: 1px solid var(--float-edge);
137
+ box-shadow: inset 0 0 0 1px var(--elev-edge, var(--float-edge-inner)), var(--elev-drop);
136
138
  border-radius: 16px;
137
139
  padding: 6px;
138
140
  opacity: 0;
@@ -186,7 +188,7 @@
186
188
  transition: background var(--dur-fast) var(--ease);
187
189
  }
188
190
  /* Menu rows use the reading surface and an edge on hover — see
189
- src/styles/dropdown.css:164 `.ui-dropdown__item:hover`.
191
+ src/styles/dropdown.css:166 `.ui-dropdown__item:hover`.
190
192
  why: docs/specification.md#elevation */
191
193
  .vopt:hover { outline: 1px solid var(--control-edge); outline-offset: -1px; background: var(--surface); /* ring-gap: inherit — control paint; its focus indicator uses the containing surface. */ }
192
194
  /* The row is a keyboard stop: src/components/dropdown.js:554
@@ -238,10 +240,12 @@
238
240
  right: 0; top: 44px;
239
241
  width: 252px;
240
242
  background: var(--bg-elevated); --ring-gap: var(--bg-elevated); /* the floating step — see dropdown.css. #295 */
241
- /* The two-step edge and the drop: --border-strong outside, --border one pixel
242
- inside it, then the broad faint drop. why: docs/specification.md#elevation */
243
- border: 1px solid var(--border-strong);
244
- box-shadow: inset 0 0 0 1px var(--elev-edge, var(--border)), var(--elev-drop);
243
+ /* The two-step edge and the drop, through the level's own names: in DARK that is
244
+ --border-strong outside and --border one pixel inside it, then the drop; in LIGHT
245
+ both lines are transparent and the drop carries the whole separation.
246
+ why: docs/specification.md#elevation */
247
+ border: 1px solid var(--float-edge);
248
+ box-shadow: inset 0 0 0 1px var(--elev-edge, var(--float-edge-inner)), var(--elev-drop);
245
249
  border-radius: 14px;
246
250
  overflow: hidden;
247
251
  opacity: 0;
@@ -133,10 +133,10 @@
133
133
  --pink: #e97ca5;
134
134
  --amber: #ffcf6a;
135
135
 
136
- /* The elevation ladder, bottom to top. A surface says how high it is with its step and
137
- with the hairline around it, and --muted caps how far the ladder can go. Nothing casts
138
- below the floating step; the floating step itself adds --elev-drop under its inner
139
- line and nothing else in the kit does. why: docs/specification.md#elevation */
136
+ /* The elevation ladder, bottom to top. A surface says how high it is with its step
137
+ and, IN DARK, with the hairline around it; --muted caps how far the ladder can go.
138
+ Nothing casts in dark below the floating step, and the floating step adds
139
+ --elev-drop under its inner line. why: docs/specification.md#elevation */
140
140
  --bg: #0e0d14;
141
141
  --table-bg: var(--bg);
142
142
  --surface-2: #161520; /* non-text sunken marks and tracks */
@@ -150,6 +150,15 @@
150
150
  --border: #332f45;
151
151
  --border-strong: #453f5c;
152
152
 
153
+ /* WHICH EDGE EACH LEVEL DRAWS. One name per level, so a sheet never has to ask
154
+ which theme it is in. Dark answers all four with the hairline, because a drop
155
+ on a near-black page reaches 1.20 at its core and has nowhere else to go —
156
+ light answers them with the drop instead. why: docs/specification.md#elevation */
157
+ --card-edge: var(--border); /* a card */
158
+ --rail-edge: var(--border); /* the shell's rail */
159
+ --float-edge: var(--border-strong); /* a floating surface, outer line */
160
+ --float-edge-inner: var(--border); /* a floating surface, inner line */
161
+
153
162
  /* Fields use the card surface and an edge in both themes. why: docs/specification.md#elevation */
154
163
  --field-bg: var(--surface);
155
164
  --field-edge: var(--border);
@@ -257,13 +266,20 @@
257
266
  token and cannot be — a var() inside a custom property is substituted on the
258
267
  element that DECLARES it, so an --elev-edge read here resolves at :root and
259
268
  nothing below could re-point it. A floating surface writes the pair itself:
260
- box-shadow: inset 0 0 0 1px var(--elev-edge, var(--border)), var(--elev-drop);
269
+ box-shadow: inset 0 0 0 1px var(--elev-edge, var(--float-edge-inner)), var(--elev-drop);
261
270
  Dark spends 62% / 50% of the ink and reaches 1.20 at the drop's core, having
262
271
  nowhere to go on a near-black page. why: docs/specification.md#elevation */
263
272
  --elev-drop:
264
273
  0 14px 30px -12px color-mix(in srgb, var(--shadow-ink) 62%, transparent),
265
274
  0 3px 9px -4px color-mix(in srgb, var(--shadow-ink) 50%, transparent);
266
275
 
276
+ /* The ladder's two resting rungs, which only light climbs. Transparent here and
277
+ not `none`, for the reason the deprecated --shadow-* tokens are: these are read
278
+ in a layer list, and `none` is valid only alone — it would invalidate the whole
279
+ declaration and take the focus ring with it. why: docs/specification.md#elevation */
280
+ --elev-rest: 0 0 #0000; /* a card */
281
+ --elev-rail: 0 0 #0000; /* the shell's rail, cast sideways */
282
+
267
283
  /* Backdrop that dims the page behind an overlay (drawer, modal, composer). */
268
284
  --scrim: rgba(0, 0, 0, 0.58);
269
285
 
@@ -312,6 +328,17 @@
312
328
  --border: #e4e7ee;
313
329
  --border-strong: #cdd2dc;
314
330
 
331
+ /* Light draws a LEVEL with a drop and no line at all. Decided by Artur on the
332
+ #490 triage round t3: "It's better" — the soft shadow over the hairline, against
333
+ the reference he supplied. A line divides two regions of ONE surface (a card's
334
+ rows, a table's rules, the rail's head band, the topbar); a drop says a surface
335
+ is HIGHER, and only a higher surface may cast one.
336
+ why: docs/specification.md#elevation */
337
+ --card-edge: transparent;
338
+ --rail-edge: transparent;
339
+ --float-edge: transparent;
340
+ --float-edge-inner: transparent;
341
+
315
342
  /* Light draws a field with its EDGE, because since #448 it has no well left to draw it
316
343
  with: a card and a panel are both white, the sunken step falls two rungs under either,
317
344
  and --border on it measures 1.047:1. why: docs/specification.md#elevation */
@@ -389,12 +416,21 @@
389
416
  --shadow-ink: #1e1e32;
390
417
  --sheen: #ffffff;
391
418
 
392
- /* 18% / 10% against dark's 62% / 50%. Light's ink is #1e1e32 on a card that is
393
- now #ffffff, and 18% is what puts the drop core at #d7d7da — 1.44, the strongest
394
- separation either theme gets. why: docs/specification.md#elevation */
419
+ /* THE LIGHT LADDER. Three rungs, each two layers: a wide faint one and a short one.
420
+ Each layer is offset on one axis, blurred at least twice as wide as it is offset,
421
+ and held in by a negative spread. None is a glow — a glow is a zero-offset layer of
422
+ the signal's own colour. A level with no line casts a rung that reaches on every
423
+ side it exposes, which is why the wide layer's offset stays under half its blur.
424
+ why: docs/specification.md#elevation */
425
+ --elev-rest:
426
+ 0 2px 26px -3px color-mix(in srgb, var(--shadow-ink) 9%, transparent),
427
+ 0 1px 3px -1px color-mix(in srgb, var(--shadow-ink) 7%, transparent);
428
+ --elev-rail:
429
+ 10px 0 24px -14px color-mix(in srgb, var(--shadow-ink) 16%, transparent),
430
+ 1px 0 3px -1px color-mix(in srgb, var(--shadow-ink) 7%, transparent);
395
431
  --elev-drop:
396
- 0 14px 30px -12px color-mix(in srgb, var(--shadow-ink) 18%, transparent),
397
- 0 3px 9px -4px color-mix(in srgb, var(--shadow-ink) 10%, transparent);
432
+ 0 20px 46px -16px color-mix(in srgb, var(--shadow-ink) 22%, transparent),
433
+ 0 1px 14px -4px color-mix(in srgb, var(--shadow-ink) 12%, transparent);
398
434
 
399
435
  --scrim: rgba(20, 22, 40, 0.38);
400
436