@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/CHANGELOG.md +33 -0
- package/README.md +74 -0
- package/catalog/index.html +2138 -10
- package/gemboss-ui.css +19 -3
- package/package.json +2 -1
- package/src/collections.css +350 -0
- package/src/collections.tsx +414 -0
- package/src/index-table.tsx +216 -0
- package/src/index.ts +11 -0
- package/src/kit-plain.css +18 -2
- package/src/kit-plain.js +136 -1
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);
|