@danieldeusing/design 0.29.0 → 0.31.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/README.md +1 -0
- package/dist/danieldeusing-design.css +120 -1
- package/dist/danieldeusing-design.min.css +2 -2
- package/package.json +1 -1
- package/runtime/index.js +3 -2
- package/runtime/tabletools.js +557 -0
- package/src/components.css +119 -0
- package/templates/documentation.html +5 -5
package/README.md
CHANGED
|
@@ -112,6 +112,7 @@ Four dependency-free ES modules, tree-shakeable from `@danieldeusing/design/runt
|
|
|
112
112
|
| `initDropdowns()` | `<details class="dropdown">` behaviour: one-open, click-away, Escape. |
|
|
113
113
|
| `initSelects()` | Replaces the OS dropdown on every `<select>` with the themed listbox — the option list is painted outside the page, so CSS alone can never reach it. Markup contract: none. The `<select>` keeps the value and still fires `input`/`change`, and selects rendered later are enhanced on their own. |
|
|
114
114
|
| `initTablePagination()` | Page every `<table data-table-id>` to 20 rows, with a 5/10/20/50/100/200 picker remembered per table. It has no sort and no filter — it hides all but one window of the rows a page has **already** filtered and sorted, so the order is always filter → sort → slice over the full set. A table without a `data-table-id` is left alone. |
|
|
115
|
+
| `initTableTools()` | Give every `<table data-table-tools>` a search box, per-column sort and filter controls in its header, and a bar naming whatever is in force. Markup contract: `<th data-col="key">`. Optional `data-filter="pick"` for a value list built from the column's own cells, `data-sort-type="num"`, and `data-value` on a `<td>` to sort by something it does not print. The view is remembered per table. |
|
|
115
116
|
| `initAnimToggle()` | Wire `[data-anim-toggle]` buttons (`.anim-toggle`) to flip `html.anim-off` + persist it. |
|
|
116
117
|
| `initResolutionZoom(1920)` | **Deprecated since 0.29.0 — a no-op.** Scaling above 1920 is CSS now (the fluid root font size in `tokens.css`), so it needs no script. Kept exported so a surface can bump its pin without editing its `<head>` in the same commit. Delete the call, and any inline pre-paint zoom block with it: a page that still sets `style.zoom` on top of 0.29.0 scales twice. |
|
|
117
118
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! danieldeusing-design v0.
|
|
1
|
+
/*! danieldeusing-design v0.31.0 | MIT | https://github.com/danieldeusing/danieldeusing-design */
|
|
2
2
|
/* ===== reset.css ===== */
|
|
3
3
|
/*
|
|
4
4
|
* danieldeusing-design — minimal reset.
|
|
@@ -1620,6 +1620,125 @@ html.dgm-locked, html.dgm-locked body { overflow: hidden; }
|
|
|
1620
1620
|
/* The strip is navigation for a screen. On paper the page IS the map. */
|
|
1621
1621
|
@media print { .minimap { display: none !important; } }
|
|
1622
1622
|
|
|
1623
|
+
/* ── TABLE TOOLS: search on top, sort + filter in the header ──────────────────
|
|
1624
|
+
Paired with runtime/tabletools.js. The estate grew four generations of table
|
|
1625
|
+
filtering before this existed — cockpit's `tr.act-filters`, cockpit's older
|
|
1626
|
+
`tr.filters`, the family contacts table's bare box row — and each spent a
|
|
1627
|
+
whole row of vertical space advertising a capability idle on most visits.
|
|
1628
|
+
|
|
1629
|
+
Two controls in the <th> cost nothing when unused and sit on the column they
|
|
1630
|
+
act on. The filter is a `details.dropdown`, the SYSTEM's dropdown, so
|
|
1631
|
+
one-open / click-away / Escape come from initDropdowns() and are not
|
|
1632
|
+
reimplemented here. Only the parts that are genuinely table-specific are
|
|
1633
|
+
declared below. */
|
|
1634
|
+
.tbl-toolbar {
|
|
1635
|
+
display: flex;
|
|
1636
|
+
align-items: center;
|
|
1637
|
+
gap: 0.5rem;
|
|
1638
|
+
margin-block-end: 0.5rem;
|
|
1639
|
+
}
|
|
1640
|
+
.tbl-search {
|
|
1641
|
+
flex: 1 1 auto;
|
|
1642
|
+
min-inline-size: 0;
|
|
1643
|
+
font: inherit;
|
|
1644
|
+
font-size: var(--fs-base);
|
|
1645
|
+
color: var(--foreground);
|
|
1646
|
+
background: var(--background);
|
|
1647
|
+
border: 1px solid var(--border);
|
|
1648
|
+
padding: 0.25rem 0.5rem;
|
|
1649
|
+
}
|
|
1650
|
+
.tbl-search:focus-visible { outline: 2px solid var(--ring); outline-offset: 1px; }
|
|
1651
|
+
|
|
1652
|
+
/* The header cell keeps its label on the baseline and parks the controls at its
|
|
1653
|
+
trailing edge, so a column of numbers still reads as a column of numbers. */
|
|
1654
|
+
.tbl-tools {
|
|
1655
|
+
display: inline-flex;
|
|
1656
|
+
align-items: center;
|
|
1657
|
+
gap: 0.15rem;
|
|
1658
|
+
margin-inline-start: 0.4rem;
|
|
1659
|
+
vertical-align: baseline;
|
|
1660
|
+
}
|
|
1661
|
+
.tbl-sort,
|
|
1662
|
+
.tbl-filter > summary {
|
|
1663
|
+
background: transparent;
|
|
1664
|
+
border: 0;
|
|
1665
|
+
padding: 0 0.15rem;
|
|
1666
|
+
font: inherit;
|
|
1667
|
+
font-size: var(--fs-base);
|
|
1668
|
+
line-height: 1;
|
|
1669
|
+
color: var(--muted-foreground);
|
|
1670
|
+
cursor: pointer;
|
|
1671
|
+
opacity: 0.55;
|
|
1672
|
+
transition: opacity 0.15s ease, color 0.15s ease;
|
|
1673
|
+
}
|
|
1674
|
+
.tbl-sort:hover,
|
|
1675
|
+
.tbl-filter > summary:hover,
|
|
1676
|
+
th[aria-sort]:not([aria-sort="none"]) .tbl-sort { opacity: 1; color: var(--primary); }
|
|
1677
|
+
.tbl-sort:focus-visible,
|
|
1678
|
+
.tbl-filter > summary:focus-visible { outline: 2px solid var(--ring); outline-offset: 1px; opacity: 1; }
|
|
1679
|
+
|
|
1680
|
+
/* A COLUMN THAT IS FILTERING SAYS SO ON ITSELF, not only in the bar above. The
|
|
1681
|
+
bar can be scrolled off a long table; the header cannot, because it is what
|
|
1682
|
+
the reader is looking at when they wonder where a row went. */
|
|
1683
|
+
.tbl-filter.is-on > summary { opacity: 1; color: var(--primary); }
|
|
1684
|
+
.tbl-filter.is-on > summary::after { content: "•"; margin-inline-start: 0.1em; }
|
|
1685
|
+
|
|
1686
|
+
.tbl-filter-panel { padding: 0.4rem; max-block-size: 60vh; overflow-y: auto; }
|
|
1687
|
+
.tbl-filter-input {
|
|
1688
|
+
font: inherit;
|
|
1689
|
+
font-size: var(--fs-base);
|
|
1690
|
+
color: var(--foreground);
|
|
1691
|
+
background: var(--background);
|
|
1692
|
+
border: 1px solid var(--border);
|
|
1693
|
+
padding: 0.2rem 0.4rem;
|
|
1694
|
+
inline-size: 12rem;
|
|
1695
|
+
max-inline-size: 100%;
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
/* ── WHAT IS IN FORCE, AND THE WAY BACK ───────────────────────────────────────
|
|
1699
|
+
A table that is already withholding rows for a reason nobody on screen gave is
|
|
1700
|
+
the failure this prevents: an empty table and a filtered one look nearly
|
|
1701
|
+
identical. The bar names every filter and any non-default sort, and carries
|
|
1702
|
+
the only way back.
|
|
1703
|
+
|
|
1704
|
+
`[hidden]` IS NOT OPTIONAL. `[hidden] { display: none }` is a UA rule at the
|
|
1705
|
+
lowest specificity, so `display: flex` here would beat it and the bar would
|
|
1706
|
+
render on every table whether or not anything was in force — the indicator
|
|
1707
|
+
wearing the very fault it reports. */
|
|
1708
|
+
.tbl-view {
|
|
1709
|
+
display: flex;
|
|
1710
|
+
flex-wrap: wrap;
|
|
1711
|
+
align-items: center;
|
|
1712
|
+
gap: 0.4rem;
|
|
1713
|
+
margin-block-end: 0.6rem;
|
|
1714
|
+
padding: 0.35rem 0.6rem;
|
|
1715
|
+
font-size: var(--fs-base);
|
|
1716
|
+
border-inline-start: 3px solid var(--primary);
|
|
1717
|
+
background: color-mix(in srgb, var(--primary) 10%, var(--background));
|
|
1718
|
+
}
|
|
1719
|
+
.tbl-view[hidden] { display: none; }
|
|
1720
|
+
.tbl-view-lead { color: var(--primary); font-weight: 700; }
|
|
1721
|
+
.tbl-view-lead::before { content: "▸ "; }
|
|
1722
|
+
.tbl-view-chip {
|
|
1723
|
+
border: 1px solid var(--border);
|
|
1724
|
+
background: var(--card);
|
|
1725
|
+
padding: 0.05rem 0.4rem;
|
|
1726
|
+
white-space: nowrap;
|
|
1727
|
+
}
|
|
1728
|
+
/* The way out sits at the far end of the line it undoes, and falls in beside the
|
|
1729
|
+
chips when the row wraps on a phone rather than stranding itself on a line. */
|
|
1730
|
+
.tbl-view [data-view-reset] { margin-inline-start: auto; }
|
|
1731
|
+
|
|
1732
|
+
@media (pointer: coarse) {
|
|
1733
|
+
.tbl-sort, .tbl-filter > summary { min-inline-size: 44px; min-block-size: 44px;
|
|
1734
|
+
display: inline-flex; align-items: center; justify-content: center; }
|
|
1735
|
+
}
|
|
1736
|
+
@media print {
|
|
1737
|
+
.tbl-toolbar, .tbl-tools { display: none !important; }
|
|
1738
|
+
/* The bar PRINTS. A printed table that is silently filtered is the same
|
|
1739
|
+
failure on paper, with no way to interrogate it at all. */
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1623
1742
|
/* ===== chrome.css ===== */
|
|
1624
1743
|
/*
|
|
1625
1744
|
* chrome.css — the app chrome kit: sticky header bar, fixed status footer,
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
/*! danieldeusing-design v0.
|
|
2
|
-
*,*::before,*::after{box-sizing: border-box}*{margin: 0}html{-webkit-text-size-adjust: 100%;text-size-adjust: 100%;tab-size: 4}body{min-height: 100vh;min-height: 100dvh;line-height: 1.5}img,picture,video,canvas,svg{display: block;max-width: 100%}input,button,textarea,select{font: inherit;color: inherit}p,h1,h2,h3,h4,h5,h6{overflow-wrap: break-word}ul,ol{list-style: none;padding: 0}a{color: inherit;text-decoration: none}table{border-collapse: collapse}:root{--background: #f5efe2;--foreground: #43352a;--card: #efe7d4;--card-foreground: #43352a;--popover: #efe7d4;--popover-foreground: #43352a;--primary: #8a4516;--primary-foreground: #f8f3e8;--secondary: #ece3cf;--secondary-foreground: #43352a;--muted: #ece3cf;--muted-foreground: #71614e;--accent: #8a4516;--accent-foreground: #f8f3e8;--destructive: #a02c2c;--success: #1a6b2e;--warning: #845600;--info: #1c5f9e;--pending: #5f3dc4;--border: #d9cdb6;--input: #d9cdb6;--ring: #8a4516;--glow: rgba(160,82,45,0.1);--glow-soft: rgba(160,82,45,0.05);--scanline-opacity: 0.15;--radius: 0rem;--font-mono: "JetBrains Mono Variable","JetBrains Mono","Menlo",monospace}html[data-theme="green"]{--background: #020604;--foreground: #4fdd7d;--card: #04110a;--card-foreground: #4fdd7d;--popover: #04110a;--popover-foreground: #4fdd7d;--primary: #33ff66;--primary-foreground: #02160a;--secondary: #071509;--secondary-foreground: #4fdd7d;--muted: #071509;--muted-foreground: #3da45e;--accent: #33ff66;--accent-foreground: #02160a;--destructive: #ff5c5c;--success: #4ade80;--warning: #f5b32b;--info: #4dabf7;--pending: #b197fc;--border: #1c4a2c;--input: #1c4a2c;--ring: #33ff66;--glow: rgba(51,255,102,0.35);--glow-soft: rgba(51,255,102,0.12);--scanline-opacity: 0.5}html[data-theme="mono"]{--background: #050505;--foreground: #d4d4d4;--card: #0d0d0d;--card-foreground: #d4d4d4;--popover: #0d0d0d;--popover-foreground: #d4d4d4;--primary: #ffffff;--primary-foreground: #050505;--secondary: #111111;--secondary-foreground: #d4d4d4;--muted: #111111;--muted-foreground: #8a8a8a;--accent: #ffffff;--accent-foreground: #050505;--destructive: #ff5c5c;--success: #4ade80;--warning: #f5b32b;--info: #4dabf7;--pending: #b197fc;--border: #333333;--input: #333333;--ring: #ffffff;--glow: rgba(255,255,255,0.25);--glow-soft: rgba(255,255,255,0.08);--scanline-opacity: 0.45}html[data-theme="paper"]{--background: #fafafa;--foreground: #1f1f1f;--card: #f1f1f1;--card-foreground: #1f1f1f;--popover: #f1f1f1;--popover-foreground: #1f1f1f;--primary: #000000;--primary-foreground: #fafafa;--secondary: #efefef;--secondary-foreground: #1f1f1f;--muted: #efefef;--muted-foreground: #595959;--accent: #000000;--accent-foreground: #fafafa;--destructive: #b3261e;--success: #1a7031;--warning: #8f5d00;--info: #1c5f9e;--pending: #5f3dc4;--border: #d4d4d4;--input: #d4d4d4;--ring: #000000;--glow: rgba(0,0,0,0.06);--glow-soft: rgba(0,0,0,0.04);--scanline-opacity: 0.12}:root{--content-w: 78rem;font-size: max(1rem,calc(1rem + (100vw - 1920px) / 120));--content-pad: 1.5rem;--fs-base: 0.75rem;--fs-lg: 0.9375rem;--fs-xl: 1.125rem;--space-section: 2.5rem;--fs-2xl: 1.5rem;--lh-tight: 1.3;--lh-base: 1.5;--field-label-w: 8.5rem}html{scroll-behavior: smooth}*,*::before,*::after{border-color: var(--border)}:focus-visible{outline: 2px solid var(--ring);outline-offset: 2px}body{background: var(--background);color: var(--foreground);font-family: var(--font-mono);font-size: var(--fs-base);line-height: var(--lh-base);overflow-x: hidden;-webkit-font-smoothing: antialiased;-moz-osx-font-smoothing: grayscale}table{font-size: var(--fs-base);width: 100%}th,td{padding: 0.45rem 0.85rem;text-align: left;vertical-align: top;border-block-end: 1px solid color-mix(in srgb,var(--border) 55%,transparent)}thead th{border-block-end-color: var(--border);color: var(--muted-foreground);font-weight: 600}tbody tr:last-child>*{border-block-end: 0}tr>*:first-child{padding-inline-start: 0}tr>*:last-child{padding-inline-end: 0}tr[hidden]{display: none !important}button:not(:disabled),summary,[role="button"]{cursor: pointer}body::after{content: "";position: fixed;inset: 0;z-index: 40;pointer-events: none;background: repeating-linear-gradient( 0deg,rgba(0,0,0,0.25) 0px,rgba(0,0,0,0.25) 1px,transparent 1px,transparent 3px );opacity: var(--scanline-opacity)}::selection{background: var(--primary);color: var(--primary-foreground)}:where(section) + :where(section),:where(section.doc){margin-block-start: var(--space-section)}:where(section,.wrap,.body)>:where(h2){margin-block-start: var(--space-section)}:where(section,.wrap,.body)>:where(h3){margin-block-start: calc(var(--space-section) * 0.6)}:where(section,.wrap,.body)>:where(h2,h3):first-child{margin-block-start: 0}:where(h2,h3) + :where(p,table,.tablewrap,svg,.legend){margin-block-start: 0.6rem}:where(p) + :where(table,.tablewrap,svg,.legend){margin-block-start: 0.6rem}.glow{color: var(--primary);text-shadow: 0 0 8px var(--glow)}.glow-lg{color: var(--primary);text-shadow: 0 0 12px var(--glow)}.field-row{display: grid;grid-template-columns: var(--field-label-w) minmax(0,1fr);align-items: center;gap: 0.5rem 0.75rem;margin: 0.35rem 0}.field-row>.lbl{color: var(--muted-foreground);font-size: var(--fs-base);text-align: left}.field-row>.field-val{display: flex;flex-wrap: wrap;align-items: center;gap: 0.4rem 0.9rem;min-width: 0}@media (max-width: 40rem){.field-row{grid-template-columns: 1fr;gap: 0.2rem}}.prompt{font-size: var(--fs-base);font-weight: 700;color: var(--muted-foreground)}.prompt::before{content: "$ ";color: var(--primary)}.comment::before{content: "# ";color: var(--border)}.cursor-block{display: inline-block;width: 0.55em;height: 1em;margin-left: 8px;background: var(--primary);vertical-align: baseline;transform: translateY(0.12em);animation: ddd-blink 1.1s step-end infinite}@keyframes ddd-blink{50%{opacity: 0}}.btn-terminal{display: inline-block;background: var(--primary);color: var(--primary-foreground);font-weight: 700;padding: 12px 24px;box-shadow: 0 0 16px var(--glow);transition: box-shadow 0.15s ease,transform 0.15s ease}.btn-terminal::before{content: "> "}.btn-terminal--ghost{background: transparent;color: var(--primary);border: 1px solid var(--border);box-shadow: none;font-weight: 400}.btn-terminal--ghost::before{content: ""}.btn-terminal--ghost:hover{border-color: var(--primary);background: color-mix(in srgb,var(--primary) 10%,transparent);box-shadow: none}.btn-terminal:hover{box-shadow: 0 0 28px var(--glow);transform: translateY(-2px)}.btn-terminal:disabled{opacity: 0.45;cursor: default;box-shadow: none;transform: none}.btn-terminal--compact{padding: 0.28rem 0.7rem;font-size: var(--fs-base)}.btn-terminal--destructive{display: inline-flex;align-items: center;justify-content: center;padding: 0.3rem;color: var(--destructive);border-color: color-mix(in srgb,var(--destructive) 45%,var(--border))}.btn-terminal--destructive::before{content: ""}.btn-terminal--destructive::after{content: "";display: block;width: 0.875rem;height: 0.875rem;background: currentColor;-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M9 3h6v3H9zM4 6h16v2H4zM6 9h12l-1 12H7z'/%3E%3C/svg%3E") center / contain no-repeat;mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M9 3h6v3H9zM4 6h16v2H4zM6 9h12l-1 12H7z'/%3E%3C/svg%3E") center / contain no-repeat}.btn-terminal--destructive:hover{border-color: var(--destructive);background: color-mix(in srgb,var(--destructive) 12%,transparent);box-shadow: none}@media (pointer: coarse){.btn-terminal--destructive{min-width: 44px;min-height: 44px}}.btn-terminal--edit{display: inline-flex;align-items: center;justify-content: center;padding: 0.3rem}.btn-terminal--edit::before{content: ""}.btn-terminal--edit::after{content: "";display: block;width: 0.875rem;height: 0.875rem;background: currentColor;-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04l-3.75-3.75-1.83 1.83 3.75 3.75 1.83-1.83z'/%3E%3C/svg%3E") center / contain no-repeat;mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04l-3.75-3.75-1.83 1.83 3.75 3.75 1.83-1.83z'/%3E%3C/svg%3E") center / contain no-repeat}@media (pointer: coarse){.btn-terminal--edit{min-width: 44px;min-height: 44px}}.link-quiet{color: var(--muted-foreground);text-decoration: underline;text-underline-offset: 4px;transition: color 0.15s ease}.link-quiet:hover{color: var(--primary)}.anim-toggle{display: inline-flex;align-items: center;gap: 6px;font: inherit;color: var(--muted-foreground);background: none;border: 0;cursor: pointer;transition: color 0.15s ease}.anim-toggle:hover{color: var(--primary)}.anim-toggle[aria-pressed="true"] [data-anim-box]{color: var(--primary)}.dd-dot{display: inline-block;width: 11px;height: 11px;flex: none;border-radius: 50%;background: currentColor}.dd-flag{display: inline-block;width: 15px;height: 10px;flex: none;border: 1px solid var(--border);background-size: cover;background-repeat: no-repeat}.dd-flag-de{background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 18 12'><rect width='18' height='4' fill='%23262626'/><rect y='4' width='18' height='4' fill='%23c83232'/><rect y='8' width='18' height='4' fill='%23e8be32'/></svg>")}.dd-flag-en{background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 18 12'><rect width='18' height='12' fill='%23b22234'/><rect y='4' width='18' height='4' fill='%23ececec'/><rect width='8' height='6' fill='%233c3b6e'/></svg>")}.dd-flag-pt{background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 18 12'><rect width='18' height='12' fill='%231c9148'/><polygon points='9,1.5 15.5,6 9,10.5 2.5,6' fill='%23e8c832'/><rect x='8' y='5' width='2' height='2' fill='%232c3f87'/></svg>")}.dd-flag-es{background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 18 12'><rect width='6' height='12' fill='%2315734c'/><rect x='6' width='6' height='12' fill='%23ececec'/><rect x='12' width='6' height='12' fill='%23bf2233'/><rect x='8' y='5' width='2' height='2' fill='%237a5c3a'/></svg>")}.ascii-rule{margin: 1rem 0;border: 0;color: var(--border);line-height: 1;white-space: nowrap;overflow: hidden;user-select: none}.ascii-rule::before{content: "────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────"}.card-terminal{background: var(--card);border: 1px solid var(--border);transition: border-color 0.15s ease,box-shadow 0.15s ease}.card-terminal:hover{border-color: var(--primary);box-shadow: 0 0 20px var(--glow-soft)}.dropdown{position: relative}.dropdown>summary{display: inline-flex;align-items: center;gap: 6px;cursor: pointer;user-select: none;list-style: none;color: var(--muted-foreground);transition: color 0.15s ease}.dropdown>summary::-webkit-details-marker{display: none}.dropdown>summary:hover,.dropdown[open]>summary{color: var(--primary)}.dropdown-panel{position: absolute;right: 0;bottom: calc(100% + 12px);z-index: 50;min-width: 128px;margin: 0;padding: 4px 0;list-style: none;background: var(--card);border: 1px solid var(--border);border-radius: var(--radius);box-shadow: 0 0 20px var(--glow-soft)}.dropdown-panel--down{top: calc(100% + 8px);bottom: auto;left: 0;right: auto}.dropdown-item{display: flex;align-items: center;gap: 8px;width: 100%;padding: 5px 14px;text-align: left;font: inherit;line-height: 1.5;background: none;border: 0;color: var(--muted-foreground);cursor: pointer;white-space: nowrap;transition: color 0.15s ease}.dropdown-item:hover{color: var(--primary);background: var(--muted)}.dropdown-item[aria-current="true"],html:not([data-theme]) .dropdown-item[data-theme-value="warm"],html[data-theme="green"] .dropdown-item[data-theme-value="green"],html[data-theme="mono"] .dropdown-item[data-theme-value="mono"],html[data-theme="paper"] .dropdown-item[data-theme-value="paper"],html[data-theme="warm"] .dropdown-item[data-theme-value="warm"]{color: var(--primary);text-shadow: 0 0 8px var(--glow)}input[type="text"],input[type="search"],input[type="url"],input[type="email"],input[type="password"],input[type="number"],input[type="tel"],input[type="date"],input:not([type]),textarea{max-width: 100%;padding: 0.35rem 0.5rem;font: inherit;font-size: var(--fs-base);line-height: var(--lh-tight);color: var(--foreground);background-color: transparent;border: 1px solid color-mix(in srgb,var(--foreground) 60%,transparent);transition: border-color 0.15s ease}textarea{line-height: var(--lh-base)}input[type="text"]:hover,input[type="search"]:hover,input[type="url"]:hover,input[type="email"]:hover,input[type="password"]:hover,input[type="number"]:hover,input[type="tel"]:hover,input[type="date"]:hover,input:not([type]):hover,textarea:hover{border-color: var(--primary)}input:disabled,textarea:disabled{opacity: 0.55;cursor: not-allowed}select,.select-trigger{appearance: none;-webkit-appearance: none;max-width: 100%;padding: 0.35rem 1.45rem 0.35rem 0.5rem;font: inherit;font-size: var(--fs-base);line-height: var(--lh-tight);text-align: left;color: var(--foreground);background-color: transparent;background-image: linear-gradient(45deg,transparent 50%,currentColor 50%),linear-gradient(135deg,currentColor 50%,transparent 50%);background-position: right 0.78rem center,right 0.5rem center;background-size: 0.28rem 0.28rem;background-repeat: no-repeat;border: 1px solid color-mix(in srgb,var(--foreground) 60%,transparent);cursor: pointer;transition: border-color 0.15s ease}select:hover,.select-trigger:hover,.select-trigger[aria-expanded="true"]{border-color: var(--primary)}select:disabled,.select-trigger:disabled{opacity: 0.45;cursor: default}.select-trigger[aria-expanded="true"]{background-image: linear-gradient(135deg,transparent 50%,currentColor 50%),linear-gradient(45deg,currentColor 50%,transparent 50%)}.select-field{position: relative;display: inline-flex;max-width: 100%;vertical-align: middle}.select-field>select{position: absolute;inset: 0;width: 100%;height: 100%;opacity: 0;pointer-events: none}.select-trigger{display: inline-flex;align-items: center;width: 100%}.select-value{min-width: 0;overflow: hidden;text-overflow: ellipsis;white-space: nowrap}.select-value::after{content: "\200b"}.select-panel{position: fixed;z-index: 60;margin: 0;padding: 0.15rem 0;overflow-y: auto;overscroll-behavior: contain;list-style: none;font-size: var(--fs-base);line-height: var(--lh-tight);color: var(--popover-foreground);background: var(--popover);border: 1px solid color-mix(in srgb,var(--foreground) 60%,transparent);box-shadow: 0 0 20px var(--glow-soft)}.select-option{display: flex;align-items: center;gap: 0.4rem;padding: 0.28rem 0.9rem 0.28rem 0.55rem;border-inline-start: 2px solid transparent;white-space: nowrap;cursor: pointer}.select-option:hover,.select-option[data-active="true"]{background: var(--muted)}.select-option[aria-selected="true"]{color: var(--primary);font-weight: 700;border-inline-start-color: var(--primary)}.select-option[aria-disabled="true"]{color: color-mix(in srgb,var(--popover-foreground) 65%,var(--popover));cursor: default}.select-option[aria-disabled="true"]:hover{background: none}.select-group{padding: 0.35rem 0.9rem 0.15rem;color: var(--muted-foreground);font-size: var(--fs-base);text-transform: uppercase;letter-spacing: 0.06em}.table-pager{display: flex;flex-wrap: wrap;align-items: center;gap: 0.45rem 0.9rem;margin-block-start: 0.6rem;font-size: var(--fs-base)}.table-pager-status{margin: 0;color: var(--muted-foreground)}.table-pager-size{display: inline-flex;align-items: center;gap: 0.4rem;margin-inline-start: auto;color: var(--muted-foreground)}.table-pager-nav{display: inline-flex;gap: 0.4rem}.eli5{margin: 1.25em 0;padding: 0.7em 1em;color: var(--foreground);background: color-mix(in srgb,var(--primary) 7%,var(--card));border: 1px solid var(--border);border-left: 3px solid var(--primary);border-radius: var(--radius);font-size: var(--fs-base);line-height: 1.6}.eli5::before{content: "ELI5";display: inline-flex;align-items: center;justify-content: center;margin-right: 0.6em;vertical-align: 0.08em;font-family: var(--font-mono);font-size: 0.72em;font-weight: 700;line-height: 1;letter-spacing: 0.04em;padding: 0.34em 0.6em;border-radius: var(--radius);background: var(--destructive);color: var(--primary-foreground)}.eli5-term{font-weight: 700;color: var(--primary)}html.anim-off *,html.anim-off *::before,html.anim-off *::after{animation: none !important}html.anim-off .cursor-block{display: none}@media (prefers-reduced-motion: no-preference){html.term-anim [data-term] .prompt:not(.term-live),html.term-anim [data-term] .prompt:not(.term-live)::before{color: transparent}html.term-anim [data-term] [data-term-out]:not(.term-show){visibility: hidden}html.term-anim [data-term] [data-term-out].term-show{animation: ddd-term-output 0.2s steps(3,end) both}@keyframes ddd-term-output{from{opacity: 0}}html.term-anim [data-term] [data-term-out].term-show>*{animation: ddd-term-output 0.25s steps(3,end) both}html.term-anim [data-term] [data-term-out].term-show>*:nth-child(2){animation-delay: 0.09s}html.term-anim [data-term] [data-term-out].term-show>*:nth-child(3){animation-delay: 0.18s}html.term-anim [data-term] [data-term-out].term-show>*:nth-child(4){animation-delay: 0.27s}html.term-anim [data-term] [data-term-out].term-show>*:nth-child(5){animation-delay: 0.36s}html.term-anim [data-term] [data-term-out].term-show>*:nth-child(6){animation-delay: 0.45s}html.term-anim [data-term] [data-term-out].term-show>*:nth-child(7){animation-delay: 0.54s}html.term-anim [data-term] [data-term-out].term-show>*:nth-child(8){animation-delay: 0.63s}html.term-anim [data-term] [data-term-out].term-show>*:nth-child(9){animation-delay: 0.72s}html.term-anim [data-term] [data-term-out].term-show>*:nth-child(10){animation-delay: 0.81s}html.term-anim [data-term] [data-term-out].term-show>*:nth-child(n + 11){animation-delay: 0.9s}.term-caret{display: inline-block;width: 0.55em;height: 1em;margin-left: 2px;background: var(--primary);vertical-align: baseline;transform: translateY(0.12em)}}.tabs{display: flex;flex-wrap: wrap;gap: 0.15rem;margin: 1.35rem 0 0;border-bottom: 2px solid color-mix(in srgb,var(--border) 80%,transparent)}.tab{background: transparent;border: 0;margin-bottom: -2px;font: inherit;font-size: var(--fs-base);cursor: pointer;padding: 0.5rem 0.95rem;color: var(--muted-foreground);transition: background 0.12s ease,color 0.12s ease}.tab:hover{color: var(--primary);background: color-mix(in srgb,var(--primary) 10%,transparent)}.tab[aria-selected="true"]{background: var(--primary);color: var(--background);font-weight: 700}.tab:focus-visible{outline: 2px solid color-mix(in srgb,var(--primary) 55%,transparent);outline-offset: -2px}.tab--info{margin-inline-start: auto}@media (max-width: 40rem){.tab--info{margin-inline-start: 0}}@media (pointer: coarse),(max-width: 40rem){.tab{padding: 0.7rem 0.8rem;min-height: 44px}}section.doc.tab-panel{margin-top: 1.35rem}details.fold{border-top: 1px solid color-mix(in srgb,currentColor 15%,transparent)}details.fold>summary{display: flex;align-items: center;gap: 0.5rem;cursor: pointer;list-style: none;padding: 0.55rem 0;font-size: var(--fs-base);color: var(--muted-foreground)}details.fold>summary::-webkit-details-marker{display: none}details.fold>summary::before{content: "\25B8";flex: none;opacity: 0.6}details.fold[open]>summary::before{content: "\25BE"}details.fold>summary:hover{color: var(--primary)}details.fold>summary:focus-visible{outline: 2px solid color-mix(in srgb,currentColor 45%,transparent);outline-offset: 2px}.fold-body{font-size: var(--fs-base);padding: 0 0 0.7rem 1.1rem}.fold-body>* + *{margin-top: 0.6rem}.legend{list-style: none;padding: 0;margin: 0.7rem 0 0;display: grid;gap: 0.4rem 1.4rem;font-size: var(--fs-base)}@media (min-width: 52rem){.legend{grid-template-columns: repeat(2,minmax(0,1fr))}}.legend>li{line-height: 1.6}.legend>li>:first-child{margin-right: 0.45rem}.dgm-zoomable{position: relative;cursor: zoom-in}.dgm-zoomable::after{content: "⤢";position: absolute;top: 0.4rem;right: 0.5rem;font-size: 0.85rem;line-height: 1;opacity: 0.45;pointer-events: none;color: var(--muted-foreground)}.dgm-zoomable:hover::after,.dgm-zoomable:focus-visible::after{opacity: 1;color: var(--primary)}.dgm-zoomable:focus-visible{outline: 2px solid var(--primary);outline-offset: 3px}html.dgm-locked,html.dgm-locked body{overflow: hidden}.dgm-overlay{display: none;position: fixed;inset: 0;z-index: 90;background: color-mix(in srgb,var(--background) 96%,transparent);flex-direction: column}.dgm-overlay.open{display: flex}.dgm-bar{flex: none;display: flex;align-items: center;justify-content: flex-end;gap: 0.4rem;padding: 0.5rem 0.75rem;border-bottom: 1px solid var(--border);background: var(--card)}.dgm-btn{font: inherit;font-size: var(--fs-base);cursor: pointer;padding: 0.3rem 0.7rem;min-height: 32px;background: transparent;border: 1px solid var(--border);color: var(--foreground)}.dgm-btn:hover{border-color: var(--primary);color: var(--primary)}.dgm-btn:focus-visible{outline: 2px solid var(--primary);outline-offset: 1px}.dgm-close{border-color: color-mix(in srgb,var(--destructive) 55%,transparent);color: var(--destructive)}.dgm-close:hover{border-color: var(--destructive);color: var(--destructive)}.dgm-stage{flex: 1;overflow: hidden;display: grid;place-items: center;cursor: grab;touch-action: none}.dgm-stage.is-grabbing{cursor: grabbing}.dgm-art{transform-origin: center center;will-change: transform}.dgm-art>svg,.dgm-art>img{display: block;max-width: none;max-height: none}@media print{.dgm-overlay,.dgm-zoomable::after{display: none !important}}.minimap{position: fixed;left: 0;top: var(--ls-nav-top,3rem);bottom: var(--ls-nav-bottom,2.2rem);z-index: 20;display: none;flex-direction: column;justify-content: center;gap: 0.5rem;width: 2rem;padding: 0.75rem 0 0.75rem 0.75rem;overflow-y: auto;overscroll-behavior: contain;scrollbar-width: none}.minimap::-webkit-scrollbar{display: none}@media (min-width: 64rem){.minimap{display: flex}}.minimap-bar{flex: 0 0 auto;width: var(--minimap-bar-w,100%);height: 2px;padding: 0;border: 0;border-radius: 1px;background: var(--border);cursor: pointer;transition: background 0.15s ease,transform 0.15s ease;transform-origin: left center}.minimap-bar:hover{background: var(--muted-foreground);transform: scaleY(2)}.minimap-bar[data-tip]{cursor: pointer}.minimap-bar:focus-visible{outline: 2px solid var(--ring);outline-offset: 3px}.minimap-bar.active{background: var(--primary);transform: scaleY(2)}@media print{.minimap{display: none !important}}.wrap{max-width: var(--content-w);margin-inline: auto;padding-inline: var(--content-pad)}.tablewrap{position: relative;overflow-x: auto;overflow-y: auto;max-block-size: var(--tablewrap-max-h,none)}.tablewrap::after{content: "";position: absolute;inset-block: 0;right: 0;width: 1.5rem;pointer-events: none;opacity: 0.9;background: linear-gradient(to right,transparent,var(--tablewrap-fade,var(--background,transparent)))}.tablewrap thead th{position: sticky;inset-block-start: 0;z-index: 1;background: var(--background,#f5efe2)}.tickstrip:not(:empty){--tablewrap-fade: var(--card);border: 1px solid var(--border);background: var(--card);margin: 1.6rem 0 1.5rem}.ticktable{width: 100%;border-collapse: collapse;font-size: var(--fs-base)}.ticktable td{padding: 0.5rem 0.75rem;vertical-align: baseline;border: 0;white-space: nowrap}.ticktable td.tick-stats{width: 100%;white-space: normal;text-align: right}.tick{border-bottom: 1px solid color-mix(in srgb,var(--border) 55%,transparent)}.tick:last-child{border-bottom: 0}.tick-dot{font-size: 0.7em;width: 1.5rem;padding-right: 0 !important;border-left: 3px solid transparent;padding-left: 0.6rem !important}.tick--ok .tick-dot{color: var(--success);border-left-color: color-mix(in srgb,var(--success) 45%,transparent)}.tick--stale .tick-dot{color: var(--destructive);border-left-color: var(--destructive)}.tick--never .tick-dot{color: var(--muted-foreground);border-left-color: color-mix(in srgb,var(--muted-foreground) 35%,transparent)}.tick-name{font-weight: 700;color: var(--foreground);letter-spacing: 0.01em}.tick-last,.tick-next{font-variant-numeric: tabular-nums;color: var(--muted-foreground)}.tick-next{opacity: 0.72}.tick--stale .tick-last{color: var(--destructive);font-weight: 700}.tick--never .tick-last{font-style: italic}.tick-stats{color: var(--muted-foreground)}.tick-stats b{font-weight: 700;color: var(--primary);font-variant-numeric: tabular-nums}.tick-sep{font-style: normal;opacity: 0.35;margin: 0 0.5rem}.tick--stale{background: color-mix(in srgb,var(--destructive) 8%,transparent)}@media (max-width: 40rem){.ticktable,.ticktable tbody,.tick,.ticktable td{display: block}.tick{padding: 0.4rem 0.5rem}.ticktable td{display: inline-block;padding: 0.1rem 0.5rem 0.1rem 0}.ticktable td.tick-stats{display: block;width: auto;text-align: left}}.skip-link{position: absolute;left: -999px;top: 0;z-index: 100;background: var(--card);border: 1px solid var(--primary);padding: 0.5rem 1rem;color: var(--primary)}.skip-link:focus{left: 0.5rem;top: 0.5rem}.visually-hidden{position: absolute;width: 1px;height: 1px;overflow: hidden;clip: rect(0 0 0 0);white-space: nowrap}header.bar{position: sticky;top: 0;z-index: 30;display: flex;align-items: center;justify-content: space-between;gap: 1rem;padding: 0.6rem 1.25rem;border-bottom: 1px solid var(--border);background: var(--card)}header.bar .brand{display: inline-flex;align-items: center;font-weight: 700;font-size: 1rem}header.bar .brand,header.bar .brand a{color: inherit;text-decoration: none}header.bar .dropdown summary{padding: 0.6rem 0.4rem}.bar-right{display: flex;align-items: center;gap: 1rem;flex-shrink: 0}footer.status{position: fixed;inset-inline: 0;bottom: 0;z-index: 50;min-height: 2rem;display: flex;align-items: center;justify-content: space-between;gap: 1rem;padding: 0.35rem 1.25rem;border-top: 1px solid var(--border);background: var(--card);font-size: var(--fs-base)}footer.status .dropdown summary,footer.status .anim-toggle{padding: 0.55rem 0.4rem}.status-left{color: var(--muted-foreground);min-width: 0;overflow: hidden;text-overflow: ellipsis;white-space: nowrap}.status-right{display: flex;align-items: center;gap: 1rem;flex-shrink: 0}.sep{width: 1px;height: 0.9rem;background: var(--border)}.doc-link{color: var(--muted-foreground);text-decoration: none;transition: color 0.15s ease}.doc-link:hover{color: var(--primary)}.doc-link--forward{color: var(--primary);white-space: nowrap}.doc-link--forward:hover{color: var(--primary);text-decoration: underline}.ls-group{display: block;padding: 0.55rem 0 0.15rem;font-size: var(--fs-base);letter-spacing: 0.08em;color: var(--muted-foreground);opacity: 0.75}.ls-panel>li:first-child .ls-group{padding-top: 0}.ls-row{display: flex;align-items: baseline;gap: 0.9rem}.ls-perm{color: color-mix(in srgb,var(--muted-foreground) 75%,var(--card));font-size: var(--fs-base);letter-spacing: 0}.ls-row--sub .ls-name{margin-left: 0.55rem;padding-left: 0.75rem;border-left: 1px solid color-mix(in srgb,var(--muted-foreground) 40%,transparent)}.ls-row--sub2 .ls-name{margin-left: 1.7rem}.ls-panel .ls-name{color: var(--muted-foreground)}.ls-panel .ls-row--dir .ls-name{color: var(--primary);font-weight: 700}.ls-row[aria-current="page"]{background: color-mix(in srgb,var(--primary) 12%,transparent);box-shadow: inset 3px 0 0 0 var(--primary)}.ls-row[aria-current="page"] .ls-name{color: var(--primary);font-weight: 700;text-shadow: 0 0 8px var(--glow)}.ls-row[aria-current="page"] .ls-perm{color: color-mix(in srgb,var(--primary) 65%,transparent)}.ls-row[aria-current="page"]::after{content: "←";margin-left: auto;color: var(--primary)}.dropdown-panel.ls-panel{min-width: 17rem;left: auto !important;right: 0}:root{--ls-nav-w: 17rem;--ls-nav-top: 3rem;--ls-nav-bottom: 2.2rem}@media (min-width: 48.0625rem){html{--ls-nav-inset: 0rem}html:has(.ls-nav):not([data-ls-nav="off"]){--ls-nav-inset: var(--ls-nav-w)}body{padding-right: var(--ls-nav-inset)}header.bar,.bleed-rail{margin-right: calc(-1 * var(--ls-nav-inset))}.ls-nav{position: fixed;top: var(--ls-nav-top);right: 0;bottom: var(--ls-nav-bottom);width: var(--ls-nav-w);z-index: 25;display: flex;flex-direction: column;overflow: hidden;background: var(--card);border-left: 1px solid var(--border)}html:not(.anim-off) .ls-nav{transition: transform 0.18s ease}html[data-ls-nav="off"] .ls-nav{transform: translateX(100%)}.ls-nav-head{display: inline-flex;align-items: center;gap: 0.5rem;margin-left: 0.75rem}.ls-nav-title{font-weight: 700;font-size: var(--fs-base)}.ls-nav .ls-panel{flex: 1;min-height: 0;overflow-y: auto;list-style: none;margin: 0;padding: 0.5rem 0 1rem}.ls-nav-toggle{background: none;border: 0;font: inherit;cursor: pointer;line-height: 1;color: var(--muted-foreground);padding: 0.4rem 0.55rem}.ls-nav-toggle::after{content: "\00BB"}html[data-ls-nav="off"] .ls-nav-toggle::after{content: "\00AB"}.ls-nav-toggle:hover{color: var(--primary)}.ls-nav-toggle:focus-visible{outline: 2px solid color-mix(in srgb,var(--primary) 55%,transparent);outline-offset: -2px}}.nav-burger{display: none;background: none;border: 0;color: var(--muted-foreground);cursor: pointer;padding: 0.55rem;margin: -0.3rem -0.3rem -0.3rem 0}.nav-burger:hover{color: var(--primary)}.mobile-nav{display: none;list-style: none;margin: 0;padding: 0}.mobile-footer{display: none}@media (max-width: 48rem){.nav-burger{display: inline-flex;align-items: center}.site-nav{display: none;position: absolute;top: 100%;left: 0;right: 0;z-index: 40;background: var(--card);border-bottom: 1px solid var(--border);padding: 0.4rem 1.25rem 0.9rem;max-height: calc(100vh - 6rem);overflow-y: auto}.site-nav.open{display: block}.site-nav>.ls-nav,.mobile-nav{display: flex;flex-direction: column}.site-nav:has(>.ls-nav) .mobile-nav{display: none}.site-nav>.ls-nav .ls-row{align-items: center;min-height: 44px;border-bottom: 1px solid var(--border)}.site-nav>.ls-nav .ls-row .ls-name{display: flex;align-items: center;flex: 1;min-height: 44px}.site-nav>.ls-nav .ls-row:last-child{border-bottom: 0}.mobile-nav .mobile-item{display: flex;align-items: baseline;gap: 0.9rem;padding: 0.65rem 0;text-decoration: none;color: var(--foreground);font-size: var(--fs-base);border-bottom: 1px solid var(--border)}.mobile-nav li:last-child .mobile-item{border-bottom: 0}.mobile-nav .mobile-item:active .ls-name,.mobile-nav .mobile-item:hover .ls-name{color: var(--primary)}.ls-nav-toggle{display: none}footer.status{display: none}.mobile-footer{display: flex;flex-direction: column;gap: 0.55rem;margin-top: 0.8rem;border-top: 1px solid var(--border);padding-top: 0.8rem;font-size: var(--fs-base)}.mobile-footer .doc-link{padding: 0.2rem 0}.mobile-footer .mobile-theme summary{list-style: none;display: flex;align-items: center;gap: 0.5rem;color: var(--muted-foreground);cursor: pointer;padding: 0.2rem 0}.mobile-footer .mobile-theme summary::-webkit-details-marker{display: none}.mobile-footer .mf-chev{margin-left: auto;opacity: 0.7}.mobile-footer .mf-panel{display: flex;flex-direction: column;margin-top: 0.3rem}.mobile-footer .mf-panel .dropdown-item{text-align: left;padding: 0.4rem 0.6rem}.mobile-footer .anim-toggle{display: inline-flex;gap: 0.5rem;align-items: center;background: none;border: 0;padding: 0.2rem 0;color: var(--muted-foreground);font: inherit;cursor: pointer}}[data-tip]{cursor: help}span[data-tip]::after,th[data-tip]::after,button[data-tip]::after{content: "\FE0E\24D8";margin-inline-start: 0.35em;color: var(--muted-foreground);font-size: 0.9em;font-weight: 400;user-select: none;vertical-align: baseline}.minimap-bar[data-tip]::after,[data-tip][data-tip-bare]::after{content: none}#ddtip{position: fixed;z-index: 9999;display: none;max-width: 340px;background: var(--background);color: var(--foreground);border: 1px solid var(--muted-foreground);border-radius: var(--radius);padding: 7px 10px;font-size: var(--fs-base);line-height: 1.45;font-weight: 400;text-align: left;white-space: normal;pointer-events: none;box-shadow: 0 4px 14px rgba(0,0,0,0.25)}@media print{@page{margin: 14mm 12mm}:root,html,html[data-theme]{--background: #ffffff;--foreground: #111111;--card: #ffffff;--card-foreground: #111111;--popover: #ffffff;--popover-foreground: #111111;--primary: #000000;--primary-foreground: #ffffff;--secondary: #ffffff;--secondary-foreground: #111111;--muted: #ffffff;--muted-foreground: #444444;--accent: #000000;--accent-foreground: #ffffff;--destructive: #7a1c1c;--border: #9a9a9a;--input: #9a9a9a;--ring: #000000;--glow: transparent;--glow-soft: transparent;--scanline-opacity: 0}html{zoom: 1 !important;font-size: 16px;scroll-behavior: auto}body{background: #ffffff;color: var(--foreground);font-size: 12px;line-height: 1.45;overflow: visible}body::after{display: none !important}*,*::before,*::after{animation: none !important;transition: none !important;text-shadow: none !important;box-shadow: none !important}html.term-anim [data-term] [data-term-out],html.term-anim [data-term] [data-term-out]:not(.term-show),html.term-anim [data-term] [data-term-out]>*{visibility: visible !important;opacity: 1 !important}html.term-anim [data-term] .prompt:not(.term-live){color: var(--muted-foreground) !important}html.term-anim [data-term] .prompt:not(.term-live)::before{color: var(--primary) !important}header.bar,footer.status,.site-nav,.nav-burger,.mobile-nav,.mobile-footer,.skip-link,.dropdown,.dropdown-panel,.select-panel,.anim-toggle,.cursor-block,.term-caret,.ascii-rule,.toc,#ddtip{display: none !important}body{padding-right: 0 !important}[data-tip]{cursor: auto}span[data-tip]::after,th[data-tip]::after,button[data-tip]::after{content: none}.wrap{max-width: none !important;margin: 0 !important;padding: 0 !important}.layout{display: block !important}.content{min-width: 0}h1,.title{font-size: 1.5rem !important}h2{font-size: 1.15rem !important}h3,.sub{font-size: 1rem !important}h4{font-size: 0.9rem !important}.prompt{font-size: 0.7rem}h1,h2,h3,h4,.prompt,.toc-label{break-after: avoid-page;break-inside: avoid}p,li,blockquote{orphans: 3;widows: 3}pre,figure,blockquote,tr,img,.eli5,.card-terminal{break-inside: avoid}svg{break-inside: auto}thead{display: table-header-group}.tablewrap,.table-scroll,pre,.diagram{overflow: visible !important;max-width: 100% !important}pre{white-space: pre-wrap !important;overflow-wrap: anywhere;font-size: 0.75rem !important;background: transparent !important}pre.mermaid[data-processed]{white-space: normal !important}code{background: transparent !important;overflow-wrap: anywhere}table{width: 100% !important;table-layout: auto;border-collapse: collapse;font-size: 0.75rem !important}th,td{overflow-wrap: break-word;white-space: normal !important}td code,th code,td a,th a{overflow-wrap: anywhere}a{color: inherit;text-decoration: underline;text-underline-offset: 2px}a.card-terminal,a[class*="card"]{text-decoration: none}.glow,.glow-lg{color: inherit}.btn-terminal{border: 1px solid var(--border);background: transparent;color: var(--foreground)}.mermaid svg{display: block;margin-inline: auto;width: auto !important;height: auto !important;max-width: 100% !important;max-height: 235mm !important}.mermaid svg .node rect,.mermaid svg .node polygon,.mermaid svg .node circle,.mermaid svg .node path,.mermaid svg .label-container,.mermaid svg .cluster rect,.mermaid svg .actor,.mermaid svg .note{fill: #ffffff !important;stroke: #333333 !important}.mermaid svg .edgePath path,.mermaid svg .flowchart-link,.mermaid svg line,.mermaid svg .messageLine0,.mermaid svg .messageLine1{stroke: #333333 !important}.mermaid svg .arrowheadPath,.mermaid svg marker path,.mermaid svg marker polygon{fill: #333333 !important;stroke: #333333 !important}.mermaid svg text,.mermaid svg .nodeLabel,.mermaid svg .edgeLabel,.mermaid svg .cluster-label,.mermaid svg .messageText,.mermaid svg foreignObject div,.mermaid svg foreignObject span,.mermaid svg p{fill: #111111 !important;color: #111111 !important;background: transparent !important}.mermaid svg .edgeLabel rect,.mermaid svg .edgeLabel foreignObject>div{fill: #ffffff !important;background: #ffffff !important}}
|
|
1
|
+
/*! danieldeusing-design v0.31.0 | MIT | https://github.com/danieldeusing/danieldeusing-design */
|
|
2
|
+
*,*::before,*::after{box-sizing: border-box}*{margin: 0}html{-webkit-text-size-adjust: 100%;text-size-adjust: 100%;tab-size: 4}body{min-height: 100vh;min-height: 100dvh;line-height: 1.5}img,picture,video,canvas,svg{display: block;max-width: 100%}input,button,textarea,select{font: inherit;color: inherit}p,h1,h2,h3,h4,h5,h6{overflow-wrap: break-word}ul,ol{list-style: none;padding: 0}a{color: inherit;text-decoration: none}table{border-collapse: collapse}:root{--background: #f5efe2;--foreground: #43352a;--card: #efe7d4;--card-foreground: #43352a;--popover: #efe7d4;--popover-foreground: #43352a;--primary: #8a4516;--primary-foreground: #f8f3e8;--secondary: #ece3cf;--secondary-foreground: #43352a;--muted: #ece3cf;--muted-foreground: #71614e;--accent: #8a4516;--accent-foreground: #f8f3e8;--destructive: #a02c2c;--success: #1a6b2e;--warning: #845600;--info: #1c5f9e;--pending: #5f3dc4;--border: #d9cdb6;--input: #d9cdb6;--ring: #8a4516;--glow: rgba(160,82,45,0.1);--glow-soft: rgba(160,82,45,0.05);--scanline-opacity: 0.15;--radius: 0rem;--font-mono: "JetBrains Mono Variable","JetBrains Mono","Menlo",monospace}html[data-theme="green"]{--background: #020604;--foreground: #4fdd7d;--card: #04110a;--card-foreground: #4fdd7d;--popover: #04110a;--popover-foreground: #4fdd7d;--primary: #33ff66;--primary-foreground: #02160a;--secondary: #071509;--secondary-foreground: #4fdd7d;--muted: #071509;--muted-foreground: #3da45e;--accent: #33ff66;--accent-foreground: #02160a;--destructive: #ff5c5c;--success: #4ade80;--warning: #f5b32b;--info: #4dabf7;--pending: #b197fc;--border: #1c4a2c;--input: #1c4a2c;--ring: #33ff66;--glow: rgba(51,255,102,0.35);--glow-soft: rgba(51,255,102,0.12);--scanline-opacity: 0.5}html[data-theme="mono"]{--background: #050505;--foreground: #d4d4d4;--card: #0d0d0d;--card-foreground: #d4d4d4;--popover: #0d0d0d;--popover-foreground: #d4d4d4;--primary: #ffffff;--primary-foreground: #050505;--secondary: #111111;--secondary-foreground: #d4d4d4;--muted: #111111;--muted-foreground: #8a8a8a;--accent: #ffffff;--accent-foreground: #050505;--destructive: #ff5c5c;--success: #4ade80;--warning: #f5b32b;--info: #4dabf7;--pending: #b197fc;--border: #333333;--input: #333333;--ring: #ffffff;--glow: rgba(255,255,255,0.25);--glow-soft: rgba(255,255,255,0.08);--scanline-opacity: 0.45}html[data-theme="paper"]{--background: #fafafa;--foreground: #1f1f1f;--card: #f1f1f1;--card-foreground: #1f1f1f;--popover: #f1f1f1;--popover-foreground: #1f1f1f;--primary: #000000;--primary-foreground: #fafafa;--secondary: #efefef;--secondary-foreground: #1f1f1f;--muted: #efefef;--muted-foreground: #595959;--accent: #000000;--accent-foreground: #fafafa;--destructive: #b3261e;--success: #1a7031;--warning: #8f5d00;--info: #1c5f9e;--pending: #5f3dc4;--border: #d4d4d4;--input: #d4d4d4;--ring: #000000;--glow: rgba(0,0,0,0.06);--glow-soft: rgba(0,0,0,0.04);--scanline-opacity: 0.12}:root{--content-w: 78rem;font-size: max(1rem,calc(1rem + (100vw - 1920px) / 120));--content-pad: 1.5rem;--fs-base: 0.75rem;--fs-lg: 0.9375rem;--fs-xl: 1.125rem;--space-section: 2.5rem;--fs-2xl: 1.5rem;--lh-tight: 1.3;--lh-base: 1.5;--field-label-w: 8.5rem}html{scroll-behavior: smooth}*,*::before,*::after{border-color: var(--border)}:focus-visible{outline: 2px solid var(--ring);outline-offset: 2px}body{background: var(--background);color: var(--foreground);font-family: var(--font-mono);font-size: var(--fs-base);line-height: var(--lh-base);overflow-x: hidden;-webkit-font-smoothing: antialiased;-moz-osx-font-smoothing: grayscale}table{font-size: var(--fs-base);width: 100%}th,td{padding: 0.45rem 0.85rem;text-align: left;vertical-align: top;border-block-end: 1px solid color-mix(in srgb,var(--border) 55%,transparent)}thead th{border-block-end-color: var(--border);color: var(--muted-foreground);font-weight: 600}tbody tr:last-child>*{border-block-end: 0}tr>*:first-child{padding-inline-start: 0}tr>*:last-child{padding-inline-end: 0}tr[hidden]{display: none !important}button:not(:disabled),summary,[role="button"]{cursor: pointer}body::after{content: "";position: fixed;inset: 0;z-index: 40;pointer-events: none;background: repeating-linear-gradient( 0deg,rgba(0,0,0,0.25) 0px,rgba(0,0,0,0.25) 1px,transparent 1px,transparent 3px );opacity: var(--scanline-opacity)}::selection{background: var(--primary);color: var(--primary-foreground)}:where(section) + :where(section),:where(section.doc){margin-block-start: var(--space-section)}:where(section,.wrap,.body)>:where(h2){margin-block-start: var(--space-section)}:where(section,.wrap,.body)>:where(h3){margin-block-start: calc(var(--space-section) * 0.6)}:where(section,.wrap,.body)>:where(h2,h3):first-child{margin-block-start: 0}:where(h2,h3) + :where(p,table,.tablewrap,svg,.legend){margin-block-start: 0.6rem}:where(p) + :where(table,.tablewrap,svg,.legend){margin-block-start: 0.6rem}.glow{color: var(--primary);text-shadow: 0 0 8px var(--glow)}.glow-lg{color: var(--primary);text-shadow: 0 0 12px var(--glow)}.field-row{display: grid;grid-template-columns: var(--field-label-w) minmax(0,1fr);align-items: center;gap: 0.5rem 0.75rem;margin: 0.35rem 0}.field-row>.lbl{color: var(--muted-foreground);font-size: var(--fs-base);text-align: left}.field-row>.field-val{display: flex;flex-wrap: wrap;align-items: center;gap: 0.4rem 0.9rem;min-width: 0}@media (max-width: 40rem){.field-row{grid-template-columns: 1fr;gap: 0.2rem}}.prompt{font-size: var(--fs-base);font-weight: 700;color: var(--muted-foreground)}.prompt::before{content: "$ ";color: var(--primary)}.comment::before{content: "# ";color: var(--border)}.cursor-block{display: inline-block;width: 0.55em;height: 1em;margin-left: 8px;background: var(--primary);vertical-align: baseline;transform: translateY(0.12em);animation: ddd-blink 1.1s step-end infinite}@keyframes ddd-blink{50%{opacity: 0}}.btn-terminal{display: inline-block;background: var(--primary);color: var(--primary-foreground);font-weight: 700;padding: 12px 24px;box-shadow: 0 0 16px var(--glow);transition: box-shadow 0.15s ease,transform 0.15s ease}.btn-terminal::before{content: "> "}.btn-terminal--ghost{background: transparent;color: var(--primary);border: 1px solid var(--border);box-shadow: none;font-weight: 400}.btn-terminal--ghost::before{content: ""}.btn-terminal--ghost:hover{border-color: var(--primary);background: color-mix(in srgb,var(--primary) 10%,transparent);box-shadow: none}.btn-terminal:hover{box-shadow: 0 0 28px var(--glow);transform: translateY(-2px)}.btn-terminal:disabled{opacity: 0.45;cursor: default;box-shadow: none;transform: none}.btn-terminal--compact{padding: 0.28rem 0.7rem;font-size: var(--fs-base)}.btn-terminal--destructive{display: inline-flex;align-items: center;justify-content: center;padding: 0.3rem;color: var(--destructive);border-color: color-mix(in srgb,var(--destructive) 45%,var(--border))}.btn-terminal--destructive::before{content: ""}.btn-terminal--destructive::after{content: "";display: block;width: 0.875rem;height: 0.875rem;background: currentColor;-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M9 3h6v3H9zM4 6h16v2H4zM6 9h12l-1 12H7z'/%3E%3C/svg%3E") center / contain no-repeat;mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M9 3h6v3H9zM4 6h16v2H4zM6 9h12l-1 12H7z'/%3E%3C/svg%3E") center / contain no-repeat}.btn-terminal--destructive:hover{border-color: var(--destructive);background: color-mix(in srgb,var(--destructive) 12%,transparent);box-shadow: none}@media (pointer: coarse){.btn-terminal--destructive{min-width: 44px;min-height: 44px}}.btn-terminal--edit{display: inline-flex;align-items: center;justify-content: center;padding: 0.3rem}.btn-terminal--edit::before{content: ""}.btn-terminal--edit::after{content: "";display: block;width: 0.875rem;height: 0.875rem;background: currentColor;-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04l-3.75-3.75-1.83 1.83 3.75 3.75 1.83-1.83z'/%3E%3C/svg%3E") center / contain no-repeat;mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04l-3.75-3.75-1.83 1.83 3.75 3.75 1.83-1.83z'/%3E%3C/svg%3E") center / contain no-repeat}@media (pointer: coarse){.btn-terminal--edit{min-width: 44px;min-height: 44px}}.link-quiet{color: var(--muted-foreground);text-decoration: underline;text-underline-offset: 4px;transition: color 0.15s ease}.link-quiet:hover{color: var(--primary)}.anim-toggle{display: inline-flex;align-items: center;gap: 6px;font: inherit;color: var(--muted-foreground);background: none;border: 0;cursor: pointer;transition: color 0.15s ease}.anim-toggle:hover{color: var(--primary)}.anim-toggle[aria-pressed="true"] [data-anim-box]{color: var(--primary)}.dd-dot{display: inline-block;width: 11px;height: 11px;flex: none;border-radius: 50%;background: currentColor}.dd-flag{display: inline-block;width: 15px;height: 10px;flex: none;border: 1px solid var(--border);background-size: cover;background-repeat: no-repeat}.dd-flag-de{background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 18 12'><rect width='18' height='4' fill='%23262626'/><rect y='4' width='18' height='4' fill='%23c83232'/><rect y='8' width='18' height='4' fill='%23e8be32'/></svg>")}.dd-flag-en{background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 18 12'><rect width='18' height='12' fill='%23b22234'/><rect y='4' width='18' height='4' fill='%23ececec'/><rect width='8' height='6' fill='%233c3b6e'/></svg>")}.dd-flag-pt{background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 18 12'><rect width='18' height='12' fill='%231c9148'/><polygon points='9,1.5 15.5,6 9,10.5 2.5,6' fill='%23e8c832'/><rect x='8' y='5' width='2' height='2' fill='%232c3f87'/></svg>")}.dd-flag-es{background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 18 12'><rect width='6' height='12' fill='%2315734c'/><rect x='6' width='6' height='12' fill='%23ececec'/><rect x='12' width='6' height='12' fill='%23bf2233'/><rect x='8' y='5' width='2' height='2' fill='%237a5c3a'/></svg>")}.ascii-rule{margin: 1rem 0;border: 0;color: var(--border);line-height: 1;white-space: nowrap;overflow: hidden;user-select: none}.ascii-rule::before{content: "────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────"}.card-terminal{background: var(--card);border: 1px solid var(--border);transition: border-color 0.15s ease,box-shadow 0.15s ease}.card-terminal:hover{border-color: var(--primary);box-shadow: 0 0 20px var(--glow-soft)}.dropdown{position: relative}.dropdown>summary{display: inline-flex;align-items: center;gap: 6px;cursor: pointer;user-select: none;list-style: none;color: var(--muted-foreground);transition: color 0.15s ease}.dropdown>summary::-webkit-details-marker{display: none}.dropdown>summary:hover,.dropdown[open]>summary{color: var(--primary)}.dropdown-panel{position: absolute;right: 0;bottom: calc(100% + 12px);z-index: 50;min-width: 128px;margin: 0;padding: 4px 0;list-style: none;background: var(--card);border: 1px solid var(--border);border-radius: var(--radius);box-shadow: 0 0 20px var(--glow-soft)}.dropdown-panel--down{top: calc(100% + 8px);bottom: auto;left: 0;right: auto}.dropdown-item{display: flex;align-items: center;gap: 8px;width: 100%;padding: 5px 14px;text-align: left;font: inherit;line-height: 1.5;background: none;border: 0;color: var(--muted-foreground);cursor: pointer;white-space: nowrap;transition: color 0.15s ease}.dropdown-item:hover{color: var(--primary);background: var(--muted)}.dropdown-item[aria-current="true"],html:not([data-theme]) .dropdown-item[data-theme-value="warm"],html[data-theme="green"] .dropdown-item[data-theme-value="green"],html[data-theme="mono"] .dropdown-item[data-theme-value="mono"],html[data-theme="paper"] .dropdown-item[data-theme-value="paper"],html[data-theme="warm"] .dropdown-item[data-theme-value="warm"]{color: var(--primary);text-shadow: 0 0 8px var(--glow)}input[type="text"],input[type="search"],input[type="url"],input[type="email"],input[type="password"],input[type="number"],input[type="tel"],input[type="date"],input:not([type]),textarea{max-width: 100%;padding: 0.35rem 0.5rem;font: inherit;font-size: var(--fs-base);line-height: var(--lh-tight);color: var(--foreground);background-color: transparent;border: 1px solid color-mix(in srgb,var(--foreground) 60%,transparent);transition: border-color 0.15s ease}textarea{line-height: var(--lh-base)}input[type="text"]:hover,input[type="search"]:hover,input[type="url"]:hover,input[type="email"]:hover,input[type="password"]:hover,input[type="number"]:hover,input[type="tel"]:hover,input[type="date"]:hover,input:not([type]):hover,textarea:hover{border-color: var(--primary)}input:disabled,textarea:disabled{opacity: 0.55;cursor: not-allowed}select,.select-trigger{appearance: none;-webkit-appearance: none;max-width: 100%;padding: 0.35rem 1.45rem 0.35rem 0.5rem;font: inherit;font-size: var(--fs-base);line-height: var(--lh-tight);text-align: left;color: var(--foreground);background-color: transparent;background-image: linear-gradient(45deg,transparent 50%,currentColor 50%),linear-gradient(135deg,currentColor 50%,transparent 50%);background-position: right 0.78rem center,right 0.5rem center;background-size: 0.28rem 0.28rem;background-repeat: no-repeat;border: 1px solid color-mix(in srgb,var(--foreground) 60%,transparent);cursor: pointer;transition: border-color 0.15s ease}select:hover,.select-trigger:hover,.select-trigger[aria-expanded="true"]{border-color: var(--primary)}select:disabled,.select-trigger:disabled{opacity: 0.45;cursor: default}.select-trigger[aria-expanded="true"]{background-image: linear-gradient(135deg,transparent 50%,currentColor 50%),linear-gradient(45deg,currentColor 50%,transparent 50%)}.select-field{position: relative;display: inline-flex;max-width: 100%;vertical-align: middle}.select-field>select{position: absolute;inset: 0;width: 100%;height: 100%;opacity: 0;pointer-events: none}.select-trigger{display: inline-flex;align-items: center;width: 100%}.select-value{min-width: 0;overflow: hidden;text-overflow: ellipsis;white-space: nowrap}.select-value::after{content: "\200b"}.select-panel{position: fixed;z-index: 60;margin: 0;padding: 0.15rem 0;overflow-y: auto;overscroll-behavior: contain;list-style: none;font-size: var(--fs-base);line-height: var(--lh-tight);color: var(--popover-foreground);background: var(--popover);border: 1px solid color-mix(in srgb,var(--foreground) 60%,transparent);box-shadow: 0 0 20px var(--glow-soft)}.select-option{display: flex;align-items: center;gap: 0.4rem;padding: 0.28rem 0.9rem 0.28rem 0.55rem;border-inline-start: 2px solid transparent;white-space: nowrap;cursor: pointer}.select-option:hover,.select-option[data-active="true"]{background: var(--muted)}.select-option[aria-selected="true"]{color: var(--primary);font-weight: 700;border-inline-start-color: var(--primary)}.select-option[aria-disabled="true"]{color: color-mix(in srgb,var(--popover-foreground) 65%,var(--popover));cursor: default}.select-option[aria-disabled="true"]:hover{background: none}.select-group{padding: 0.35rem 0.9rem 0.15rem;color: var(--muted-foreground);font-size: var(--fs-base);text-transform: uppercase;letter-spacing: 0.06em}.table-pager{display: flex;flex-wrap: wrap;align-items: center;gap: 0.45rem 0.9rem;margin-block-start: 0.6rem;font-size: var(--fs-base)}.table-pager-status{margin: 0;color: var(--muted-foreground)}.table-pager-size{display: inline-flex;align-items: center;gap: 0.4rem;margin-inline-start: auto;color: var(--muted-foreground)}.table-pager-nav{display: inline-flex;gap: 0.4rem}.eli5{margin: 1.25em 0;padding: 0.7em 1em;color: var(--foreground);background: color-mix(in srgb,var(--primary) 7%,var(--card));border: 1px solid var(--border);border-left: 3px solid var(--primary);border-radius: var(--radius);font-size: var(--fs-base);line-height: 1.6}.eli5::before{content: "ELI5";display: inline-flex;align-items: center;justify-content: center;margin-right: 0.6em;vertical-align: 0.08em;font-family: var(--font-mono);font-size: 0.72em;font-weight: 700;line-height: 1;letter-spacing: 0.04em;padding: 0.34em 0.6em;border-radius: var(--radius);background: var(--destructive);color: var(--primary-foreground)}.eli5-term{font-weight: 700;color: var(--primary)}html.anim-off *,html.anim-off *::before,html.anim-off *::after{animation: none !important}html.anim-off .cursor-block{display: none}@media (prefers-reduced-motion: no-preference){html.term-anim [data-term] .prompt:not(.term-live),html.term-anim [data-term] .prompt:not(.term-live)::before{color: transparent}html.term-anim [data-term] [data-term-out]:not(.term-show){visibility: hidden}html.term-anim [data-term] [data-term-out].term-show{animation: ddd-term-output 0.2s steps(3,end) both}@keyframes ddd-term-output{from{opacity: 0}}html.term-anim [data-term] [data-term-out].term-show>*{animation: ddd-term-output 0.25s steps(3,end) both}html.term-anim [data-term] [data-term-out].term-show>*:nth-child(2){animation-delay: 0.09s}html.term-anim [data-term] [data-term-out].term-show>*:nth-child(3){animation-delay: 0.18s}html.term-anim [data-term] [data-term-out].term-show>*:nth-child(4){animation-delay: 0.27s}html.term-anim [data-term] [data-term-out].term-show>*:nth-child(5){animation-delay: 0.36s}html.term-anim [data-term] [data-term-out].term-show>*:nth-child(6){animation-delay: 0.45s}html.term-anim [data-term] [data-term-out].term-show>*:nth-child(7){animation-delay: 0.54s}html.term-anim [data-term] [data-term-out].term-show>*:nth-child(8){animation-delay: 0.63s}html.term-anim [data-term] [data-term-out].term-show>*:nth-child(9){animation-delay: 0.72s}html.term-anim [data-term] [data-term-out].term-show>*:nth-child(10){animation-delay: 0.81s}html.term-anim [data-term] [data-term-out].term-show>*:nth-child(n + 11){animation-delay: 0.9s}.term-caret{display: inline-block;width: 0.55em;height: 1em;margin-left: 2px;background: var(--primary);vertical-align: baseline;transform: translateY(0.12em)}}.tabs{display: flex;flex-wrap: wrap;gap: 0.15rem;margin: 1.35rem 0 0;border-bottom: 2px solid color-mix(in srgb,var(--border) 80%,transparent)}.tab{background: transparent;border: 0;margin-bottom: -2px;font: inherit;font-size: var(--fs-base);cursor: pointer;padding: 0.5rem 0.95rem;color: var(--muted-foreground);transition: background 0.12s ease,color 0.12s ease}.tab:hover{color: var(--primary);background: color-mix(in srgb,var(--primary) 10%,transparent)}.tab[aria-selected="true"]{background: var(--primary);color: var(--background);font-weight: 700}.tab:focus-visible{outline: 2px solid color-mix(in srgb,var(--primary) 55%,transparent);outline-offset: -2px}.tab--info{margin-inline-start: auto}@media (max-width: 40rem){.tab--info{margin-inline-start: 0}}@media (pointer: coarse),(max-width: 40rem){.tab{padding: 0.7rem 0.8rem;min-height: 44px}}section.doc.tab-panel{margin-top: 1.35rem}details.fold{border-top: 1px solid color-mix(in srgb,currentColor 15%,transparent)}details.fold>summary{display: flex;align-items: center;gap: 0.5rem;cursor: pointer;list-style: none;padding: 0.55rem 0;font-size: var(--fs-base);color: var(--muted-foreground)}details.fold>summary::-webkit-details-marker{display: none}details.fold>summary::before{content: "\25B8";flex: none;opacity: 0.6}details.fold[open]>summary::before{content: "\25BE"}details.fold>summary:hover{color: var(--primary)}details.fold>summary:focus-visible{outline: 2px solid color-mix(in srgb,currentColor 45%,transparent);outline-offset: 2px}.fold-body{font-size: var(--fs-base);padding: 0 0 0.7rem 1.1rem}.fold-body>* + *{margin-top: 0.6rem}.legend{list-style: none;padding: 0;margin: 0.7rem 0 0;display: grid;gap: 0.4rem 1.4rem;font-size: var(--fs-base)}@media (min-width: 52rem){.legend{grid-template-columns: repeat(2,minmax(0,1fr))}}.legend>li{line-height: 1.6}.legend>li>:first-child{margin-right: 0.45rem}.dgm-zoomable{position: relative;cursor: zoom-in}.dgm-zoomable::after{content: "⤢";position: absolute;top: 0.4rem;right: 0.5rem;font-size: 0.85rem;line-height: 1;opacity: 0.45;pointer-events: none;color: var(--muted-foreground)}.dgm-zoomable:hover::after,.dgm-zoomable:focus-visible::after{opacity: 1;color: var(--primary)}.dgm-zoomable:focus-visible{outline: 2px solid var(--primary);outline-offset: 3px}html.dgm-locked,html.dgm-locked body{overflow: hidden}.dgm-overlay{display: none;position: fixed;inset: 0;z-index: 90;background: color-mix(in srgb,var(--background) 96%,transparent);flex-direction: column}.dgm-overlay.open{display: flex}.dgm-bar{flex: none;display: flex;align-items: center;justify-content: flex-end;gap: 0.4rem;padding: 0.5rem 0.75rem;border-bottom: 1px solid var(--border);background: var(--card)}.dgm-btn{font: inherit;font-size: var(--fs-base);cursor: pointer;padding: 0.3rem 0.7rem;min-height: 32px;background: transparent;border: 1px solid var(--border);color: var(--foreground)}.dgm-btn:hover{border-color: var(--primary);color: var(--primary)}.dgm-btn:focus-visible{outline: 2px solid var(--primary);outline-offset: 1px}.dgm-close{border-color: color-mix(in srgb,var(--destructive) 55%,transparent);color: var(--destructive)}.dgm-close:hover{border-color: var(--destructive);color: var(--destructive)}.dgm-stage{flex: 1;overflow: hidden;display: grid;place-items: center;cursor: grab;touch-action: none}.dgm-stage.is-grabbing{cursor: grabbing}.dgm-art{transform-origin: center center;will-change: transform}.dgm-art>svg,.dgm-art>img{display: block;max-width: none;max-height: none}@media print{.dgm-overlay,.dgm-zoomable::after{display: none !important}}.minimap{position: fixed;left: 0;top: var(--ls-nav-top,3rem);bottom: var(--ls-nav-bottom,2.2rem);z-index: 20;display: none;flex-direction: column;justify-content: center;gap: 0.5rem;width: 2rem;padding: 0.75rem 0 0.75rem 0.75rem;overflow-y: auto;overscroll-behavior: contain;scrollbar-width: none}.minimap::-webkit-scrollbar{display: none}@media (min-width: 64rem){.minimap{display: flex}}.minimap-bar{flex: 0 0 auto;width: var(--minimap-bar-w,100%);height: 2px;padding: 0;border: 0;border-radius: 1px;background: var(--border);cursor: pointer;transition: background 0.15s ease,transform 0.15s ease;transform-origin: left center}.minimap-bar:hover{background: var(--muted-foreground);transform: scaleY(2)}.minimap-bar[data-tip]{cursor: pointer}.minimap-bar:focus-visible{outline: 2px solid var(--ring);outline-offset: 3px}.minimap-bar.active{background: var(--primary);transform: scaleY(2)}@media print{.minimap{display: none !important}}.tbl-toolbar{display: flex;align-items: center;gap: 0.5rem;margin-block-end: 0.5rem}.tbl-search{flex: 1 1 auto;min-inline-size: 0;font: inherit;font-size: var(--fs-base);color: var(--foreground);background: var(--background);border: 1px solid var(--border);padding: 0.25rem 0.5rem}.tbl-search:focus-visible{outline: 2px solid var(--ring);outline-offset: 1px}.tbl-tools{display: inline-flex;align-items: center;gap: 0.15rem;margin-inline-start: 0.4rem;vertical-align: baseline}.tbl-sort,.tbl-filter>summary{background: transparent;border: 0;padding: 0 0.15rem;font: inherit;font-size: var(--fs-base);line-height: 1;color: var(--muted-foreground);cursor: pointer;opacity: 0.55;transition: opacity 0.15s ease,color 0.15s ease}.tbl-sort:hover,.tbl-filter>summary:hover,th[aria-sort]:not([aria-sort="none"]) .tbl-sort{opacity: 1;color: var(--primary)}.tbl-sort:focus-visible,.tbl-filter>summary:focus-visible{outline: 2px solid var(--ring);outline-offset: 1px;opacity: 1}.tbl-filter.is-on>summary{opacity: 1;color: var(--primary)}.tbl-filter.is-on>summary::after{content: "•";margin-inline-start: 0.1em}.tbl-filter-panel{padding: 0.4rem;max-block-size: 60vh;overflow-y: auto}.tbl-filter-input{font: inherit;font-size: var(--fs-base);color: var(--foreground);background: var(--background);border: 1px solid var(--border);padding: 0.2rem 0.4rem;inline-size: 12rem;max-inline-size: 100%}.tbl-view{display: flex;flex-wrap: wrap;align-items: center;gap: 0.4rem;margin-block-end: 0.6rem;padding: 0.35rem 0.6rem;font-size: var(--fs-base);border-inline-start: 3px solid var(--primary);background: color-mix(in srgb,var(--primary) 10%,var(--background))}.tbl-view[hidden]{display: none}.tbl-view-lead{color: var(--primary);font-weight: 700}.tbl-view-lead::before{content: "▸ "}.tbl-view-chip{border: 1px solid var(--border);background: var(--card);padding: 0.05rem 0.4rem;white-space: nowrap}.tbl-view [data-view-reset]{margin-inline-start: auto}@media (pointer: coarse){.tbl-sort,.tbl-filter>summary{min-inline-size: 44px;min-block-size: 44px;display: inline-flex;align-items: center;justify-content: center}}@media print{.tbl-toolbar,.tbl-tools{display: none !important}}.wrap{max-width: var(--content-w);margin-inline: auto;padding-inline: var(--content-pad)}.tablewrap{position: relative;overflow-x: auto;overflow-y: auto;max-block-size: var(--tablewrap-max-h,none)}.tablewrap::after{content: "";position: absolute;inset-block: 0;right: 0;width: 1.5rem;pointer-events: none;opacity: 0.9;background: linear-gradient(to right,transparent,var(--tablewrap-fade,var(--background,transparent)))}.tablewrap thead th{position: sticky;inset-block-start: 0;z-index: 1;background: var(--background,#f5efe2)}.tickstrip:not(:empty){--tablewrap-fade: var(--card);border: 1px solid var(--border);background: var(--card);margin: 1.6rem 0 1.5rem}.ticktable{width: 100%;border-collapse: collapse;font-size: var(--fs-base)}.ticktable td{padding: 0.5rem 0.75rem;vertical-align: baseline;border: 0;white-space: nowrap}.ticktable td.tick-stats{width: 100%;white-space: normal;text-align: right}.tick{border-bottom: 1px solid color-mix(in srgb,var(--border) 55%,transparent)}.tick:last-child{border-bottom: 0}.tick-dot{font-size: 0.7em;width: 1.5rem;padding-right: 0 !important;border-left: 3px solid transparent;padding-left: 0.6rem !important}.tick--ok .tick-dot{color: var(--success);border-left-color: color-mix(in srgb,var(--success) 45%,transparent)}.tick--stale .tick-dot{color: var(--destructive);border-left-color: var(--destructive)}.tick--never .tick-dot{color: var(--muted-foreground);border-left-color: color-mix(in srgb,var(--muted-foreground) 35%,transparent)}.tick-name{font-weight: 700;color: var(--foreground);letter-spacing: 0.01em}.tick-last,.tick-next{font-variant-numeric: tabular-nums;color: var(--muted-foreground)}.tick-next{opacity: 0.72}.tick--stale .tick-last{color: var(--destructive);font-weight: 700}.tick--never .tick-last{font-style: italic}.tick-stats{color: var(--muted-foreground)}.tick-stats b{font-weight: 700;color: var(--primary);font-variant-numeric: tabular-nums}.tick-sep{font-style: normal;opacity: 0.35;margin: 0 0.5rem}.tick--stale{background: color-mix(in srgb,var(--destructive) 8%,transparent)}@media (max-width: 40rem){.ticktable,.ticktable tbody,.tick,.ticktable td{display: block}.tick{padding: 0.4rem 0.5rem}.ticktable td{display: inline-block;padding: 0.1rem 0.5rem 0.1rem 0}.ticktable td.tick-stats{display: block;width: auto;text-align: left}}.skip-link{position: absolute;left: -999px;top: 0;z-index: 100;background: var(--card);border: 1px solid var(--primary);padding: 0.5rem 1rem;color: var(--primary)}.skip-link:focus{left: 0.5rem;top: 0.5rem}.visually-hidden{position: absolute;width: 1px;height: 1px;overflow: hidden;clip: rect(0 0 0 0);white-space: nowrap}header.bar{position: sticky;top: 0;z-index: 30;display: flex;align-items: center;justify-content: space-between;gap: 1rem;padding: 0.6rem 1.25rem;border-bottom: 1px solid var(--border);background: var(--card)}header.bar .brand{display: inline-flex;align-items: center;font-weight: 700;font-size: 1rem}header.bar .brand,header.bar .brand a{color: inherit;text-decoration: none}header.bar .dropdown summary{padding: 0.6rem 0.4rem}.bar-right{display: flex;align-items: center;gap: 1rem;flex-shrink: 0}footer.status{position: fixed;inset-inline: 0;bottom: 0;z-index: 50;min-height: 2rem;display: flex;align-items: center;justify-content: space-between;gap: 1rem;padding: 0.35rem 1.25rem;border-top: 1px solid var(--border);background: var(--card);font-size: var(--fs-base)}footer.status .dropdown summary,footer.status .anim-toggle{padding: 0.55rem 0.4rem}.status-left{color: var(--muted-foreground);min-width: 0;overflow: hidden;text-overflow: ellipsis;white-space: nowrap}.status-right{display: flex;align-items: center;gap: 1rem;flex-shrink: 0}.sep{width: 1px;height: 0.9rem;background: var(--border)}.doc-link{color: var(--muted-foreground);text-decoration: none;transition: color 0.15s ease}.doc-link:hover{color: var(--primary)}.doc-link--forward{color: var(--primary);white-space: nowrap}.doc-link--forward:hover{color: var(--primary);text-decoration: underline}.ls-group{display: block;padding: 0.55rem 0 0.15rem;font-size: var(--fs-base);letter-spacing: 0.08em;color: var(--muted-foreground);opacity: 0.75}.ls-panel>li:first-child .ls-group{padding-top: 0}.ls-row{display: flex;align-items: baseline;gap: 0.9rem}.ls-perm{color: color-mix(in srgb,var(--muted-foreground) 75%,var(--card));font-size: var(--fs-base);letter-spacing: 0}.ls-row--sub .ls-name{margin-left: 0.55rem;padding-left: 0.75rem;border-left: 1px solid color-mix(in srgb,var(--muted-foreground) 40%,transparent)}.ls-row--sub2 .ls-name{margin-left: 1.7rem}.ls-panel .ls-name{color: var(--muted-foreground)}.ls-panel .ls-row--dir .ls-name{color: var(--primary);font-weight: 700}.ls-row[aria-current="page"]{background: color-mix(in srgb,var(--primary) 12%,transparent);box-shadow: inset 3px 0 0 0 var(--primary)}.ls-row[aria-current="page"] .ls-name{color: var(--primary);font-weight: 700;text-shadow: 0 0 8px var(--glow)}.ls-row[aria-current="page"] .ls-perm{color: color-mix(in srgb,var(--primary) 65%,transparent)}.ls-row[aria-current="page"]::after{content: "←";margin-left: auto;color: var(--primary)}.dropdown-panel.ls-panel{min-width: 17rem;left: auto !important;right: 0}:root{--ls-nav-w: 17rem;--ls-nav-top: 3rem;--ls-nav-bottom: 2.2rem}@media (min-width: 48.0625rem){html{--ls-nav-inset: 0rem}html:has(.ls-nav):not([data-ls-nav="off"]){--ls-nav-inset: var(--ls-nav-w)}body{padding-right: var(--ls-nav-inset)}header.bar,.bleed-rail{margin-right: calc(-1 * var(--ls-nav-inset))}.ls-nav{position: fixed;top: var(--ls-nav-top);right: 0;bottom: var(--ls-nav-bottom);width: var(--ls-nav-w);z-index: 25;display: flex;flex-direction: column;overflow: hidden;background: var(--card);border-left: 1px solid var(--border)}html:not(.anim-off) .ls-nav{transition: transform 0.18s ease}html[data-ls-nav="off"] .ls-nav{transform: translateX(100%)}.ls-nav-head{display: inline-flex;align-items: center;gap: 0.5rem;margin-left: 0.75rem}.ls-nav-title{font-weight: 700;font-size: var(--fs-base)}.ls-nav .ls-panel{flex: 1;min-height: 0;overflow-y: auto;list-style: none;margin: 0;padding: 0.5rem 0 1rem}.ls-nav-toggle{background: none;border: 0;font: inherit;cursor: pointer;line-height: 1;color: var(--muted-foreground);padding: 0.4rem 0.55rem}.ls-nav-toggle::after{content: "\00BB"}html[data-ls-nav="off"] .ls-nav-toggle::after{content: "\00AB"}.ls-nav-toggle:hover{color: var(--primary)}.ls-nav-toggle:focus-visible{outline: 2px solid color-mix(in srgb,var(--primary) 55%,transparent);outline-offset: -2px}}.nav-burger{display: none;background: none;border: 0;color: var(--muted-foreground);cursor: pointer;padding: 0.55rem;margin: -0.3rem -0.3rem -0.3rem 0}.nav-burger:hover{color: var(--primary)}.mobile-nav{display: none;list-style: none;margin: 0;padding: 0}.mobile-footer{display: none}@media (max-width: 48rem){.nav-burger{display: inline-flex;align-items: center}.site-nav{display: none;position: absolute;top: 100%;left: 0;right: 0;z-index: 40;background: var(--card);border-bottom: 1px solid var(--border);padding: 0.4rem 1.25rem 0.9rem;max-height: calc(100vh - 6rem);overflow-y: auto}.site-nav.open{display: block}.site-nav>.ls-nav,.mobile-nav{display: flex;flex-direction: column}.site-nav:has(>.ls-nav) .mobile-nav{display: none}.site-nav>.ls-nav .ls-row{align-items: center;min-height: 44px;border-bottom: 1px solid var(--border)}.site-nav>.ls-nav .ls-row .ls-name{display: flex;align-items: center;flex: 1;min-height: 44px}.site-nav>.ls-nav .ls-row:last-child{border-bottom: 0}.mobile-nav .mobile-item{display: flex;align-items: baseline;gap: 0.9rem;padding: 0.65rem 0;text-decoration: none;color: var(--foreground);font-size: var(--fs-base);border-bottom: 1px solid var(--border)}.mobile-nav li:last-child .mobile-item{border-bottom: 0}.mobile-nav .mobile-item:active .ls-name,.mobile-nav .mobile-item:hover .ls-name{color: var(--primary)}.ls-nav-toggle{display: none}footer.status{display: none}.mobile-footer{display: flex;flex-direction: column;gap: 0.55rem;margin-top: 0.8rem;border-top: 1px solid var(--border);padding-top: 0.8rem;font-size: var(--fs-base)}.mobile-footer .doc-link{padding: 0.2rem 0}.mobile-footer .mobile-theme summary{list-style: none;display: flex;align-items: center;gap: 0.5rem;color: var(--muted-foreground);cursor: pointer;padding: 0.2rem 0}.mobile-footer .mobile-theme summary::-webkit-details-marker{display: none}.mobile-footer .mf-chev{margin-left: auto;opacity: 0.7}.mobile-footer .mf-panel{display: flex;flex-direction: column;margin-top: 0.3rem}.mobile-footer .mf-panel .dropdown-item{text-align: left;padding: 0.4rem 0.6rem}.mobile-footer .anim-toggle{display: inline-flex;gap: 0.5rem;align-items: center;background: none;border: 0;padding: 0.2rem 0;color: var(--muted-foreground);font: inherit;cursor: pointer}}[data-tip]{cursor: help}span[data-tip]::after,th[data-tip]::after,button[data-tip]::after{content: "\FE0E\24D8";margin-inline-start: 0.35em;color: var(--muted-foreground);font-size: 0.9em;font-weight: 400;user-select: none;vertical-align: baseline}.minimap-bar[data-tip]::after,[data-tip][data-tip-bare]::after{content: none}#ddtip{position: fixed;z-index: 9999;display: none;max-width: 340px;background: var(--background);color: var(--foreground);border: 1px solid var(--muted-foreground);border-radius: var(--radius);padding: 7px 10px;font-size: var(--fs-base);line-height: 1.45;font-weight: 400;text-align: left;white-space: normal;pointer-events: none;box-shadow: 0 4px 14px rgba(0,0,0,0.25)}@media print{@page{margin: 14mm 12mm}:root,html,html[data-theme]{--background: #ffffff;--foreground: #111111;--card: #ffffff;--card-foreground: #111111;--popover: #ffffff;--popover-foreground: #111111;--primary: #000000;--primary-foreground: #ffffff;--secondary: #ffffff;--secondary-foreground: #111111;--muted: #ffffff;--muted-foreground: #444444;--accent: #000000;--accent-foreground: #ffffff;--destructive: #7a1c1c;--border: #9a9a9a;--input: #9a9a9a;--ring: #000000;--glow: transparent;--glow-soft: transparent;--scanline-opacity: 0}html{zoom: 1 !important;font-size: 16px;scroll-behavior: auto}body{background: #ffffff;color: var(--foreground);font-size: 12px;line-height: 1.45;overflow: visible}body::after{display: none !important}*,*::before,*::after{animation: none !important;transition: none !important;text-shadow: none !important;box-shadow: none !important}html.term-anim [data-term] [data-term-out],html.term-anim [data-term] [data-term-out]:not(.term-show),html.term-anim [data-term] [data-term-out]>*{visibility: visible !important;opacity: 1 !important}html.term-anim [data-term] .prompt:not(.term-live){color: var(--muted-foreground) !important}html.term-anim [data-term] .prompt:not(.term-live)::before{color: var(--primary) !important}header.bar,footer.status,.site-nav,.nav-burger,.mobile-nav,.mobile-footer,.skip-link,.dropdown,.dropdown-panel,.select-panel,.anim-toggle,.cursor-block,.term-caret,.ascii-rule,.toc,#ddtip{display: none !important}body{padding-right: 0 !important}[data-tip]{cursor: auto}span[data-tip]::after,th[data-tip]::after,button[data-tip]::after{content: none}.wrap{max-width: none !important;margin: 0 !important;padding: 0 !important}.layout{display: block !important}.content{min-width: 0}h1,.title{font-size: 1.5rem !important}h2{font-size: 1.15rem !important}h3,.sub{font-size: 1rem !important}h4{font-size: 0.9rem !important}.prompt{font-size: 0.7rem}h1,h2,h3,h4,.prompt,.toc-label{break-after: avoid-page;break-inside: avoid}p,li,blockquote{orphans: 3;widows: 3}pre,figure,blockquote,tr,img,.eli5,.card-terminal{break-inside: avoid}svg{break-inside: auto}thead{display: table-header-group}.tablewrap,.table-scroll,pre,.diagram{overflow: visible !important;max-width: 100% !important}pre{white-space: pre-wrap !important;overflow-wrap: anywhere;font-size: 0.75rem !important;background: transparent !important}pre.mermaid[data-processed]{white-space: normal !important}code{background: transparent !important;overflow-wrap: anywhere}table{width: 100% !important;table-layout: auto;border-collapse: collapse;font-size: 0.75rem !important}th,td{overflow-wrap: break-word;white-space: normal !important}td code,th code,td a,th a{overflow-wrap: anywhere}a{color: inherit;text-decoration: underline;text-underline-offset: 2px}a.card-terminal,a[class*="card"]{text-decoration: none}.glow,.glow-lg{color: inherit}.btn-terminal{border: 1px solid var(--border);background: transparent;color: var(--foreground)}.mermaid svg{display: block;margin-inline: auto;width: auto !important;height: auto !important;max-width: 100% !important;max-height: 235mm !important}.mermaid svg .node rect,.mermaid svg .node polygon,.mermaid svg .node circle,.mermaid svg .node path,.mermaid svg .label-container,.mermaid svg .cluster rect,.mermaid svg .actor,.mermaid svg .note{fill: #ffffff !important;stroke: #333333 !important}.mermaid svg .edgePath path,.mermaid svg .flowchart-link,.mermaid svg line,.mermaid svg .messageLine0,.mermaid svg .messageLine1{stroke: #333333 !important}.mermaid svg .arrowheadPath,.mermaid svg marker path,.mermaid svg marker polygon{fill: #333333 !important;stroke: #333333 !important}.mermaid svg text,.mermaid svg .nodeLabel,.mermaid svg .edgeLabel,.mermaid svg .cluster-label,.mermaid svg .messageText,.mermaid svg foreignObject div,.mermaid svg foreignObject span,.mermaid svg p{fill: #111111 !important;color: #111111 !important;background: transparent !important}.mermaid svg .edgeLabel rect,.mermaid svg .edgeLabel foreignObject>div{fill: #ffffff !important;background: #ffffff !important}}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danieldeusing/design",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.0",
|
|
4
4
|
"description": "The danieldeusing terminal design system \u2014 framework-agnostic CSS tokens, components, and a vanilla-JS runtime for the CRT/JetBrains-Mono look used across danieldeusing.de, seedr, and briefs.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/runtime/index.js
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
*
|
|
7
7
|
* import { applyStoredTheme, initThemeSwitcher } from "@danieldeusing/design/runtime";
|
|
8
8
|
*
|
|
9
|
-
* The CSS works on its own; this layer adds theme switching,
|
|
10
|
-
*
|
|
9
|
+
* The CSS works on its own; this layer adds theme switching, dropdown
|
|
10
|
+
* behaviour, table tools and the terminal typing animation.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
export * from "./theme.js";
|
|
@@ -23,3 +23,4 @@ export * from "./tablescroll.js";
|
|
|
23
23
|
export * from "./pagination.js";
|
|
24
24
|
export * from "./minimap.js";
|
|
25
25
|
export * from "./tooltip.js";
|
|
26
|
+
export * from "./tabletools.js";
|
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* danieldeusing-design — a table's search, its per-column filters and its sort.
|
|
3
|
+
*
|
|
4
|
+
* Every table in the estate had been growing these by hand. cockpit's own
|
|
5
|
+
* `table-view.js` opens by explaining that `cockpitTable` "is copied into four
|
|
6
|
+
* pages and has drifted into three generations"; the family contacts table grew
|
|
7
|
+
* a row of bare filter boxes because nothing said what a table should look like.
|
|
8
|
+
* Consolidating inside one surface fixed it for that surface. This is the same
|
|
9
|
+
* move one level up, and the storage key is deliberately cockpit's own
|
|
10
|
+
* (`table-view:<id>`) so a reader's saved views survive the migration.
|
|
11
|
+
*
|
|
12
|
+
* ── THE ICONS LIVE IN THE HEADER, THE SEARCH LIVES ON TOP ─────────────────────
|
|
13
|
+
*
|
|
14
|
+
* A row of text boxes under the header (`tr.filters`) spends a whole row of
|
|
15
|
+
* vertical space announcing a capability that is idle on most visits, and it
|
|
16
|
+
* reads as a form to fill in. Two small controls in the `<th>` cost nothing when
|
|
17
|
+
* unused and sit on the column they act on. The filter opens a `<details
|
|
18
|
+
* class="dropdown">` — the system's existing dropdown, so one-open, click-away
|
|
19
|
+
* and Escape are already handled by initDropdowns() and are not reimplemented.
|
|
20
|
+
*
|
|
21
|
+
* ── COMPOSING WITH THE PAGER, WHICH ALSO OWNS `hidden` ────────────────────────
|
|
22
|
+
*
|
|
23
|
+
* runtime/pagination.js pages a table by setting `hidden` on the rows outside
|
|
24
|
+
* the window. If filtering ALSO used `hidden` the two would overwrite each
|
|
25
|
+
* other: the pager clears `hidden` on the first twenty rows in the tbody, which
|
|
26
|
+
* would include rows the filter had just excluded.
|
|
27
|
+
*
|
|
28
|
+
* So a filtered-out row is DETACHED from the tbody and held here, and the tbody
|
|
29
|
+
* is left containing exactly the matching rows in sort order. The pager then
|
|
30
|
+
* sees the set it is documented to expect — "filter and sort produce the rows,
|
|
31
|
+
* the pager slices them" — and needs no knowledge of this file. Its
|
|
32
|
+
* MutationObserver notices the childList change and re-pages on its own.
|
|
33
|
+
*
|
|
34
|
+
* Detaching rather than hiding also means `:nth-child` striping and the pager's
|
|
35
|
+
* own counts are honest without either of them having to know a filter exists.
|
|
36
|
+
*
|
|
37
|
+
* ── NORMALISE AGAINST TODAY'S COLUMNS ─────────────────────────────────────────
|
|
38
|
+
*
|
|
39
|
+
* Inherited from cockpit's engine, and the reason it is not a detail:
|
|
40
|
+
* localStorage outlives the code. A stored sortKey naming a column that has
|
|
41
|
+
* since been renamed would reach an undefined column and throw where the table
|
|
42
|
+
* should be. Unknown keys are dropped, and a direction only survives WITH the
|
|
43
|
+
* column it sorted — applying a remembered direction to a different column hands
|
|
44
|
+
* back a view the reader never chose.
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
const STORE_PREFIX = "table-view:";
|
|
48
|
+
const instances = new WeakMap();
|
|
49
|
+
|
|
50
|
+
const textOf = (el) => (el ? (el.textContent || "").trim() : "");
|
|
51
|
+
|
|
52
|
+
/* A header cell's own words, without the controls injected into it. */
|
|
53
|
+
const labelOf = (th) => {
|
|
54
|
+
let out = "";
|
|
55
|
+
for (const node of th.childNodes) {
|
|
56
|
+
if (node.nodeType === 1 && node.classList && node.classList.contains("tbl-tools")) continue;
|
|
57
|
+
out += node.textContent || "";
|
|
58
|
+
}
|
|
59
|
+
return out.trim();
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/* Sort value: `data-value` wins so a column can sort by something it does not
|
|
63
|
+
print (an ISO date under a friendly one, cents under a formatted amount). */
|
|
64
|
+
const cellValue = (row, index) => {
|
|
65
|
+
const cell = row.cells[index];
|
|
66
|
+
if (!cell) return "";
|
|
67
|
+
const explicit = cell.getAttribute("data-value");
|
|
68
|
+
return explicit === null ? textOf(cell) : explicit.trim();
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/*
|
|
72
|
+
* THE DIRECTION IS APPLIED IN HERE, not by the caller, and that is the whole
|
|
73
|
+
* reason this takes `dir`. A blank cell sorts LAST IN BOTH DIRECTIONS — treating
|
|
74
|
+
* it as 0 would file "not reported" between the negative and the positive
|
|
75
|
+
* numbers, which reads as a measurement rather than an absence. Multiplying the
|
|
76
|
+
* comparator's result by the direction outside it inverts that rule along with
|
|
77
|
+
* everything else, so descending puts every blank FIRST. Caught by the fixture:
|
|
78
|
+
* ascending ended [...Carla, Dieter] and descending began [Dieter, Carla].
|
|
79
|
+
*/
|
|
80
|
+
const compare = (a, b, type, dir) => {
|
|
81
|
+
const aBlank = a === "", bBlank = b === "";
|
|
82
|
+
if (aBlank && bBlank) return 0;
|
|
83
|
+
if (aBlank) return 1;
|
|
84
|
+
if (bBlank) return -1;
|
|
85
|
+
if (type === "num") {
|
|
86
|
+
const na = parseFloat(a.replace(/[^0-9.eE+-]/g, ""));
|
|
87
|
+
const nb = parseFloat(b.replace(/[^0-9.eE+-]/g, ""));
|
|
88
|
+
const aNaN = Number.isNaN(na), bNaN = Number.isNaN(nb);
|
|
89
|
+
if (aNaN && bNaN) return 0;
|
|
90
|
+
if (aNaN) return 1;
|
|
91
|
+
if (bNaN) return -1;
|
|
92
|
+
return dir * (na - nb);
|
|
93
|
+
}
|
|
94
|
+
return dir * a.localeCompare(b, undefined, { numeric: true, sensitivity: "base" });
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
function columnsOf(table) {
|
|
98
|
+
const head = table.tHead && table.tHead.rows[0];
|
|
99
|
+
if (!head) return [];
|
|
100
|
+
const out = [];
|
|
101
|
+
for (let i = 0; i < head.cells.length; i += 1) {
|
|
102
|
+
const th = head.cells[i];
|
|
103
|
+
const key = th.getAttribute("data-col");
|
|
104
|
+
if (!key) continue; // a column opted out is left alone
|
|
105
|
+
out.push({
|
|
106
|
+
key, index: i, th,
|
|
107
|
+
// The label must EXCLUDE the controls this file injects into the same cell.
|
|
108
|
+
// textOf(th) after a snapshot picks up the sort and filter glyphs, and the
|
|
109
|
+
// view bar then reads "sorted by name↕⌕ ▼". data-col-label wins when set.
|
|
110
|
+
label: th.getAttribute("data-col-label") || labelOf(th),
|
|
111
|
+
type: th.getAttribute("data-sort-type") || "text",
|
|
112
|
+
filter: th.getAttribute("data-filter") || "text",
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const defaults = (inst) => ({ sortKey: inst.defaultSortKey, dir: inst.defaultDir, filters: {}, search: "" });
|
|
119
|
+
|
|
120
|
+
const activeFilters = (inst, view) => {
|
|
121
|
+
const out = {};
|
|
122
|
+
for (const col of inst.columns) {
|
|
123
|
+
const value = view.filters && view.filters[col.key];
|
|
124
|
+
if (value) out[col.key] = String(value);
|
|
125
|
+
}
|
|
126
|
+
return out;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const isDefault = (inst, view) =>
|
|
130
|
+
view.sortKey === inst.defaultSortKey && view.dir === inst.defaultDir &&
|
|
131
|
+
!view.search && Object.keys(activeFilters(inst, view)).length === 0;
|
|
132
|
+
|
|
133
|
+
function normalize(inst, raw) {
|
|
134
|
+
const view = defaults(inst);
|
|
135
|
+
let stored;
|
|
136
|
+
try { stored = JSON.parse(raw); } catch { return view; }
|
|
137
|
+
if (!stored || typeof stored !== "object" || Array.isArray(stored)) return view;
|
|
138
|
+
const known = new Set(inst.columns.map((c) => c.key));
|
|
139
|
+
if (known.has(stored.sortKey)) {
|
|
140
|
+
view.sortKey = stored.sortKey;
|
|
141
|
+
if (stored.dir === 1 || stored.dir === -1) view.dir = stored.dir;
|
|
142
|
+
}
|
|
143
|
+
if (stored.filters && typeof stored.filters === "object" && !Array.isArray(stored.filters)) {
|
|
144
|
+
for (const key of Object.keys(stored.filters)) {
|
|
145
|
+
if (!known.has(key)) continue;
|
|
146
|
+
const text = String(stored.filters[key] ?? "").trim().toLowerCase();
|
|
147
|
+
if (text) view.filters[key] = text;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (typeof stored.search === "string") view.search = stored.search.trim().toLowerCase();
|
|
151
|
+
return view;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/* Both wrapped: Safari in private mode THROWS on localStorage rather than
|
|
155
|
+
returning null, and a table that refuses to render because a preference could
|
|
156
|
+
not be read is worse than one that starts at its defaults. */
|
|
157
|
+
function save(inst) {
|
|
158
|
+
if (!inst.id) return;
|
|
159
|
+
try {
|
|
160
|
+
if (isDefault(inst, inst.view)) localStorage.removeItem(STORE_PREFIX + inst.id);
|
|
161
|
+
else localStorage.setItem(STORE_PREFIX + inst.id, JSON.stringify({
|
|
162
|
+
sortKey: inst.view.sortKey, dir: inst.view.dir,
|
|
163
|
+
filters: activeFilters(inst, inst.view), search: inst.view.search || "",
|
|
164
|
+
}));
|
|
165
|
+
} catch { /* the view still applies for this visit; only the memory is lost */ }
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function restore(inst) {
|
|
169
|
+
if (!inst.id) return defaults(inst);
|
|
170
|
+
try { return normalize(inst, localStorage.getItem(STORE_PREFIX + inst.id)); }
|
|
171
|
+
catch { return defaults(inst); }
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const matches = (inst, row) => {
|
|
175
|
+
const view = inst.view;
|
|
176
|
+
if (view.search) {
|
|
177
|
+
const hay = textOf(row).toLowerCase();
|
|
178
|
+
if (!hay.includes(view.search)) return false;
|
|
179
|
+
}
|
|
180
|
+
for (const col of inst.columns) {
|
|
181
|
+
const want = view.filters[col.key];
|
|
182
|
+
if (!want) continue;
|
|
183
|
+
const got = (cellValue(row, col.index) || "").toLowerCase();
|
|
184
|
+
// `pick` is an exact match and `text` is a contains — spelling both as
|
|
185
|
+
// "contains" would be a small lie about why a row is missing.
|
|
186
|
+
if (col.filter === "pick" ? got !== want : !got.includes(want)) return false;
|
|
187
|
+
}
|
|
188
|
+
return true;
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
export function applyTableView(table) {
|
|
192
|
+
const inst = instances.get(table);
|
|
193
|
+
if (!inst) return;
|
|
194
|
+
const body = table.tBodies[0];
|
|
195
|
+
if (!body) return;
|
|
196
|
+
|
|
197
|
+
const keep = [], drop = [];
|
|
198
|
+
for (const row of inst.allRows) (matches(inst, row) ? keep : drop).push(row);
|
|
199
|
+
|
|
200
|
+
const col = inst.columns.find((c) => c.key === inst.view.sortKey);
|
|
201
|
+
if (col) {
|
|
202
|
+
keep.sort((a, b) => compare(cellValue(a, col.index), cellValue(b, col.index), col.type, inst.view.dir));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/*
|
|
206
|
+
* A DETAIL ROW IS NOT A ROW. An expandable table puts a second <tr> under the
|
|
207
|
+
* one it belongs to — the contacts book's per-person panel, spanning every
|
|
208
|
+
* column. Treated as data it would be filtered on its own text and sorted away
|
|
209
|
+
* from its parent, which is how a detail panel ends up under a stranger.
|
|
210
|
+
*
|
|
211
|
+
* So `data-row-for="<key>"` marks a child of `data-row-key="<key>"`: it is
|
|
212
|
+
* excluded from matching and from the sort, and simply follows its parent
|
|
213
|
+
* wherever the parent lands. A child whose parent is filtered out goes with it.
|
|
214
|
+
*/
|
|
215
|
+
const frag = document.createDocumentFragment();
|
|
216
|
+
for (const row of keep) {
|
|
217
|
+
frag.appendChild(row);
|
|
218
|
+
for (const child of inst.childrenOf.get(row) || []) frag.appendChild(child);
|
|
219
|
+
}
|
|
220
|
+
for (const row of drop) {
|
|
221
|
+
for (const child of inst.childrenOf.get(row) || []) child.remove();
|
|
222
|
+
row.remove();
|
|
223
|
+
}
|
|
224
|
+
body.appendChild(frag);
|
|
225
|
+
// What we just wrote, so the observer can tell OUR output from a real
|
|
226
|
+
// re-render. A synchronous "applying" flag cannot: MutationObserver delivers
|
|
227
|
+
// asynchronously, so the flag is already back to false when the callback
|
|
228
|
+
// runs, and the observer re-enters forever. pagination.js avoids the same
|
|
229
|
+
// trap by writing only real changes; this is that discipline for a reorder.
|
|
230
|
+
inst.lastWritten = [...body.rows];
|
|
231
|
+
|
|
232
|
+
paintHeader(inst);
|
|
233
|
+
paintBar(inst);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function paintHeader(inst) {
|
|
237
|
+
for (const col of inst.columns) {
|
|
238
|
+
const sorted = inst.view.sortKey === col.key;
|
|
239
|
+
col.th.setAttribute("aria-sort", sorted ? (inst.view.dir === 1 ? "ascending" : "descending") : "none");
|
|
240
|
+
if (col.sortBtn) col.sortBtn.textContent = sorted ? (inst.view.dir === 1 ? "▲" : "▼") : "↕";
|
|
241
|
+
const on = Boolean(inst.view.filters[col.key]);
|
|
242
|
+
if (col.filterWrap) col.filterWrap.classList.toggle("is-on", on);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function paintBar(inst) {
|
|
247
|
+
const view = inst.view, bar = inst.bar;
|
|
248
|
+
const chips = [];
|
|
249
|
+
const chip = (text, title) => {
|
|
250
|
+
const s = document.createElement("span");
|
|
251
|
+
s.className = "tbl-view-chip";
|
|
252
|
+
s.textContent = text;
|
|
253
|
+
if (title) s.title = title;
|
|
254
|
+
return s;
|
|
255
|
+
};
|
|
256
|
+
const active = activeFilters(inst, view);
|
|
257
|
+
for (const col of inst.columns) {
|
|
258
|
+
const value = active[col.key];
|
|
259
|
+
if (!value) continue;
|
|
260
|
+
const exact = col.filter === "pick";
|
|
261
|
+
chips.push(chip(col.label + (exact ? " = " : " ~ ") + value,
|
|
262
|
+
exact ? `${col.label} is exactly “${value}”` : `${col.label} contains “${value}”`));
|
|
263
|
+
}
|
|
264
|
+
if (view.search) chips.push(chip("search ~ " + view.search, `any column contains “${view.search}”`));
|
|
265
|
+
if (view.sortKey !== inst.defaultSortKey || view.dir !== inst.defaultDir) {
|
|
266
|
+
const col = inst.columns.find((c) => c.key === view.sortKey);
|
|
267
|
+
if (col) chips.push(chip(`sorted by ${col.label} ${view.dir === 1 ? "▲" : "▼"}`,
|
|
268
|
+
`sorted by ${col.label}, ${view.dir === 1 ? "ascending" : "descending"}`));
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
bar.textContent = "";
|
|
272
|
+
if (!chips.length) { bar.hidden = true; return; }
|
|
273
|
+
const lead = document.createElement("span");
|
|
274
|
+
lead.className = "tbl-view-lead";
|
|
275
|
+
lead.title = "This table remembers its filters and its sort in this browser. Reset view puts it back to the order and the rows it ships with.";
|
|
276
|
+
lead.textContent = "saved view";
|
|
277
|
+
bar.appendChild(lead);
|
|
278
|
+
for (const c of chips) bar.appendChild(c);
|
|
279
|
+
const reset = document.createElement("button");
|
|
280
|
+
reset.type = "button";
|
|
281
|
+
reset.className = "btn-terminal btn-terminal--ghost btn-terminal--compact";
|
|
282
|
+
reset.setAttribute("data-view-reset", "");
|
|
283
|
+
reset.textContent = "reset view";
|
|
284
|
+
bar.appendChild(reset);
|
|
285
|
+
bar.hidden = false;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function buildHeaderControls(inst) {
|
|
289
|
+
for (const col of inst.columns) {
|
|
290
|
+
const tools = document.createElement("span");
|
|
291
|
+
tools.className = "tbl-tools";
|
|
292
|
+
|
|
293
|
+
const sort = document.createElement("button");
|
|
294
|
+
sort.type = "button";
|
|
295
|
+
sort.className = "tbl-sort";
|
|
296
|
+
sort.setAttribute("data-tip", "sort by " + col.label);
|
|
297
|
+
sort.setAttribute("aria-label", "sort by " + col.label);
|
|
298
|
+
sort.textContent = "↕";
|
|
299
|
+
sort.addEventListener("click", () => {
|
|
300
|
+
if (inst.view.sortKey === col.key) inst.view.dir = inst.view.dir === 1 ? -1 : 1;
|
|
301
|
+
else { inst.view.sortKey = col.key; inst.view.dir = 1; }
|
|
302
|
+
save(inst); applyTableView(inst.table);
|
|
303
|
+
});
|
|
304
|
+
col.sortBtn = sort;
|
|
305
|
+
tools.appendChild(sort);
|
|
306
|
+
|
|
307
|
+
const wrap = document.createElement("details");
|
|
308
|
+
wrap.className = "dropdown tbl-filter";
|
|
309
|
+
const summary = document.createElement("summary");
|
|
310
|
+
summary.setAttribute("data-tip", "filter " + col.label);
|
|
311
|
+
summary.setAttribute("aria-label", "filter " + col.label);
|
|
312
|
+
summary.textContent = "⌕";
|
|
313
|
+
wrap.appendChild(summary);
|
|
314
|
+
|
|
315
|
+
const panel = document.createElement("div");
|
|
316
|
+
panel.className = "dropdown-panel dropdown-panel--down tbl-filter-panel";
|
|
317
|
+
|
|
318
|
+
if (col.filter === "pick") {
|
|
319
|
+
// The list is built from the column's own cells, so it can never offer a
|
|
320
|
+
// value the table does not contain.
|
|
321
|
+
const seen = new Map();
|
|
322
|
+
for (const row of inst.allRows) {
|
|
323
|
+
const raw = cellValue(row, col.index);
|
|
324
|
+
if (raw) seen.set(raw.toLowerCase(), raw);
|
|
325
|
+
}
|
|
326
|
+
const any = document.createElement("button");
|
|
327
|
+
any.type = "button"; any.className = "dropdown-item"; any.textContent = "(any)";
|
|
328
|
+
any.addEventListener("click", () => {
|
|
329
|
+
inst.view.filters[col.key] = ""; wrap.open = false; save(inst); applyTableView(inst.table);
|
|
330
|
+
});
|
|
331
|
+
panel.appendChild(any);
|
|
332
|
+
for (const [lower, label] of [...seen.entries()].sort((a, b) => a[1].localeCompare(b[1]))) {
|
|
333
|
+
const b = document.createElement("button");
|
|
334
|
+
b.type = "button"; b.className = "dropdown-item"; b.textContent = label;
|
|
335
|
+
b.addEventListener("click", () => {
|
|
336
|
+
inst.view.filters[col.key] = lower; wrap.open = false; save(inst); applyTableView(inst.table);
|
|
337
|
+
});
|
|
338
|
+
panel.appendChild(b);
|
|
339
|
+
}
|
|
340
|
+
} else {
|
|
341
|
+
const input = document.createElement("input");
|
|
342
|
+
input.type = "search";
|
|
343
|
+
input.className = "tbl-filter-input";
|
|
344
|
+
input.placeholder = col.label + " contains…";
|
|
345
|
+
input.setAttribute("aria-label", "filter " + col.label);
|
|
346
|
+
// cockpit learned this one the expensive way: without it a password
|
|
347
|
+
// manager offers to fill every filter box on the page.
|
|
348
|
+
input.setAttribute("data-1p-ignore", "");
|
|
349
|
+
input.addEventListener("input", () => {
|
|
350
|
+
inst.view.filters[col.key] = input.value.trim().toLowerCase();
|
|
351
|
+
save(inst); applyTableView(inst.table);
|
|
352
|
+
});
|
|
353
|
+
col.filterInput = input;
|
|
354
|
+
panel.appendChild(input);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
wrap.appendChild(panel);
|
|
358
|
+
col.filterWrap = wrap;
|
|
359
|
+
tools.appendChild(wrap);
|
|
360
|
+
col.th.appendChild(tools);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/*
|
|
365
|
+
* Re-read the rows and the header from the DOM as it is NOW.
|
|
366
|
+
*
|
|
367
|
+
* Every table in this estate that is worth filtering is rendered from data and
|
|
368
|
+
* rewritten wholesale — `contact-rows.innerHTML = …` on the contacts book,
|
|
369
|
+
* cockpit's in-place patcher on its automation tables, a 30-second poll behind
|
|
370
|
+
* both. A component that snapshotted its rows once would hold a list of detached
|
|
371
|
+
* <tr>s after the first repaint and quietly filter nothing, and its header
|
|
372
|
+
* controls would be gone with the <thead> that carried them.
|
|
373
|
+
*/
|
|
374
|
+
function snapshot(inst) {
|
|
375
|
+
const body = inst.table.tBodies[0];
|
|
376
|
+
if (!body) return;
|
|
377
|
+
const rows = [...body.rows];
|
|
378
|
+
inst.childrenOf = new Map();
|
|
379
|
+
inst.allRows = [];
|
|
380
|
+
const byKey = new Map();
|
|
381
|
+
for (const row of rows) {
|
|
382
|
+
const parentKey = row.getAttribute("data-row-for");
|
|
383
|
+
if (parentKey === null) {
|
|
384
|
+
inst.allRows.push(row);
|
|
385
|
+
const key = row.getAttribute("data-row-key");
|
|
386
|
+
if (key !== null) byKey.set(key, row);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
for (const row of rows) {
|
|
390
|
+
const parentKey = row.getAttribute("data-row-for");
|
|
391
|
+
if (parentKey === null) continue;
|
|
392
|
+
const parent = byKey.get(parentKey);
|
|
393
|
+
// An orphan detail row — parent filtered out by the page itself, or a stale
|
|
394
|
+
// key — is left as ordinary content rather than dropped. Removing a row
|
|
395
|
+
// because its key did not resolve would delete data over a typo.
|
|
396
|
+
if (!parent) { inst.allRows.push(row); continue; }
|
|
397
|
+
if (!inst.childrenOf.has(parent)) inst.childrenOf.set(parent, []);
|
|
398
|
+
inst.childrenOf.get(parent).push(row);
|
|
399
|
+
}
|
|
400
|
+
// The <thead> may have been replaced too, so the column objects must be
|
|
401
|
+
// re-read and the controls re-injected onto the cells that exist now.
|
|
402
|
+
const fresh = columnsOf(inst.table);
|
|
403
|
+
if (fresh.length) {
|
|
404
|
+
// Carry the CONTROLS across, not just the inputs. columnsOf() builds new
|
|
405
|
+
// column objects, and paintHeader() writes the sort glyph and the active-filter
|
|
406
|
+
// mark through col.sortBtn / col.filterWrap — drop those and the header stops
|
|
407
|
+
// reporting the state it is actually in. Measured: aria-sort said "ascending"
|
|
408
|
+
// while the button still showed the neutral glyph.
|
|
409
|
+
for (const col of fresh) {
|
|
410
|
+
const prev = inst.columns.find((c) => c.key === col.key);
|
|
411
|
+
if (!prev) continue;
|
|
412
|
+
col.filterInput = prev.filterInput;
|
|
413
|
+
col.sortBtn = prev.sortBtn;
|
|
414
|
+
col.filterWrap = prev.filterWrap;
|
|
415
|
+
}
|
|
416
|
+
inst.columns = fresh;
|
|
417
|
+
if (!inst.table.querySelector(".tbl-tools")) buildHeaderControls(inst);
|
|
418
|
+
else refreshPickOptions(inst);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/*
|
|
423
|
+
* A `pick` list is derived from the column's own cells, so it has to be rebuilt
|
|
424
|
+
* when those cells change — a re-render that introduces a new relationship would
|
|
425
|
+
* otherwise leave a dropdown that cannot offer it, and the reader would conclude
|
|
426
|
+
* the value does not exist. Rebuilt only when the set actually differs, so an
|
|
427
|
+
* open dropdown is not torn out from under the pointer on every poll.
|
|
428
|
+
*/
|
|
429
|
+
function refreshPickOptions(inst) {
|
|
430
|
+
for (const col of inst.columns) {
|
|
431
|
+
if (col.filter !== "pick" || !col.filterWrap) continue;
|
|
432
|
+
const seen = new Map();
|
|
433
|
+
for (const row of inst.allRows) {
|
|
434
|
+
const raw = cellValue(row, col.index);
|
|
435
|
+
if (raw) seen.set(raw.toLowerCase(), raw);
|
|
436
|
+
}
|
|
437
|
+
const wanted = [...seen.entries()].sort((a, b) => a[1].localeCompare(b[1]));
|
|
438
|
+
const panel = col.filterWrap.querySelector(".tbl-filter-panel");
|
|
439
|
+
if (!panel) continue;
|
|
440
|
+
const current = [...panel.querySelectorAll(".dropdown-item")].slice(1).map((b) => b.textContent);
|
|
441
|
+
if (current.length === wanted.length && current.every((v, i) => v === wanted[i][1])) continue;
|
|
442
|
+
for (const b of [...panel.querySelectorAll(".dropdown-item")].slice(1)) b.remove();
|
|
443
|
+
for (const [lower, label] of wanted) {
|
|
444
|
+
const b = document.createElement("button");
|
|
445
|
+
b.type = "button"; b.className = "dropdown-item"; b.textContent = label;
|
|
446
|
+
b.addEventListener("click", () => {
|
|
447
|
+
inst.view.filters[col.key] = lower;
|
|
448
|
+
col.filterWrap.open = false;
|
|
449
|
+
save(inst); applyTableView(inst.table);
|
|
450
|
+
});
|
|
451
|
+
panel.appendChild(b);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function enhance(table) {
|
|
457
|
+
if (instances.has(table)) return;
|
|
458
|
+
const columns = columnsOf(table);
|
|
459
|
+
if (!columns.length) return;
|
|
460
|
+
const body = table.tBodies[0];
|
|
461
|
+
if (!body) return;
|
|
462
|
+
|
|
463
|
+
const inst = {
|
|
464
|
+
table, columns,
|
|
465
|
+
id: table.getAttribute("data-table-id") || "",
|
|
466
|
+
allRows: [],
|
|
467
|
+
childrenOf: new Map(),
|
|
468
|
+
lastWritten: [],
|
|
469
|
+
defaultSortKey: table.getAttribute("data-sort-key") || columns[0].key,
|
|
470
|
+
defaultDir: table.getAttribute("data-sort-dir") === "desc" ? -1 : 1,
|
|
471
|
+
};
|
|
472
|
+
instances.set(table, inst);
|
|
473
|
+
inst.view = restore(inst);
|
|
474
|
+
|
|
475
|
+
// Before the scroll wrapper, never inside it — `.tablewrap` scrolls sideways,
|
|
476
|
+
// and a search box in there slides out of reach on exactly the wide tables
|
|
477
|
+
// that need one. Same reasoning as the pager's anchor, opposite side.
|
|
478
|
+
const anchor = table.closest(".tablewrap") || table;
|
|
479
|
+
|
|
480
|
+
const toolbar = document.createElement("div");
|
|
481
|
+
toolbar.className = "tbl-toolbar";
|
|
482
|
+
const search = document.createElement("input");
|
|
483
|
+
search.type = "search";
|
|
484
|
+
search.className = "tbl-search";
|
|
485
|
+
search.placeholder = "search this table…";
|
|
486
|
+
search.setAttribute("aria-label", "search this table");
|
|
487
|
+
search.setAttribute("data-1p-ignore", "");
|
|
488
|
+
search.value = inst.view.search || "";
|
|
489
|
+
search.addEventListener("input", () => {
|
|
490
|
+
inst.view.search = search.value.trim().toLowerCase();
|
|
491
|
+
save(inst); applyTableView(table);
|
|
492
|
+
});
|
|
493
|
+
toolbar.appendChild(search);
|
|
494
|
+
inst.searchInput = search;
|
|
495
|
+
|
|
496
|
+
const bar = document.createElement("div");
|
|
497
|
+
bar.className = "tbl-view";
|
|
498
|
+
bar.hidden = true;
|
|
499
|
+
bar.addEventListener("click", (event) => {
|
|
500
|
+
if (!event.target.closest("[data-view-reset]")) return;
|
|
501
|
+
inst.view = defaults(inst);
|
|
502
|
+
if (inst.searchInput) inst.searchInput.value = "";
|
|
503
|
+
for (const col of inst.columns) if (col.filterInput) col.filterInput.value = "";
|
|
504
|
+
save(inst); applyTableView(table);
|
|
505
|
+
});
|
|
506
|
+
inst.bar = bar;
|
|
507
|
+
|
|
508
|
+
anchor.before(toolbar);
|
|
509
|
+
anchor.before(bar);
|
|
510
|
+
|
|
511
|
+
// snapshot() builds the header controls itself when they are absent, and it
|
|
512
|
+
// must run FIRST: a `pick` column's option list is derived from the rows, so
|
|
513
|
+
// building the controls against an empty set yields a dropdown offering only
|
|
514
|
+
// "(any)".
|
|
515
|
+
snapshot(inst);
|
|
516
|
+
for (const col of inst.columns) {
|
|
517
|
+
if (col.filterInput) col.filterInput.value = inst.view.filters[col.key] || "";
|
|
518
|
+
}
|
|
519
|
+
applyTableView(table);
|
|
520
|
+
|
|
521
|
+
/*
|
|
522
|
+
* WATCHING THE TABLE, NOT THE TBODY — a renderer is entitled to replace the
|
|
523
|
+
* tbody node rather than fill it, and an observer bound to the old one would
|
|
524
|
+
* be left watching a detached element. Same reasoning as pagination.js.
|
|
525
|
+
*
|
|
526
|
+
* The guard is an IDENTITY CHECK against the row order we last wrote, not a
|
|
527
|
+
* flag. applyTableView() moves every row, so without a guard this re-enters
|
|
528
|
+
* forever — and a synchronous flag cannot close it, because MutationObserver
|
|
529
|
+
* delivers asynchronously and the flag is already cleared by then. Measured:
|
|
530
|
+
* the first version hung the page.
|
|
531
|
+
*/
|
|
532
|
+
inst.observer = new MutationObserver(() => {
|
|
533
|
+
const body = table.tBodies[0];
|
|
534
|
+
if (!body) return;
|
|
535
|
+
const now = body.rows;
|
|
536
|
+
const last = inst.lastWritten || [];
|
|
537
|
+
if (now.length === last.length && last.every((row, i) => now[i] === row)) return;
|
|
538
|
+
snapshot(inst);
|
|
539
|
+
applyTableView(table);
|
|
540
|
+
});
|
|
541
|
+
inst.observer.observe(table, { childList: true, subtree: true });
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Give every `<table data-table-tools>` a search box, per-column sort and filter
|
|
546
|
+
* controls in its header, and a bar naming whatever is currently in force.
|
|
547
|
+
*
|
|
548
|
+
* Markup contract: `<th data-col="key">` on each column that participates.
|
|
549
|
+
* Optional: `data-filter="pick"` for a value list instead of a text box,
|
|
550
|
+
* `data-sort-type="num"`, `data-col-label`, and `data-value` on a `<td>` to sort
|
|
551
|
+
* by something other than what it prints.
|
|
552
|
+
*
|
|
553
|
+
* @param {ParentNode} [root=document]
|
|
554
|
+
*/
|
|
555
|
+
export function initTableTools(root = document) {
|
|
556
|
+
for (const table of root.querySelectorAll("table[data-table-tools]")) enhance(table);
|
|
557
|
+
}
|
package/src/components.css
CHANGED
|
@@ -1046,3 +1046,122 @@ html.dgm-locked, html.dgm-locked body { overflow: hidden; }
|
|
|
1046
1046
|
|
|
1047
1047
|
/* The strip is navigation for a screen. On paper the page IS the map. */
|
|
1048
1048
|
@media print { .minimap { display: none !important; } }
|
|
1049
|
+
|
|
1050
|
+
/* ── TABLE TOOLS: search on top, sort + filter in the header ──────────────────
|
|
1051
|
+
Paired with runtime/tabletools.js. The estate grew four generations of table
|
|
1052
|
+
filtering before this existed — cockpit's `tr.act-filters`, cockpit's older
|
|
1053
|
+
`tr.filters`, the family contacts table's bare box row — and each spent a
|
|
1054
|
+
whole row of vertical space advertising a capability idle on most visits.
|
|
1055
|
+
|
|
1056
|
+
Two controls in the <th> cost nothing when unused and sit on the column they
|
|
1057
|
+
act on. The filter is a `details.dropdown`, the SYSTEM's dropdown, so
|
|
1058
|
+
one-open / click-away / Escape come from initDropdowns() and are not
|
|
1059
|
+
reimplemented here. Only the parts that are genuinely table-specific are
|
|
1060
|
+
declared below. */
|
|
1061
|
+
.tbl-toolbar {
|
|
1062
|
+
display: flex;
|
|
1063
|
+
align-items: center;
|
|
1064
|
+
gap: 0.5rem;
|
|
1065
|
+
margin-block-end: 0.5rem;
|
|
1066
|
+
}
|
|
1067
|
+
.tbl-search {
|
|
1068
|
+
flex: 1 1 auto;
|
|
1069
|
+
min-inline-size: 0;
|
|
1070
|
+
font: inherit;
|
|
1071
|
+
font-size: var(--fs-base);
|
|
1072
|
+
color: var(--foreground);
|
|
1073
|
+
background: var(--background);
|
|
1074
|
+
border: 1px solid var(--border);
|
|
1075
|
+
padding: 0.25rem 0.5rem;
|
|
1076
|
+
}
|
|
1077
|
+
.tbl-search:focus-visible { outline: 2px solid var(--ring); outline-offset: 1px; }
|
|
1078
|
+
|
|
1079
|
+
/* The header cell keeps its label on the baseline and parks the controls at its
|
|
1080
|
+
trailing edge, so a column of numbers still reads as a column of numbers. */
|
|
1081
|
+
.tbl-tools {
|
|
1082
|
+
display: inline-flex;
|
|
1083
|
+
align-items: center;
|
|
1084
|
+
gap: 0.15rem;
|
|
1085
|
+
margin-inline-start: 0.4rem;
|
|
1086
|
+
vertical-align: baseline;
|
|
1087
|
+
}
|
|
1088
|
+
.tbl-sort,
|
|
1089
|
+
.tbl-filter > summary {
|
|
1090
|
+
background: transparent;
|
|
1091
|
+
border: 0;
|
|
1092
|
+
padding: 0 0.15rem;
|
|
1093
|
+
font: inherit;
|
|
1094
|
+
font-size: var(--fs-base);
|
|
1095
|
+
line-height: 1;
|
|
1096
|
+
color: var(--muted-foreground);
|
|
1097
|
+
cursor: pointer;
|
|
1098
|
+
opacity: 0.55;
|
|
1099
|
+
transition: opacity 0.15s ease, color 0.15s ease;
|
|
1100
|
+
}
|
|
1101
|
+
.tbl-sort:hover,
|
|
1102
|
+
.tbl-filter > summary:hover,
|
|
1103
|
+
th[aria-sort]:not([aria-sort="none"]) .tbl-sort { opacity: 1; color: var(--primary); }
|
|
1104
|
+
.tbl-sort:focus-visible,
|
|
1105
|
+
.tbl-filter > summary:focus-visible { outline: 2px solid var(--ring); outline-offset: 1px; opacity: 1; }
|
|
1106
|
+
|
|
1107
|
+
/* A COLUMN THAT IS FILTERING SAYS SO ON ITSELF, not only in the bar above. The
|
|
1108
|
+
bar can be scrolled off a long table; the header cannot, because it is what
|
|
1109
|
+
the reader is looking at when they wonder where a row went. */
|
|
1110
|
+
.tbl-filter.is-on > summary { opacity: 1; color: var(--primary); }
|
|
1111
|
+
.tbl-filter.is-on > summary::after { content: "•"; margin-inline-start: 0.1em; }
|
|
1112
|
+
|
|
1113
|
+
.tbl-filter-panel { padding: 0.4rem; max-block-size: 60vh; overflow-y: auto; }
|
|
1114
|
+
.tbl-filter-input {
|
|
1115
|
+
font: inherit;
|
|
1116
|
+
font-size: var(--fs-base);
|
|
1117
|
+
color: var(--foreground);
|
|
1118
|
+
background: var(--background);
|
|
1119
|
+
border: 1px solid var(--border);
|
|
1120
|
+
padding: 0.2rem 0.4rem;
|
|
1121
|
+
inline-size: 12rem;
|
|
1122
|
+
max-inline-size: 100%;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
/* ── WHAT IS IN FORCE, AND THE WAY BACK ───────────────────────────────────────
|
|
1126
|
+
A table that is already withholding rows for a reason nobody on screen gave is
|
|
1127
|
+
the failure this prevents: an empty table and a filtered one look nearly
|
|
1128
|
+
identical. The bar names every filter and any non-default sort, and carries
|
|
1129
|
+
the only way back.
|
|
1130
|
+
|
|
1131
|
+
`[hidden]` IS NOT OPTIONAL. `[hidden] { display: none }` is a UA rule at the
|
|
1132
|
+
lowest specificity, so `display: flex` here would beat it and the bar would
|
|
1133
|
+
render on every table whether or not anything was in force — the indicator
|
|
1134
|
+
wearing the very fault it reports. */
|
|
1135
|
+
.tbl-view {
|
|
1136
|
+
display: flex;
|
|
1137
|
+
flex-wrap: wrap;
|
|
1138
|
+
align-items: center;
|
|
1139
|
+
gap: 0.4rem;
|
|
1140
|
+
margin-block-end: 0.6rem;
|
|
1141
|
+
padding: 0.35rem 0.6rem;
|
|
1142
|
+
font-size: var(--fs-base);
|
|
1143
|
+
border-inline-start: 3px solid var(--primary);
|
|
1144
|
+
background: color-mix(in srgb, var(--primary) 10%, var(--background));
|
|
1145
|
+
}
|
|
1146
|
+
.tbl-view[hidden] { display: none; }
|
|
1147
|
+
.tbl-view-lead { color: var(--primary); font-weight: 700; }
|
|
1148
|
+
.tbl-view-lead::before { content: "▸ "; }
|
|
1149
|
+
.tbl-view-chip {
|
|
1150
|
+
border: 1px solid var(--border);
|
|
1151
|
+
background: var(--card);
|
|
1152
|
+
padding: 0.05rem 0.4rem;
|
|
1153
|
+
white-space: nowrap;
|
|
1154
|
+
}
|
|
1155
|
+
/* The way out sits at the far end of the line it undoes, and falls in beside the
|
|
1156
|
+
chips when the row wraps on a phone rather than stranding itself on a line. */
|
|
1157
|
+
.tbl-view [data-view-reset] { margin-inline-start: auto; }
|
|
1158
|
+
|
|
1159
|
+
@media (pointer: coarse) {
|
|
1160
|
+
.tbl-sort, .tbl-filter > summary { min-inline-size: 44px; min-block-size: 44px;
|
|
1161
|
+
display: inline-flex; align-items: center; justify-content: center; }
|
|
1162
|
+
}
|
|
1163
|
+
@media print {
|
|
1164
|
+
.tbl-toolbar, .tbl-tools { display: none !important; }
|
|
1165
|
+
/* The bar PRINTS. A printed table that is silently filtered is the same
|
|
1166
|
+
failure on paper, with no way to interrogate it at all. */
|
|
1167
|
+
}
|
|
@@ -85,13 +85,13 @@
|
|
|
85
85
|
on docs.danieldeusing.de, NOT for a doc opened from disk over file://. -->
|
|
86
86
|
<link
|
|
87
87
|
rel="stylesheet"
|
|
88
|
-
href="https://cdn.jsdelivr.net/npm/@danieldeusing/design@0.
|
|
89
|
-
onerror="this.onerror=null;this.href='/_design/danieldeusing-design-0.
|
|
88
|
+
href="https://cdn.jsdelivr.net/npm/@danieldeusing/design@0.31.0/dist/danieldeusing-design.min.css"
|
|
89
|
+
onerror="this.onerror=null;this.href='/_design/danieldeusing-design-0.31.0.min.css'"
|
|
90
90
|
/>
|
|
91
91
|
<link
|
|
92
92
|
rel="stylesheet"
|
|
93
|
-
href="https://cdn.jsdelivr.net/npm/@danieldeusing/design@0.
|
|
94
|
-
onerror="this.onerror=null;this.href='/_design/danieldeusing-design-0.
|
|
93
|
+
href="https://cdn.jsdelivr.net/npm/@danieldeusing/design@0.31.0/src/fonts.css"
|
|
94
|
+
onerror="this.onerror=null;this.href='/_design/danieldeusing-design-0.31.0.fonts.css'"
|
|
95
95
|
/>
|
|
96
96
|
|
|
97
97
|
<style>
|
|
@@ -330,7 +330,7 @@ flowchart TD
|
|
|
330
330
|
|
|
331
331
|
<script type="module">
|
|
332
332
|
import { applyStoredTheme, initThemeSwitcher, initDropdowns, initSelects, initTerminal, initAnimToggle, initBurgerNav, initLsNav, initTableScroll, initDiagramZoom, initMinimap, initTooltips } from
|
|
333
|
-
"https://cdn.jsdelivr.net/npm/@danieldeusing/design@0.
|
|
333
|
+
"https://cdn.jsdelivr.net/npm/@danieldeusing/design@0.31.0/runtime/index.js";
|
|
334
334
|
applyStoredTheme();
|
|
335
335
|
initThemeSwitcher();
|
|
336
336
|
initDropdowns();
|