@gemboss/ui 0.12.0 → 0.13.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/kit-plain.css CHANGED
@@ -88,7 +88,23 @@
88
88
  .ui-link:focus-visible,
89
89
  .ui-prose :where(a:not([class])):focus-visible { outline: 2px solid var(--ui-focus, #5661f5); outline-offset: 2px; border-radius: 2px; }
90
90
 
91
- .ui-card--media { padding: 0; overflow: hidden; display: flex; flex-direction: column; }
91
+ .ui-card--media { position: relative; padding: 0; overflow: hidden; display: flex; flex-direction: column; }
92
+ /* 0.13.0 (README "Collections", card grid): the title is the card's link and its overlay covers the card
93
+ (`a.ui-card__link`; the picture link is aria-hidden, out of the tab order); `.ui-card__menu` (a ⋯ ui-menu) sits in the
94
+ picture's corner above it, and the card stops clipping so the open menu is not cut off; `data-ratio="4/3"` for a theme
95
+ or a page. */
96
+ .ui-card__link { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-clamp: 2; overflow: hidden; color: var(--ui-text); font-size: var(--ui-fs-body); line-height: 20px; font-weight: 650; text-decoration: none; overflow-wrap: anywhere; }
97
+ .ui-card__link::after { content: ""; position: absolute; inset: 0; z-index: 0; }
98
+ .ui-card__link:focus-visible { outline: none; }
99
+ .ui-card--media:has(.ui-card__link) { cursor: pointer; }
100
+ .ui-card--media:has(.ui-card__link:hover) { border-color: var(--ui-text-muted); }
101
+ .ui-card--media:has(.ui-card__link:focus-visible) { outline: 2px solid var(--ui-focus); outline-offset: 1px; }
102
+ .ui-card--media :is(.ui-card__foot button, .ui-card__foot a, .ui-card__body a:not(.ui-card__link), .ui-card__body button, select, input, summary) { position: relative; z-index: 1; }
103
+ .ui-card__menu { position: absolute; top: 8px; right: 8px; z-index: 2; }
104
+ .ui-card--media:has(> .ui-card__menu) { overflow: visible; }
105
+ .ui-card--media:has(> .ui-card__menu) > .ui-card__media { border-radius: inherit; border-bottom-left-radius: 0; border-bottom-right-radius: 0; }
106
+ .ui-card__meta { color: var(--ui-text-muted); font-size: var(--ui-fs-caption); line-height: 16px; }
107
+ .ui-card--media[data-ratio="4/3"] > .ui-card__media { aspect-ratio: 4 / 3; }
92
108
  .ui-card__media {
93
109
  display: block; width: 100%; aspect-ratio: 16 / 10; margin: 0; padding: 0; border: 0;
94
110
  border-bottom: 1px solid var(--ui-border); background: var(--ui-surface-2); overflow: hidden; color: inherit;
@@ -475,7 +491,7 @@ button.ui-metric:focus-visible { outline: 2px solid var(--ui-focus); outline-off
475
491
  }
476
492
  .ui-chip:hover { border-style: solid; background: var(--ui-surface-2); }
477
493
  .ui-chip:focus-visible { outline: 2px solid var(--ui-focus); outline-offset: 1px; }
478
- .ui-chip:is([aria-pressed="true"], [aria-current="page"]) { border-style: solid; border-color: var(--ui-accent); background: var(--ui-accent-soft); color: var(--ui-accent-strong); }
494
+ .ui-chip:is([aria-pressed="true"], [aria-current="page"], [aria-current="true"]) { border-style: solid; border-color: var(--ui-accent); background: var(--ui-accent-soft); color: var(--ui-accent-strong); }
479
495
  @media (pointer: coarse), (max-width: 47.9975em) { .ui-chip { height: auto; min-height: 44px; min-width: 44px; } }
480
496
  /* Card stack: `<div class="ui-stack"><div class="ui-card">…</div><div class="ui-card">…</div></div>`. 16px between blocks,
481
497
  Polaris BlockStack gap 400, the admin's spacing between cards on a page. `--tight` is 8 (inside a card). */
package/src/kit-plain.js CHANGED
@@ -17,6 +17,8 @@
17
17
  initPasswordFields(); // .gb-auth-pw-toggle reveal; resetPasswordFields() on sign-out
18
18
  initSearch(); // TopbarSearch: ⌘K / Ctrl+K and "/" focus the box, Escape leaves it
19
19
  // initNav() runs by itself (0.10.0): a followed rail link shows a spinner, a rested-on link is prefetched
20
+ // initCollections() runs by itself (0.13.0): rows with data-href open on a click, .ui-index selection + bulk bar,
21
+ // client sort; gembossUI.selection(root) → the selected ids (README "Collections")
20
22
  */
21
23
  (function (g) {
22
24
  "use strict";
@@ -434,7 +436,140 @@
434
436
  }
435
437
  initNav();
436
438
 
439
+ /* Collections (0.13.0), for the markup in README "Collections" (collections.css). Runs by itself when the script loads,
440
+ delegated on the document, so rows rendered later are covered; `initCollections()` is idempotent.
441
+ - Row links: a row with `data-href` (an index-table `tr`, or any element) opens on a click anywhere that is not a
442
+ control of its own (ROW_CONTROLS: a link, button, checkbox, select, menu, `data-row-ignore`) and does not end a
443
+ text selection. It follows the row's own link to that href (so a client router, `target` and the nav spinner all
444
+ apply); Cmd / Ctrl / Shift / middle click opens a new tab. No such link: location.assign. A React IndexTable
445
+ handles its own rows (`.is-linked`, no data-href), so the two never both fire.
446
+ - Selection: in a `.ui-index`, `input[data-gb-select]` (value = the row's id) and `input[data-gb-select-all]` (the
447
+ header's and the bulk bar's). The row gets `is-selected` + aria-selected, select-all goes indeterminate, the
448
+ `.ui-bulkbar` shows with `[data-gb-count]` filled from its own template ("{n} selected"), Shift+click selects a
449
+ range, Escape clears. The page reads `gembossUI.selection(root)` (ids) or listens for `gb:selection`.
450
+ - Sort, only in a table with `data-gb-sort` (a React table owns its own buttons): `="client"` sorts its rows when a
451
+ `button.ui-table__sort` is pressed (by each cell's `data-sort-value`, else its text; numbers as numbers) and sets
452
+ aria-sort; `="event"` fires `gb:sort` ({ key: th.dataset.key, direction }) for the page. A server sort is a link
453
+ (`a.ui-table__sort`) and needs nothing. */
454
+ var ROW_CONTROLS = 'a[href], button, input, select, textarea, label, summary, details, dialog, [role="dialog"], [role="menu"], [role="button"], [role="menuitem"], [role="checkbox"], [contenteditable="true"], [contenteditable=""], [data-row-ignore], .ui-table__check';
455
+ var ESCAPE_OWNERS = 'details[open], dialog, [role="dialog"], [role="menu"]';
456
+ var collOn = false, lastBox = null;
457
+ function rowLink(row) {
458
+ var href = row.getAttribute("data-href"), links = row.querySelectorAll("a[href]");
459
+ for (var i = 0; i < links.length; i++) if (links[i].getAttribute("href") === href) return links[i];
460
+ return null;
461
+ }
462
+ function onRowClick(e) {
463
+ if (e.defaultPrevented || e.button > 1 || !e.target.closest) return;
464
+ var row = e.target.closest("[data-href]");
465
+ if (!row) return;
466
+ var hit = e.target.closest(ROW_CONTROLS);
467
+ if (hit && row.contains(hit)) return;
468
+ var sel = g.getSelection ? g.getSelection() : null;
469
+ if (sel && !sel.isCollapsed && sel.anchorNode && row.contains(sel.anchorNode)) return;
470
+ var a = rowLink(row), href = a ? a.href : new URL(row.getAttribute("data-href"), location.href).href;
471
+ if (e.metaKey || e.ctrlKey || e.shiftKey || e.button === 1) { g.open(href, "_blank", "noopener"); return; }
472
+ if (a) a.click(); else location.assign(href);
473
+ }
474
+ function selectionIn(root) {
475
+ var out = [];
476
+ root.querySelectorAll("input[data-gb-select]").forEach(function (b) { if (b.checked) out.push(b.value); });
477
+ return out;
478
+ }
479
+ function syncSelection(root) {
480
+ var boxes = root.querySelectorAll("input[data-gb-select]"), n = 0;
481
+ boxes.forEach(function (b) {
482
+ var row = b.closest("tr, .ui-list__item, [data-href]");
483
+ if (b.checked) n++;
484
+ if (row) { row.classList.toggle("is-selected", b.checked); row.setAttribute("aria-selected", b.checked ? "true" : "false"); }
485
+ });
486
+ root.querySelectorAll("input[data-gb-select-all]").forEach(function (b) { b.checked = n > 0 && n === boxes.length; b.indeterminate = n > 0 && n < boxes.length; });
487
+ var bar = root.querySelector(".ui-bulkbar");
488
+ if (bar) bar.hidden = n === 0;
489
+ var c = root.querySelector("[data-gb-count]");
490
+ if (c) c.textContent = (c.getAttribute("data-gb-count") || "{n} selected").replace("{n}", n);
491
+ root.dispatchEvent(new CustomEvent("gb:selection", { bubbles: true, detail: { ids: selectionIn(root), total: boxes.length } }));
492
+ }
493
+ function clearSelection(root) {
494
+ root.querySelectorAll("input[data-gb-select]").forEach(function (b) { b.checked = false; });
495
+ syncSelection(root);
496
+ }
497
+ function sortValue(td) {
498
+ if (!td) return "";
499
+ var v = td.getAttribute("data-sort-value");
500
+ return v != null ? v : td.textContent.trim();
501
+ }
502
+ function onSortClick(btn) {
503
+ var th = btn.closest("th"), table = btn.closest("table");
504
+ if (!th || !table) return;
505
+ var dir = th.getAttribute("aria-sort") === "ascending" ? "descending" : "ascending";
506
+ table.querySelectorAll("thead th[aria-sort]").forEach(function (x) { if (x !== th) x.setAttribute("aria-sort", "none"); });
507
+ th.setAttribute("aria-sort", dir);
508
+ if (table.getAttribute("data-gb-sort") !== "client") {
509
+ table.dispatchEvent(new CustomEvent("gb:sort", { bubbles: true, detail: { key: th.getAttribute("data-key"), direction: dir } }));
510
+ return;
511
+ }
512
+ var col = Array.prototype.indexOf.call(th.parentElement.children, th), body = table.tBodies[0];
513
+ if (!body) return;
514
+ var rows = Array.prototype.slice.call(body.rows), sign = dir === "ascending" ? 1 : -1;
515
+ var num = function (s) { var x = String(s).replace(/[\s,%$€£¥]/g, ""); return x !== "" && !isNaN(+x) ? +x : null; };
516
+ rows.sort(function (r1, r2) {
517
+ var a = sortValue(r1.cells[col]), b = sortValue(r2.cells[col]), na = num(a), nb = num(b);
518
+ return sign * (na != null && nb != null ? na - nb : String(a).localeCompare(String(b), undefined, { numeric: true, sensitivity: "base" }));
519
+ });
520
+ rows.forEach(function (r) { body.appendChild(r); });
521
+ }
522
+ function initCollections() {
523
+ if (collOn || typeof document === "undefined") return;
524
+ collOn = true;
525
+ document.addEventListener("click", function (e) {
526
+ var t = e.target;
527
+ if (!t || !t.closest) return;
528
+ // Only a table that asked for it (data-gb-sort): a React DataTable / IndexTable owns its own sort buttons.
529
+ var sb = t.closest("table[data-gb-sort] button.ui-table__sort");
530
+ if (sb) { onSortClick(sb); return; }
531
+ // A click in the checkbox cell around the box (40px cell, 24px box) selects, it does not open the row.
532
+ var cc = t.closest("td.ui-table__check, th.ui-table__check");
533
+ // Only the plain contract's boxes: a React IndexTable toggles its own cell (both would undo each other).
534
+ if (cc && t === cc) { var ib = cc.querySelector("input[data-gb-select], input[data-gb-select-all]"); if (ib) { ib.click(); return; } }
535
+ // Shift+click on a row box: remember it for the range (the change event carries no shiftKey).
536
+ var box = t.closest("input[data-gb-select]");
537
+ if (box) {
538
+ var root = box.closest(".ui-index");
539
+ if (root && e.shiftKey && lastBox && lastBox !== box && root.contains(lastBox)) {
540
+ var all = Array.prototype.slice.call(root.querySelectorAll("input[data-gb-select]"));
541
+ var i = all.indexOf(lastBox), j = all.indexOf(box), on = box.checked;
542
+ for (var k = Math.min(i, j); k <= Math.max(i, j); k++) all[k].checked = on;
543
+ syncSelection(root);
544
+ }
545
+ lastBox = box;
546
+ return;
547
+ }
548
+ onRowClick(e);
549
+ });
550
+ document.addEventListener("auxclick", function (e) { if (e.button === 1) onRowClick(e); });
551
+ document.addEventListener("change", function (e) {
552
+ var t = e.target;
553
+ if (!t || !t.matches) return;
554
+ var root = t.closest(".ui-index");
555
+ if (!root) return;
556
+ if (t.matches("input[data-gb-select-all]")) {
557
+ root.querySelectorAll("input[data-gb-select]").forEach(function (b) { b.checked = t.checked; });
558
+ syncSelection(root);
559
+ } else if (t.matches("input[data-gb-select]")) syncSelection(root);
560
+ });
561
+ document.addEventListener("keydown", function (e) {
562
+ if (e.key !== "Escape" || e.defaultPrevented || MODALS.length) return;
563
+ var root = document.activeElement && document.activeElement.closest ? document.activeElement.closest(".ui-index") : null;
564
+ // An open ⋯ menu or a dialog in the table takes this Escape (initMenus closes it), not the selection.
565
+ if (root && (document.activeElement.closest(ESCAPE_OWNERS) || root.querySelector(ESCAPE_OWNERS))) return;
566
+ if (root && root.querySelector("input[data-gb-select]:checked")) clearSelection(root);
567
+ });
568
+ }
569
+ initCollections();
570
+
437
571
  g.gembossUI = { openModal: openModal, toast: toast, confirm: confirm, initMenus: initMenus, closeMenus: closeMenus,
438
572
  initSearch: initSearch, initShell: initShell, initPasswordFields: initPasswordFields, resetPasswordFields: resetPasswordFields,
439
- initNav: initNav, openModals: function () { return MODALS.length; } };
573
+ initNav: initNav, openModals: function () { return MODALS.length; },
574
+ initCollections: initCollections, selection: selectionIn, clearSelection: clearSelection, syncSelection: syncSelection };
440
575
  })(globalThis);