@apliteni/apliteni-ui 0.107.0 → 0.108.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.
@@ -74,6 +74,18 @@
74
74
 
75
75
  **Except:** On a phone, a row whose long column is a paragraph stacks instead of scrolling: the short values on one line, the paragraph under them, and the header kept for screen readers. No column is dropped.
76
76
 
77
+ ## Keep identity sub-lines as text
78
+
79
+ <!-- rule: pinned-identity-sub-lines -->
80
+
81
+ **Why:** An ellipsis cannot cut a flex or grid box, so the name spills past the cell. No specimen draws it, because the cut needs a phone viewport.
82
+
83
+ **Rule:** A pinned identity sub-line is running text.
84
+
85
+ **Do:** Write it as a sentence, with any link inside.
86
+
87
+ **Don't:** Lay a badge, a control or a picture along it, or make it a flex or grid box.
88
+
77
89
  ## Keep updates stable
78
90
 
79
91
  <!-- rule: states -->
@@ -12,7 +12,7 @@
12
12
 
13
13
  **Don't:** Give the input a separate muted outline while the button uses `--ring`.
14
14
 
15
- **Except:** A scroll region inside a surface takes `--ring-scroll`: the same band drawn inward, with `--ring-scroll-offset`. An inset shadow paints under a box's own children, so a table scrolling under one erases the band; an outline survives it. A box that is itself the outermost surface — a panel, a card — keeps `--ring`. Text-entry controls may match `:focus-visible` on mouse focus. Both bands are declared once at the root and recomposed nowhere, because an outline leaves its offset unpainted. Adjust width and colour there.
15
+ **Except:** A box whose own edges are a surface's — a scroll region, or a box filling a table cell — takes `--ring-scroll` with `--ring-scroll-offset`. A box that is the outermost surface, such as a panel or card, keeps `--ring`. Text-entry controls may match `:focus-visible` on mouse focus. Both bands are declared once at the root and recomposed nowhere, because an outline leaves its offset unpainted. Adjust width and colour there.
16
16
 
17
17
  ## Busy controls
18
18
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apliteni/apliteni-ui",
3
- "version": "0.107.0",
3
+ "version": "0.108.0",
4
4
  "workspaces": [
5
5
  "react"
6
6
  ],
@@ -291,6 +291,15 @@
291
291
  outline-offset: var(--ring-offset);
292
292
  border-radius: var(--radius-xs);
293
293
  }
294
+ .rx-sort {
295
+ max-width: 100%;
296
+ min-width: 0;
297
+ }
298
+ .rx-sort__label {
299
+ min-width: 0;
300
+ overflow: hidden;
301
+ text-overflow: ellipsis;
302
+ }
294
303
  .rx-caret {
295
304
  margin-left: 2px;
296
305
  width: 16px;
@@ -207,6 +207,16 @@ type Column<T> = {
207
207
  num?: boolean;
208
208
  sortable?: boolean;
209
209
  render?: (row: T) => ReactNode;
210
+ /**
211
+ * `true` marks this column's body cells as holding one link and nothing else, so the
212
+ * link fills its cell and its focus ring outlines the cell rather than the words. It is
213
+ * the consumer's to declare because CSS counts elements and not words: a cell reading
214
+ * "Invoice 1162" with the number linked looks the same to a selector, and filling that
215
+ * one would put the number on a line of its own. The header cell is never marked — it
216
+ * holds a label, or a sort button that keeps its own ring.
217
+ * why: docs/components.md#dense-financial-tables
218
+ */
219
+ linked?: boolean;
210
220
  };
211
221
  type TableSort<T> = {
212
222
  key: (keyof T & string) | undefined;
@@ -1284,10 +1284,15 @@ function DataTable({
1284
1284
  scope: "col",
1285
1285
  className: [pinnedIdentity && columnIndex === 0 && "ui-table__identity", c.num && "ui-table__num", c.sortable && "rx-sortable"].filter(Boolean).join(" "),
1286
1286
  "aria-sort": sort.key === c.key ? sort.dir === 1 ? "ascending" : "descending" : c.sortable && sortIsKnown ? "none" : void 0,
1287
- children: c.sortable ? /* @__PURE__ */ jsxs13("button", { type: "button", className: "rx-sort", onClick: () => onSort(c.key), children: [
1288
- c.label,
1289
- caret(c.key)
1290
- ] }) : c.label
1287
+ children: c.sortable ? (
1288
+ // The label is its own box so it can truncate: in a pinned
1289
+ // identity column on a phone the header is capped, and a bare
1290
+ // text node would push the caret out of the cell instead.
1291
+ /* @__PURE__ */ jsxs13("button", { type: "button", className: "rx-sort", onClick: () => onSort(c.key), children: [
1292
+ /* @__PURE__ */ jsx17("span", { className: "rx-sort__label", children: c.label }),
1293
+ caret(c.key)
1294
+ ] })
1295
+ ) : c.label
1291
1296
  },
1292
1297
  c.key
1293
1298
  )
@@ -1305,7 +1310,7 @@ function DataTable({
1305
1310
  onChange: () => onToggle(r.name)
1306
1311
  }
1307
1312
  ) }) : null,
1308
- columns.map((c, columnIndex) => /* @__PURE__ */ jsx17("td", { className: [c.num && "ui-table__num", pinnedIdentity && columnIndex === 0 && "ui-table__identity"].filter(Boolean).join(" "), children: c.render ? c.render(r) : String(r[c.key]) }, c.key))
1313
+ columns.map((c, columnIndex) => /* @__PURE__ */ jsx17("td", { className: [c.num && "ui-table__num", pinnedIdentity && columnIndex === 0 && "ui-table__identity", c.linked && "ui-table__linked"].filter(Boolean).join(" "), children: c.render ? c.render(r) : String(r[c.key]) }, c.key))
1309
1314
  ] }, r.name))
1310
1315
  ] })
1311
1316
  ]
@@ -161,12 +161,42 @@
161
161
  top: 50%;
162
162
  translate: 0 -50%;
163
163
  }
164
- /* No focus rule here any more. #510 gave a cell link the ring with a rule of this file's
165
- own, because `base.css` reached no bare `a`; #587 put `a` on that shared list, so this
166
- one painted the same two declarations a second time. The guarantee is unchanged and
167
- still held by src/styles/table-focus.test.js — the corner the band follows is the
168
- `border-radius` above, which is this file's and stays.
169
- why: guidelines/state-set.md#focus-rings */
164
+ /* No generic focus rule here. #510 gave a cell link the ring because `base.css`
165
+ reached no bare `a`; #587 put `a` on that shared list, so this file only keeps
166
+ the filled-cell override below. why: guidelines/state-set.md#focus-rings */
167
+ /* The handle: a cell MARKED `.ui-table__linked` holds one link and nothing else, so the
168
+ link fills the cell and its ring outlines the cell rather than the words. The cell hands
169
+ its padding over and nothing moves. `height: 1px` is the cell stating no height of its
170
+ own, which a percentage height on the link can resolve against; a cell is stretched to
171
+ its row either way, so no row changes.
172
+ The mark is the consumer's because a selector counts elements and not words: `:only-child`
173
+ reads `<td>Invoice <a>1162</a></td>` as a lone link too. The link is named rather than
174
+ denied — a denylist of the anchors that own their box went stale twice (#510).
175
+ why: docs/components.md#dense-financial-tables, #500
176
+ held by: src/styles/table-focus.test.js */
177
+ .ui-table td.ui-table__linked:has(> :is(a:not([class]), a.ui-identity):only-child) { padding: 0; height: 1px; }
178
+ .ui-table td.ui-table__linked > :is(a:not([class]), a.ui-identity):only-child {
179
+ box-sizing: border-box;
180
+ display: flex;
181
+ align-items: center;
182
+ width: 100%;
183
+ height: 100%;
184
+ padding: var(--space-4) var(--space-4) var(--space-4) 0;
185
+ border-radius: var(--radius-xs);
186
+ }
187
+ .ui-table--dense td.ui-table__linked > :is(a:not([class]), a.ui-identity):only-child { padding: var(--space-2) var(--space-3); }
188
+ .ui-table--compact td.ui-table__linked > :is(a:not([class]), a.ui-identity):only-child { padding: var(--space-1) var(--space-3); }
189
+ /* Zebra insets the row's END cells only, so the fill does too: a marked identity behind a
190
+ selection column is a middle cell, and a step it never gave up would move its name. */
191
+ .ui-table--zebra td.ui-table__linked:first-child > :is(a:not([class]), a.ui-identity):only-child { padding-left: var(--space-3); }
192
+ .ui-table--zebra td.ui-table__linked:last-child > :is(a:not([class]), a.ui-identity):only-child { padding-right: var(--space-3); }
193
+ /* The inward band, the one a scroll region takes: a filled cell's edges are the
194
+ table's, where the pinned column's border and the scroller's 4px side padding
195
+ cut an outset ring away. Its 1px gap is unpainted, and the ring is the outline. */
196
+ .ui-table td.ui-table__linked > :is(a:not([class]), a.ui-identity):only-child:focus-visible {
197
+ outline: var(--ring-scroll);
198
+ outline-offset: var(--ring-scroll-offset);
199
+ }
170
200
 
171
201
  .ui-table__num { text-align: right; font-variant-numeric: tabular-nums; }
172
202
  /* A numeric column is sized to its content while .ui-table__title claims the rest,
@@ -356,14 +386,78 @@
356
386
  a.ui-identity:hover { text-decoration: underline; }
357
387
  .ui-identity:focus-visible { outline: var(--ring); outline-offset: var(--ring-offset); border-radius: var(--radius-xs); }
358
388
  @media (max-width: 720px) {
359
- .ui-table--pinned .ui-table__identity { width: 1%; }
389
+ /* One line wide: nothing wraps, and --ui-table-identity-max is the ceiling a consumer
390
+ retunes. The cut takes NO margin — the cell is sticky and the next column scrolls
391
+ under it, so whatever the cut lets out is painted over that column. A child's ring
392
+ needs 3px and the cell's padding is 12px, so the room comes from the padding.
393
+ why: docs/components.md#dense-financial-tables, #500, #513 */
394
+ .ui-table--pinned .ui-table__identity {
395
+ width: 1%;
396
+ max-width: var(--ui-table-identity-max);
397
+ white-space: nowrap;
398
+ overflow: clip;
399
+ text-overflow: ellipsis;
400
+ }
401
+ /* A sub-line is one line of running text and cuts itself: the cell's ellipsis cannot
402
+ reach into a child block. 4px is a focused link's ring, inside the cell's padding.
403
+ Nothing is capped here — block layout already holds a child to the cell. */
404
+ .ui-table--pinned .ui-table__identity > * {
405
+ overflow: clip;
406
+ overflow-clip-margin: var(--space-1);
407
+ text-overflow: ellipsis;
408
+ }
409
+ /* A picture has no shorter form, so it keeps its intrinsic size and the cell cuts it.
410
+ Nothing caps it, so write neither a cap nor a reset of one here. #513 */
411
+ .ui-table--pinned .ui-table__identity :is(svg, img, canvas, video, picture) {
412
+ overflow: visible;
413
+ text-overflow: clip;
414
+ }
415
+ .ui-table--pinned .ui-table__identity > *:has(:is(svg, img, canvas, video, picture)) {
416
+ text-overflow: clip;
417
+ }
418
+ /* A control's label is text, so it yields: held to the cell and cut at its END. A kit
419
+ control centres its label, and a capped one that centres loses the front of it. #513 */
420
+ .ui-table--pinned .ui-table__identity :is(.ui-btn, .ui-nav__item, .ui-badge, button, input, select, textarea) {
421
+ max-width: 100%;
422
+ min-width: auto;
423
+ overflow: hidden;
424
+ justify-content: flex-start;
425
+ text-align: start;
426
+ }
427
+ /* `a:not([class])` above is an inline-block so a WRAPPING title link paints one ring.
428
+ Nothing wraps here, and an inline-block is atomic: `text-overflow` cannot cut inside
429
+ one, so the link came out cut mid-glyph. Inline, it draws one fragment and one
430
+ ring, and the line's ellipsis reaches it. #513 */
431
+ .ui-table--pinned .ui-table__identity a:not([class]) { display: inline; }
432
+ /* Except where the fill gave the lone link the cell's box: it cuts as a block box. */
433
+ .ui-table--pinned .ui-table__identity.ui-table__linked > a:not([class]):only-child {
434
+ display: block;
435
+ align-content: center;
436
+ }
437
+ /* The ring goes on the LINE: a link longer than the cell runs past the cut, so a ring
438
+ around the link has no surface to draw its right side on. Inward, like every ring
439
+ the kit puts in a clipped region. why: docs/foundations.md#the-focus-ring, #513 */
440
+ .ui-table--pinned .ui-table__identity > *:has(a:focus-visible) {
441
+ outline: var(--ring-scroll);
442
+ outline-offset: var(--ring-scroll-offset);
443
+ border-radius: var(--radius-xs);
444
+ }
445
+ .ui-table--pinned .ui-table__identity > * a:focus-visible { outline: none; }
446
+ .ui-table--pinned .ui-table__identity:not(.ui-table__linked):has(> a:focus-visible) {
447
+ outline: var(--ring-scroll);
448
+ outline-offset: var(--ring-scroll-offset);
449
+ border-radius: var(--radius-xs);
450
+ }
451
+ .ui-table--pinned .ui-table__identity:not(.ui-table__linked) > a:focus-visible { outline: none; }
360
452
  .ui-table--pinned .ui-identity__logo { display: none; }
361
- .ui-table--pinned .ui-identity__name { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
362
- .ui-table--pinned .ui-identity { max-width: var(--panel-sm); }
453
+ /* An inline-flex box is the one child block layout does not hold to the cell, so the
454
+ cap belongs on it. The whole name stays in the DOM; only the drawn line is cut. */
455
+ .ui-table--pinned .ui-identity { max-width: 100%; }
456
+ .ui-table--pinned .ui-identity__name { min-width: 0; max-width: 100%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
363
457
  }
364
458
 
365
459
  /* One width reserves the selection track and offsets the adjacent identity. */
366
- .ui-table--pinned { --ui-table-selection-width: var(--space-12); }
460
+ .ui-table--pinned { --ui-table-selection-width: var(--space-12); --ui-table-identity-max: min(var(--panel-sm), 50vw); }
367
461
  .ui-table--pinned .ui-table__selection { box-sizing: border-box; width: var(--ui-table-selection-width); min-width: var(--ui-table-selection-width); max-width: var(--ui-table-selection-width); padding-inline: 0; text-align: center; }
368
462
  .ui-table--pinned .ui-table__selection + .ui-table__identity { left: var(--ui-table-selection-width); }
369
463
 
@@ -413,10 +507,44 @@ a.ui-identity:hover { text-decoration: underline; }
413
507
  .ui-table.ui-table--stack.ui-table--pinned > :where(thead, tbody) > tr > :where(.ui-table__identity, .ui-table__selection) {
414
508
  position: static; width: auto; min-width: 0; max-width: none;
415
509
  padding-inline: 0; border-right: 0; text-align: left;
510
+ /* The cap's cut goes with the cap: a card is as wide as the row, so there is
511
+ nothing to cut, and a clip box here would only reach a focused link's ring. */
512
+ overflow: visible; text-overflow: clip;
416
513
  }
514
+ /* The name is what the card is about, and a card has the width to wrap it. #500 put
515
+ `nowrap` on it for the narrow column, so the stacked row takes that off too. */
417
516
  .ui-table.ui-table--stack.ui-table--pinned .ui-identity__name {
418
517
  position: static; width: auto; height: auto; max-width: none;
419
- overflow: visible; clip-path: none;
518
+ overflow: visible; clip-path: none; white-space: normal; text-overflow: clip;
519
+ }
520
+ .ui-table.ui-table--stack.ui-table--pinned .ui-table__identity > * {
521
+ overflow: visible; text-overflow: clip;
522
+ }
523
+ .ui-table.ui-table--stack.ui-table--pinned .ui-table__identity > * a {
524
+ max-width: none; overflow: visible; text-overflow: clip;
525
+ }
526
+ .ui-table.ui-table--stack.ui-table--pinned .ui-table__identity > a:not([class]) {
527
+ display: inline-block;
528
+ }
529
+ .ui-table.ui-table--stack.ui-table--pinned .ui-table__identity:not(.ui-table__linked):has(> a:focus-visible) {
530
+ outline: none;
531
+ }
532
+ .ui-table.ui-table--stack.ui-table--pinned .ui-table__identity:not(.ui-table__linked) > a:focus-visible {
533
+ outline: var(--ring);
534
+ outline-offset: var(--ring-offset);
535
+ }
536
+ /* A card has no column to fill: every cell is a block on its own line and the ROW pays
537
+ the inset, so the fill's padding would stand the link a step right of the card's text
538
+ edge and the cell's `height: 1px` would hold a block cell to one pixel. Written through
539
+ the row like the resets above it, so it outranks the fill and its density. */
540
+ .ui-table.ui-table--stack > tbody > tr > td.ui-table__linked:has(> :is(a:not([class]), a.ui-identity):only-child) { height: auto; }
541
+ .ui-table.ui-table--stack > tbody > tr > td.ui-table__linked > :is(a:not([class]), a.ui-identity):only-child {
542
+ width: auto; height: auto; padding: 0;
543
+ }
544
+ /* The stacked card gives the link its own box again, so it takes the outset band. */
545
+ .ui-table.ui-table--stack > tbody > tr > td.ui-table__linked > a:only-child:focus-visible {
546
+ outline: var(--ring);
547
+ outline-offset: var(--ring-offset);
420
548
  }
421
549
  /* The bleed now pays for the row's inset instead of the end cells', so the cells still
422
550
  start on the card's own text edge and the outline keeps its room on both sides. Same