@danieldeusing/design 0.20.0 → 0.21.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 +3 -1
- package/dist/danieldeusing-design.css +203 -1
- package/dist/danieldeusing-design.min.css +2 -2
- package/package.json +1 -1
- package/runtime/index.js +1 -0
- package/runtime/select.js +539 -0
- package/src/components.css +201 -0
- package/src/print.css +1 -0
- package/templates/documentation.html +11 -6
- package/templates/page-chrome.html +6 -1
package/README.md
CHANGED
|
@@ -110,15 +110,17 @@ Four dependency-free ES modules, tree-shakeable from `@danieldeusing/design/runt
|
|
|
110
110
|
| `setTheme(name)` / `initThemeSwitcher()` | Switch themes and wire `[data-theme-value]` buttons + `[data-theme-label]`. |
|
|
111
111
|
| `initTerminal()` | The `$ command` typing animation. No-ops under reduced motion / `html.anim-off`. |
|
|
112
112
|
| `initDropdowns()` | `<details class="dropdown">` behaviour: one-open, click-away, Escape. |
|
|
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. |
|
|
113
114
|
| `initAnimToggle()` | Wire `[data-anim-toggle]` buttons (`.anim-toggle`) to flip `html.anim-off` + persist it. |
|
|
114
115
|
| `initResolutionZoom(1920)` | Scale the whole layout up on screens wider than the reference width. |
|
|
115
116
|
|
|
116
117
|
```js
|
|
117
|
-
import { applyStoredTheme, initThemeSwitcher, initDropdowns, initTerminal, initAnimToggle, initResolutionZoom } from "@danieldeusing/design/runtime";
|
|
118
|
+
import { applyStoredTheme, initThemeSwitcher, initDropdowns, initSelects, initTerminal, initAnimToggle, initResolutionZoom } from "@danieldeusing/design/runtime";
|
|
118
119
|
applyStoredTheme(); // ideally inline, pre-paint
|
|
119
120
|
initResolutionZoom(); // ideally inline, pre-paint (no flash)
|
|
120
121
|
initThemeSwitcher();
|
|
121
122
|
initDropdowns();
|
|
123
|
+
initSelects();
|
|
122
124
|
initTerminal();
|
|
123
125
|
initAnimToggle();
|
|
124
126
|
```
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! danieldeusing-design v0.
|
|
1
|
+
/*! danieldeusing-design v0.21.0 | MIT | https://github.com/danieldeusing/danieldeusing-design */
|
|
2
2
|
/* ===== reset.css ===== */
|
|
3
3
|
/*
|
|
4
4
|
* danieldeusing-design — minimal reset.
|
|
@@ -798,6 +798,207 @@ html[data-theme="warm"] .dropdown-item[data-theme-value="warm"] {
|
|
|
798
798
|
text-shadow: 0 0 8px var(--glow);
|
|
799
799
|
}
|
|
800
800
|
|
|
801
|
+
/* ─────────────────────────────────────────────────────────────────────────
|
|
802
|
+
Select — the estate's own dropdown, because a native one's list CANNOT be
|
|
803
|
+
styled. Pairs with runtime/select.js.
|
|
804
|
+
|
|
805
|
+
THE CONSTRAINT THAT DECIDES EVERYTHING HERE: a `<select>`'s option list is
|
|
806
|
+
drawn by the OPERATING SYSTEM, outside the page, and no stylesheet reaches
|
|
807
|
+
it. So every surface in the estate has been rendering a rounded, blue-
|
|
808
|
+
highlighted, system-font menu out of a terminal UI — the one component that
|
|
809
|
+
could not be made to look like the rest, no matter what the page declared.
|
|
810
|
+
`appearance: base-select` would fix it in Chrome 135+ and nowhere else, so
|
|
811
|
+
Safari would keep the system menu: the estate would then be inconsistent
|
|
812
|
+
WITH ITSELF, which is worse than being consistently wrong.
|
|
813
|
+
|
|
814
|
+
So the list is rebuilt in the page. runtime/select.js keeps the real
|
|
815
|
+
`<select>` as the value (it is still what a form submits and what page code
|
|
816
|
+
reads) and paints a button + listbox over it. THE MARKUP CONTRACT IS
|
|
817
|
+
NOTHING: author a plain `<select>`, exactly as with `<table>`.
|
|
818
|
+
|
|
819
|
+
TWO RENDERINGS, ONE DECLARATION. The bare `<select>` (JS off, or before the
|
|
820
|
+
runtime has run) and the `.select-trigger` that replaces it must be the same
|
|
821
|
+
control at the same size — a reader who tabs into a page mid-enhancement
|
|
822
|
+
must not see the box change shape. Splitting their look across two rules is
|
|
823
|
+
how they would drift, so the closed control is declared ONCE for both, by
|
|
824
|
+
ELEMENT and by class together. Five copies of `.cfg-sel` in cockpit are what
|
|
825
|
+
this replaces; none of them agreed about `:disabled`.
|
|
826
|
+
|
|
827
|
+
THE BORDER IS 60% OF --foreground, MEASURED, NOT --border. A control's edge
|
|
828
|
+
is the thing that says "this is a control" and WCAG 1.4.11 wants 3:1 for
|
|
829
|
+
that; `--border` is a CONTAINER hairline and measures 1.37 / 2.00 / 1.61 /
|
|
830
|
+
1.42 against `--background` on the four themes — invisible as a control
|
|
831
|
+
edge, and correct for the cards it was made for. The estate's hand-rolled
|
|
832
|
+
selects used `currentColor 30%`, which is 1.70 at worst. 60% is the first
|
|
833
|
+
step that clears 3:1 on all four themes against ALL THREE surfaces a control
|
|
834
|
+
can land on (worst: warm 3.24 over --background, 3.24 over --muted). Mixed
|
|
835
|
+
with `transparent` rather than with a surface so it composites correctly
|
|
836
|
+
wherever it is dropped, and derived from a token so it stays per-theme.
|
|
837
|
+
|
|
838
|
+
THE CARET IS TWO GRADIENT HALVES, not an SVG. It has to be `currentColor` —
|
|
839
|
+
a data-URI with a baked fill would need declaring four times or be wrong on
|
|
840
|
+
two themes (the luminance gap in tokens.css), and a `::after` is impossible
|
|
841
|
+
because a `<select>` cannot have generated content. Two abutting squares,
|
|
842
|
+
each half-filled on the diagonal, draw the same ▾ on both renderings.
|
|
843
|
+
───────────────────────────────────────────────────────────────────────── */
|
|
844
|
+
select,
|
|
845
|
+
.select-trigger {
|
|
846
|
+
appearance: none;
|
|
847
|
+
-webkit-appearance: none;
|
|
848
|
+
max-width: 100%;
|
|
849
|
+
padding: 0.28rem 1.45rem 0.28rem 0.5rem;
|
|
850
|
+
font: inherit;
|
|
851
|
+
font-size: var(--fs-sm);
|
|
852
|
+
line-height: var(--lh-tight);
|
|
853
|
+
text-align: left;
|
|
854
|
+
color: var(--foreground);
|
|
855
|
+
background-color: transparent;
|
|
856
|
+
background-image:
|
|
857
|
+
linear-gradient(45deg, transparent 50%, currentColor 50%),
|
|
858
|
+
linear-gradient(135deg, currentColor 50%, transparent 50%);
|
|
859
|
+
background-position:
|
|
860
|
+
right 0.78rem center,
|
|
861
|
+
right 0.5rem center;
|
|
862
|
+
background-size: 0.28rem 0.28rem;
|
|
863
|
+
background-repeat: no-repeat;
|
|
864
|
+
border: 1px solid color-mix(in srgb, var(--foreground) 60%, transparent);
|
|
865
|
+
cursor: pointer;
|
|
866
|
+
transition: border-color 0.15s ease;
|
|
867
|
+
}
|
|
868
|
+
select:hover,
|
|
869
|
+
.select-trigger:hover,
|
|
870
|
+
.select-trigger[aria-expanded="true"] {
|
|
871
|
+
border-color: var(--primary);
|
|
872
|
+
}
|
|
873
|
+
select:disabled,
|
|
874
|
+
.select-trigger:disabled {
|
|
875
|
+
opacity: 0.45;
|
|
876
|
+
cursor: default;
|
|
877
|
+
}
|
|
878
|
+
/* Open: the caret turns over, so the control says "the list is out" without
|
|
879
|
+
relying on the panel being in view — it may have flipped above the trigger. */
|
|
880
|
+
.select-trigger[aria-expanded="true"] {
|
|
881
|
+
background-image:
|
|
882
|
+
linear-gradient(135deg, transparent 50%, currentColor 50%),
|
|
883
|
+
linear-gradient(45deg, currentColor 50%, transparent 50%);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
/* The wrapper the runtime puts around the pair. It is `relative` so the real
|
|
887
|
+
`<select>` can be laid exactly over the trigger: NOT `display: none`, and not
|
|
888
|
+
`visibility: hidden` either — Chrome refuses to show a validation bubble on a
|
|
889
|
+
control it cannot focus and then blocks the submit with no message at all, so
|
|
890
|
+
a `required` select would silently stop working. Transparent and over the
|
|
891
|
+
trigger, the bubble still points at the right box. */
|
|
892
|
+
.select-field {
|
|
893
|
+
position: relative;
|
|
894
|
+
display: inline-flex;
|
|
895
|
+
max-width: 100%;
|
|
896
|
+
vertical-align: middle;
|
|
897
|
+
}
|
|
898
|
+
.select-field > select {
|
|
899
|
+
position: absolute;
|
|
900
|
+
inset: 0;
|
|
901
|
+
width: 100%;
|
|
902
|
+
height: 100%;
|
|
903
|
+
opacity: 0;
|
|
904
|
+
pointer-events: none;
|
|
905
|
+
}
|
|
906
|
+
.select-trigger {
|
|
907
|
+
display: inline-flex;
|
|
908
|
+
align-items: center;
|
|
909
|
+
width: 100%;
|
|
910
|
+
}
|
|
911
|
+
.select-value {
|
|
912
|
+
/* min-width: 0 is what lets a flex item shrink below its content — without it
|
|
913
|
+
`text-overflow: ellipsis` never fires and a long option label blows the
|
|
914
|
+
control out of its column instead of being clipped. */
|
|
915
|
+
min-width: 0;
|
|
916
|
+
overflow: hidden;
|
|
917
|
+
text-overflow: ellipsis;
|
|
918
|
+
white-space: nowrap;
|
|
919
|
+
}
|
|
920
|
+
/* An empty flex item has no line box, so a select whose current option has no
|
|
921
|
+
text (`<option value="">`) would render a control HALF THE HEIGHT of the one
|
|
922
|
+
beside it. A zero-width space gives it a line and no width, and screen readers
|
|
923
|
+
ignore it — unlike an ` `, which they read out. */
|
|
924
|
+
.select-value::after {
|
|
925
|
+
content: "\200b";
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
/* THE PANEL IS position: fixed AND APPENDED OUT OF THE FLOW, for two reasons
|
|
929
|
+
this estate hits constantly: selects live inside `.tablewrap` (which scrolls,
|
|
930
|
+
so it clips) and inside modal `<dialog>`s (a top layer, which nothing outside
|
|
931
|
+
can paint above). An absolutely-positioned panel would be cut off in the
|
|
932
|
+
first and hidden behind the second. runtime/select.js appends it to the
|
|
933
|
+
nearest open <dialog> when there is one and to <body> otherwise.
|
|
934
|
+
z-index 60 clears the scanline overlay (40), .dropdown-panel (50) and
|
|
935
|
+
footer.status (50). */
|
|
936
|
+
.select-panel {
|
|
937
|
+
position: fixed;
|
|
938
|
+
z-index: 60;
|
|
939
|
+
margin: 0;
|
|
940
|
+
padding: 0.15rem 0;
|
|
941
|
+
overflow-y: auto;
|
|
942
|
+
overscroll-behavior: contain;
|
|
943
|
+
list-style: none;
|
|
944
|
+
font-size: var(--fs-sm);
|
|
945
|
+
line-height: var(--lh-tight);
|
|
946
|
+
color: var(--popover-foreground);
|
|
947
|
+
background: var(--popover);
|
|
948
|
+
border: 1px solid color-mix(in srgb, var(--foreground) 60%, transparent);
|
|
949
|
+
box-shadow: 0 0 20px var(--glow-soft);
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
/* SELECTION CANNOT BE A COLOUR HERE, and that is measured, not felt. `--primary`
|
|
953
|
+
against `--popover-foreground` — the selected option's ink against its
|
|
954
|
+
neighbours' — is 1.65:1 on warm, 1.31 on green, 1.48 on mono, 1.27 on paper.
|
|
955
|
+
Two inks a reader cannot tell apart is not a marking. Same finding as the
|
|
956
|
+
rail's current row (0.19.0), same answer: POSITION AND AREA. The selected
|
|
957
|
+
option is the only one with a left edge marker, and it is bold; the colour is
|
|
958
|
+
the third signal, not the only one.
|
|
959
|
+
|
|
960
|
+
The keyboard's active option and the mouse's hover are the SAME state on
|
|
961
|
+
purpose — there is one pointer of attention, and showing two would ask the
|
|
962
|
+
reader which one Enter is going to take. */
|
|
963
|
+
.select-option {
|
|
964
|
+
display: flex;
|
|
965
|
+
align-items: center;
|
|
966
|
+
gap: 0.4rem;
|
|
967
|
+
padding: 0.28rem 0.9rem 0.28rem 0.55rem;
|
|
968
|
+
border-inline-start: 2px solid transparent;
|
|
969
|
+
white-space: nowrap;
|
|
970
|
+
cursor: pointer;
|
|
971
|
+
}
|
|
972
|
+
.select-option:hover,
|
|
973
|
+
.select-option[data-active="true"] {
|
|
974
|
+
background: var(--muted);
|
|
975
|
+
}
|
|
976
|
+
.select-option[aria-selected="true"] {
|
|
977
|
+
color: var(--primary);
|
|
978
|
+
font-weight: 700;
|
|
979
|
+
border-inline-start-color: var(--primary);
|
|
980
|
+
}
|
|
981
|
+
/* 65% of the ink, not 45%: below that warm lands at 2.94:1 and the option stops
|
|
982
|
+
being readable at all, and "unavailable" still has to be legible to be
|
|
983
|
+
understood as a choice that exists. 65% measures 3.75 at worst (warm). */
|
|
984
|
+
.select-option[aria-disabled="true"] {
|
|
985
|
+
color: color-mix(in srgb, var(--popover-foreground) 65%, var(--popover));
|
|
986
|
+
cursor: default;
|
|
987
|
+
}
|
|
988
|
+
.select-option[aria-disabled="true"]:hover {
|
|
989
|
+
background: none;
|
|
990
|
+
}
|
|
991
|
+
/* <optgroup> label. Nothing in the estate uses one today; it is here because a
|
|
992
|
+
listbox that silently drops the grouping a consumer wrote is worse than one
|
|
993
|
+
that never supported it. */
|
|
994
|
+
.select-group {
|
|
995
|
+
padding: 0.35rem 0.9rem 0.15rem;
|
|
996
|
+
color: var(--muted-foreground);
|
|
997
|
+
font-size: var(--fs-xs);
|
|
998
|
+
text-transform: uppercase;
|
|
999
|
+
letter-spacing: 0.06em;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
801
1002
|
/* ─────────────────────────────────────────────────────────────────────────
|
|
802
1003
|
ELI5 — an "explain like I'm 5" callout box (first-time-term explainers).
|
|
803
1004
|
───────────────────────────────────────────────────────────────────────── */
|
|
@@ -1848,6 +2049,7 @@ span[data-tip], th[data-tip] { border-bottom: 1px dotted var(--muted-foreground)
|
|
|
1848
2049
|
.skip-link,
|
|
1849
2050
|
.dropdown,
|
|
1850
2051
|
.dropdown-panel,
|
|
2052
|
+
.select-panel,
|
|
1851
2053
|
.anim-toggle,
|
|
1852
2054
|
.cursor-block,
|
|
1853
2055
|
.term-caret,
|
|
@@ -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;--content-pad: 1.5rem;--fs-xs: 0.625rem;--fs-sm: 0.6875rem;--fs-base: 0.75rem;--fs-md: 0.8125rem;--fs-lg: 0.9375rem;--fs-xl: 1.125rem;--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-md);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}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)}.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-sm);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: 0.75rem;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--compact{padding: 0.28rem 0.7rem;font-size: var(--fs-sm)}.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}}.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)}.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: 0.92em;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: 0.86em;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: 0.85rem;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: 0.9rem;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: 0.86rem}@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: 0.82rem;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(--background,transparent))}.tablewrap thead th{position: sticky;inset-block-start: 0;z-index: 1;background: var(--background,#f5efe2)}.tickstrip:not(:empty){border: 1px solid var(--border);background: var(--card);margin: 1.6rem 0 1.5rem}.ticktable{width: 100%;border-collapse: collapse;font-size: 0.82rem}.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: 0.7rem}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-xs);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: 0.72rem;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: 0.9rem}.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: 0.9rem;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: 0.85rem}.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],th[data-tip]{border-bottom: 1px dotted var(--muted-foreground)}#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: 0.72rem;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,.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],th[data-tip]{border-bottom: 0}.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.21.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;--content-pad: 1.5rem;--fs-xs: 0.625rem;--fs-sm: 0.6875rem;--fs-base: 0.75rem;--fs-md: 0.8125rem;--fs-lg: 0.9375rem;--fs-xl: 1.125rem;--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-md);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}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)}.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-sm);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: 0.75rem;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--compact{padding: 0.28rem 0.7rem;font-size: var(--fs-sm)}.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}}.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)}select,.select-trigger{appearance: none;-webkit-appearance: none;max-width: 100%;padding: 0.28rem 1.45rem 0.28rem 0.5rem;font: inherit;font-size: var(--fs-sm);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-sm);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-xs);text-transform: uppercase;letter-spacing: 0.06em}.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: 0.92em;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: 0.86em;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: 0.85rem;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: 0.9rem;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: 0.86rem}@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: 0.82rem;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(--background,transparent))}.tablewrap thead th{position: sticky;inset-block-start: 0;z-index: 1;background: var(--background,#f5efe2)}.tickstrip:not(:empty){border: 1px solid var(--border);background: var(--card);margin: 1.6rem 0 1.5rem}.ticktable{width: 100%;border-collapse: collapse;font-size: 0.82rem}.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: 0.7rem}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-xs);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: 0.72rem;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: 0.9rem}.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: 0.9rem;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: 0.85rem}.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],th[data-tip]{border-bottom: 1px dotted var(--muted-foreground)}#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: 0.72rem;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],th[data-tip]{border-bottom: 0}.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.21.0",
|
|
4
4
|
"description": "The danieldeusing terminal design system — 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
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* select.js — replaces the OPERATING SYSTEM's dropdown with the estate's own.
|
|
3
|
+
*
|
|
4
|
+
* Markup contract:
|
|
5
|
+
* <select>…</select>
|
|
6
|
+
*
|
|
7
|
+
* That is the whole contract. Like `initTableScroll()`, this takes plain HTML
|
|
8
|
+
* and needs no classes, no wrapper and no data attributes. Call `initSelects()`
|
|
9
|
+
* once; every `<select>` on the page is enhanced, and so is every one rendered
|
|
10
|
+
* afterwards — cockpit rebuilds its config tables out of innerHTML on every
|
|
11
|
+
* poll, so a widget that only enhanced what existed at load would work until
|
|
12
|
+
* the first refresh and then quietly stop.
|
|
13
|
+
*
|
|
14
|
+
* WHY A REPLACEMENT AND NOT CSS. A `<select>`'s option list is painted by the
|
|
15
|
+
* OS outside the document: rounded corners, a blue system highlight, the system
|
|
16
|
+
* font. No stylesheet reaches it. Chrome 135+ can style it with
|
|
17
|
+
* `appearance: base-select`, but Safari and Firefox cannot, and a fix that
|
|
18
|
+
* lands on one browser leaves the estate disagreeing with ITSELF, which is
|
|
19
|
+
* worse than being consistently wrong. So the list is rebuilt in the page.
|
|
20
|
+
*
|
|
21
|
+
* THE <select> STAYS AND STAYS AUTHORITATIVE. It is not cloned, mirrored or
|
|
22
|
+
* replaced by hidden inputs: it remains the element that holds the value, that
|
|
23
|
+
* a form submits, that `select.value` reads, and that emits `input`/`change`.
|
|
24
|
+
* Page code sees exactly what it saw before — that is what let 28 call sites
|
|
25
|
+
* adopt this without a single edit. It is laid transparently OVER the trigger
|
|
26
|
+
* rather than `display: none`, because Chrome refuses to show a validation
|
|
27
|
+
* bubble on an unfocusable control and then blocks the submit with no message
|
|
28
|
+
* at all, which would silently break every `required` select.
|
|
29
|
+
*
|
|
30
|
+
* Keyboard follows the ARIA APG select-only combobox: Enter/Space/Arrow open,
|
|
31
|
+
* Up/Down move, Home/End jump, printable characters type ahead (a repeated
|
|
32
|
+
* character cycles, as a native select does), Enter selects, Escape closes
|
|
33
|
+
* without changing anything, Tab moves on. Focus never leaves the trigger —
|
|
34
|
+
* the active option is pointed at with `aria-activedescendant` — so there is
|
|
35
|
+
* nowhere for it to get stuck.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
const TYPEAHEAD_MS = 700;
|
|
39
|
+
const GAP = 4; // visual px between the trigger and the panel
|
|
40
|
+
const EDGE = 8; // keep the panel this far off the viewport edge
|
|
41
|
+
|
|
42
|
+
const enhanced = new WeakMap();
|
|
43
|
+
let counter = 0;
|
|
44
|
+
let openInstance = null;
|
|
45
|
+
let documentObserver = null;
|
|
46
|
+
let globalsInstalled = false;
|
|
47
|
+
|
|
48
|
+
/*
|
|
49
|
+
* Consumers set `zoom` on <html> (initResolutionZoom lays every page out
|
|
50
|
+
* against a 1920px reference), and that puts the two halves of any positioning
|
|
51
|
+
* sum in different coordinate spaces: getBoundingClientRect() and
|
|
52
|
+
* innerWidth/innerHeight are VISUAL px, already multiplied, while style.left is
|
|
53
|
+
* a CSS length the browser multiplies AGAIN on the way out. Writing a rect
|
|
54
|
+
* straight into a length therefore applies the zoom twice — an error that grows
|
|
55
|
+
* with distance from the origin, which is how it survives review (it looks fine
|
|
56
|
+
* near the top left). Fixed twice before in this runtime: tooltip.js and
|
|
57
|
+
* lsnav.js. Divide on the WRITE; never "fix" a comparison whose operands are
|
|
58
|
+
* both already visual.
|
|
59
|
+
*/
|
|
60
|
+
const zoomOf = () => Number(getComputedStyle(document.documentElement).zoom) || 1;
|
|
61
|
+
|
|
62
|
+
const optionsOf = (instance) => instance.select.options;
|
|
63
|
+
const label = (element) => (element.textContent || "").trim();
|
|
64
|
+
|
|
65
|
+
const firstEnabled = (instance) => {
|
|
66
|
+
const options = optionsOf(instance);
|
|
67
|
+
for (let i = 0; i < options.length; i += 1) if (!options[i].disabled) return i;
|
|
68
|
+
return -1;
|
|
69
|
+
};
|
|
70
|
+
const lastEnabled = (instance) => {
|
|
71
|
+
const options = optionsOf(instance);
|
|
72
|
+
for (let i = options.length - 1; i >= 0; i -= 1) if (!options[i].disabled) return i;
|
|
73
|
+
return -1;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/* ── the closed control ─────────────────────────────────────────────────── */
|
|
77
|
+
|
|
78
|
+
function syncTrigger(instance) {
|
|
79
|
+
const { select, trigger, value } = instance;
|
|
80
|
+
const option = select.selectedIndex >= 0 ? select.options[select.selectedIndex] : null;
|
|
81
|
+
value.textContent = option ? label(option) : "";
|
|
82
|
+
trigger.disabled = select.disabled;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/*
|
|
86
|
+
* The trigger's accessible name is the LABEL plus the CURRENT VALUE — "lines,
|
|
87
|
+
* 200" — which is what a native select announces and what the APG's select-only
|
|
88
|
+
* combobox prescribes. Pointing `aria-labelledby` at the trigger's own id is
|
|
89
|
+
* how the value gets into the name; it looks like a mistake and is the pattern.
|
|
90
|
+
*
|
|
91
|
+
* The label is found the same three ways the platform finds it, in the platform's
|
|
92
|
+
* order, so a page that already labels its select correctly needs no change:
|
|
93
|
+
* an explicit aria-label, an explicit aria-labelledby, then a <label> — whether
|
|
94
|
+
* associated by `for=` or by wrapping.
|
|
95
|
+
*/
|
|
96
|
+
function nameTrigger(instance) {
|
|
97
|
+
const { select, trigger } = instance;
|
|
98
|
+
const explicit = select.getAttribute("aria-label");
|
|
99
|
+
if (explicit) {
|
|
100
|
+
trigger.setAttribute("aria-label", explicit);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
let labelId = select.getAttribute("aria-labelledby");
|
|
104
|
+
if (!labelId) {
|
|
105
|
+
const element =
|
|
106
|
+
(select.id && document.querySelector(`label[for="${CSS.escape(select.id)}"]`)) ||
|
|
107
|
+
select.closest("label");
|
|
108
|
+
if (element) {
|
|
109
|
+
if (!element.id) element.id = `${instance.id}-label`;
|
|
110
|
+
labelId = element.id;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (labelId) trigger.setAttribute("aria-labelledby", `${labelId} ${trigger.id}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function enhance(select) {
|
|
117
|
+
if (enhanced.has(select)) return;
|
|
118
|
+
// `multiple` and `size > 1` are not popups — the platform renders them inline
|
|
119
|
+
// and there is no OS menu to replace. `data-select="off"` is the opt-out.
|
|
120
|
+
if (select.multiple || select.size > 1) return;
|
|
121
|
+
if (select.dataset.select === "off") return;
|
|
122
|
+
if (select.parentElement?.classList.contains("select-field")) return;
|
|
123
|
+
if (!select.parentNode) return;
|
|
124
|
+
|
|
125
|
+
counter += 1;
|
|
126
|
+
const id = `dd-select-${counter}`;
|
|
127
|
+
|
|
128
|
+
const field = document.createElement("span");
|
|
129
|
+
field.className = "select-field";
|
|
130
|
+
// A page sizes its control on the <select> (`style="max-width:18rem"`), and
|
|
131
|
+
// once the select is out of the flow that sizing has nothing to act on. The
|
|
132
|
+
// wrapper is what occupies the space now, so it takes the inline style. Copied,
|
|
133
|
+
// not moved: the select's own style attribute is still the page's to read.
|
|
134
|
+
const inlineStyle = select.getAttribute("style");
|
|
135
|
+
if (inlineStyle) field.setAttribute("style", inlineStyle);
|
|
136
|
+
|
|
137
|
+
const trigger = document.createElement("button");
|
|
138
|
+
trigger.type = "button";
|
|
139
|
+
trigger.className = "select-trigger";
|
|
140
|
+
trigger.id = `${id}-trigger`;
|
|
141
|
+
trigger.setAttribute("role", "combobox");
|
|
142
|
+
trigger.setAttribute("aria-haspopup", "listbox");
|
|
143
|
+
trigger.setAttribute("aria-expanded", "false");
|
|
144
|
+
trigger.setAttribute("aria-controls", `${id}-panel`);
|
|
145
|
+
|
|
146
|
+
const value = document.createElement("span");
|
|
147
|
+
value.className = "select-value";
|
|
148
|
+
trigger.appendChild(value);
|
|
149
|
+
|
|
150
|
+
// A tooltip anchored to a control nobody can hover is a tooltip that never
|
|
151
|
+
// shows. Both flavours move to the visible element; the select keeps its copy
|
|
152
|
+
// so page code that reads the attribute still finds it.
|
|
153
|
+
for (const attribute of ["title", "data-tip"]) {
|
|
154
|
+
const text = select.getAttribute(attribute);
|
|
155
|
+
if (text !== null) trigger.setAttribute(attribute, text);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
select.parentNode.insertBefore(field, select);
|
|
159
|
+
field.appendChild(select);
|
|
160
|
+
field.appendChild(trigger);
|
|
161
|
+
select.setAttribute("tabindex", "-1");
|
|
162
|
+
select.setAttribute("aria-hidden", "true");
|
|
163
|
+
|
|
164
|
+
const instance = { select, field, trigger, value, id, panel: null, items: [], active: -1, typed: "", typedAt: 0 };
|
|
165
|
+
enhanced.set(select, instance);
|
|
166
|
+
nameTrigger(instance);
|
|
167
|
+
syncTrigger(instance);
|
|
168
|
+
|
|
169
|
+
trigger.addEventListener("click", () => (instance.panel ? close(instance, true) : open(instance)));
|
|
170
|
+
trigger.addEventListener("keydown", (event) => onKeydown(instance, event));
|
|
171
|
+
// Focus aimed at the hidden control — a <label> click, or page code calling
|
|
172
|
+
// select.focus() — belongs to the one the reader can see.
|
|
173
|
+
select.addEventListener("focus", () => trigger.focus());
|
|
174
|
+
// A page that sets the value itself and announces it the normal way is honoured.
|
|
175
|
+
// Our own dispatch lands here too; re-syncing an already-synced trigger is a no-op.
|
|
176
|
+
select.addEventListener("change", () => syncTrigger(instance));
|
|
177
|
+
|
|
178
|
+
// Cockpit rewrites a select's options from fetched data — a new model list, a
|
|
179
|
+
// new credential list, a filter column derived from the rows that just arrived.
|
|
180
|
+
// Without this the trigger would keep showing the label of an option that is no
|
|
181
|
+
// longer in the list, which reads as the page having lost the setting.
|
|
182
|
+
instance.observer = new MutationObserver(() => {
|
|
183
|
+
syncTrigger(instance);
|
|
184
|
+
if (instance.panel) {
|
|
185
|
+
// The list changed under an open panel. Rebuild it rather than show a stale
|
|
186
|
+
// one; focus is on the trigger throughout, so nothing is disturbed.
|
|
187
|
+
close(instance, false);
|
|
188
|
+
open(instance);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
instance.observer.observe(select, {
|
|
192
|
+
childList: true,
|
|
193
|
+
subtree: true,
|
|
194
|
+
characterData: true,
|
|
195
|
+
attributes: true,
|
|
196
|
+
attributeFilter: ["disabled", "selected", "value", "label"],
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/* ── the panel ──────────────────────────────────────────────────────────── */
|
|
201
|
+
|
|
202
|
+
function buildPanel(instance) {
|
|
203
|
+
const panel = document.createElement("ul");
|
|
204
|
+
panel.className = "select-panel";
|
|
205
|
+
panel.id = `${instance.id}-panel`;
|
|
206
|
+
panel.setAttribute("role", "listbox");
|
|
207
|
+
panel.tabIndex = -1;
|
|
208
|
+
|
|
209
|
+
instance.items = [];
|
|
210
|
+
let index = 0;
|
|
211
|
+
const addOption = (option) => {
|
|
212
|
+
const item = document.createElement("li");
|
|
213
|
+
item.className = "select-option";
|
|
214
|
+
item.id = `${instance.id}-o${index}`;
|
|
215
|
+
item.setAttribute("role", "option");
|
|
216
|
+
item.setAttribute("aria-selected", String(index === instance.select.selectedIndex));
|
|
217
|
+
if (option.disabled) item.setAttribute("aria-disabled", "true");
|
|
218
|
+
item.textContent = label(option);
|
|
219
|
+
item.dataset.index = String(index);
|
|
220
|
+
instance.items[index] = item;
|
|
221
|
+
index += 1;
|
|
222
|
+
return item;
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
// Walked child by child rather than over `select.options`, so an <optgroup>
|
|
226
|
+
// keeps its heading. The walk order is document order, which is exactly the
|
|
227
|
+
// order `select.options` flattens to — that is what keeps `data-index` a valid
|
|
228
|
+
// index into the real control.
|
|
229
|
+
for (const child of instance.select.children) {
|
|
230
|
+
if (child.tagName === "OPTGROUP") {
|
|
231
|
+
const heading = document.createElement("li");
|
|
232
|
+
heading.className = "select-group";
|
|
233
|
+
heading.setAttribute("role", "presentation");
|
|
234
|
+
heading.textContent = child.label;
|
|
235
|
+
panel.appendChild(heading);
|
|
236
|
+
for (const option of child.children) {
|
|
237
|
+
if (option.tagName === "OPTION") panel.appendChild(addOption(option));
|
|
238
|
+
}
|
|
239
|
+
} else if (child.tagName === "OPTION") {
|
|
240
|
+
panel.appendChild(addOption(child));
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return panel;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/*
|
|
247
|
+
* THE PANEL IS BUILT FRESH ON EVERY OPEN, from the select's options as they are
|
|
248
|
+
* at that instant. That is not laziness about caching — it is the only way the
|
|
249
|
+
* list cannot be stale, and staleness is the failure this widget is most likely
|
|
250
|
+
* to have shipped: cockpit replaces a select's options from fetched data all the
|
|
251
|
+
* time, and a snapshot taken at enhance time would show a model list from before
|
|
252
|
+
* the last poll while the value underneath had moved on.
|
|
253
|
+
*/
|
|
254
|
+
function open(instance) {
|
|
255
|
+
if (instance.select.disabled) return;
|
|
256
|
+
if (openInstance && openInstance !== instance) close(openInstance, false);
|
|
257
|
+
|
|
258
|
+
const panel = buildPanel(instance);
|
|
259
|
+
instance.panel = panel;
|
|
260
|
+
// Appended to the nearest <dialog> when there is one: a modal dialog is in the
|
|
261
|
+
// top layer and nothing outside it can paint above it, so a panel on <body>
|
|
262
|
+
// would open behind the dialog that owns the select. Everywhere else <body> is
|
|
263
|
+
// right — it escapes `.tablewrap`'s scroll clipping, which is where most of
|
|
264
|
+
// cockpit's selects live.
|
|
265
|
+
(instance.trigger.closest("dialog") || document.body).appendChild(panel);
|
|
266
|
+
instance.trigger.setAttribute("aria-expanded", "true");
|
|
267
|
+
openInstance = instance;
|
|
268
|
+
|
|
269
|
+
// Options are never focusable; the panel is pointed at with
|
|
270
|
+
// aria-activedescendant instead. preventDefault on mousedown is what keeps
|
|
271
|
+
// focus on the trigger when an option is clicked.
|
|
272
|
+
panel.addEventListener("mousedown", (event) => event.preventDefault());
|
|
273
|
+
panel.addEventListener("click", (event) => {
|
|
274
|
+
const item = event.target.closest(".select-option");
|
|
275
|
+
if (item) commit(instance, Number(item.dataset.index));
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
const selected = instance.select.selectedIndex;
|
|
279
|
+
setActive(instance, selected >= 0 && !instance.select.options[selected].disabled ? selected : firstEnabled(instance));
|
|
280
|
+
position(instance);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function close(instance, focusTrigger) {
|
|
284
|
+
if (instance.panel) {
|
|
285
|
+
instance.panel.remove();
|
|
286
|
+
instance.panel = null;
|
|
287
|
+
}
|
|
288
|
+
instance.items = [];
|
|
289
|
+
instance.active = -1;
|
|
290
|
+
instance.typed = "";
|
|
291
|
+
instance.trigger.setAttribute("aria-expanded", "false");
|
|
292
|
+
instance.trigger.removeAttribute("aria-activedescendant");
|
|
293
|
+
if (openInstance === instance) openInstance = null;
|
|
294
|
+
if (focusTrigger) instance.trigger.focus();
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function setActive(instance, index) {
|
|
298
|
+
const previous = instance.items[instance.active];
|
|
299
|
+
if (previous) previous.removeAttribute("data-active");
|
|
300
|
+
instance.active = index;
|
|
301
|
+
const item = instance.items[index];
|
|
302
|
+
if (!item) {
|
|
303
|
+
instance.trigger.removeAttribute("aria-activedescendant");
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
item.setAttribute("data-active", "true");
|
|
307
|
+
instance.trigger.setAttribute("aria-activedescendant", item.id);
|
|
308
|
+
item.scrollIntoView({ block: "nearest" });
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function commit(instance, index) {
|
|
312
|
+
const option = optionsOf(instance)[index];
|
|
313
|
+
if (!option || option.disabled) return; // a disabled option is not a choice; the list stays open
|
|
314
|
+
const changed = instance.select.selectedIndex !== index;
|
|
315
|
+
if (changed) {
|
|
316
|
+
instance.select.selectedIndex = index;
|
|
317
|
+
syncTrigger(instance);
|
|
318
|
+
}
|
|
319
|
+
// Closed BEFORE the events go out: a `change` handler here re-renders the table
|
|
320
|
+
// this select lives in, so anything touching the instance afterwards would be
|
|
321
|
+
// touching a detached node.
|
|
322
|
+
close(instance, true);
|
|
323
|
+
if (changed) {
|
|
324
|
+
// A native select fires `input` and THEN `change`, and both bubble. Cockpit's
|
|
325
|
+
// table filters listen for both on a container element, so dispatching only
|
|
326
|
+
// `change` would make this a quieter control than the one it replaced.
|
|
327
|
+
instance.select.dispatchEvent(new Event("input", { bubbles: true }));
|
|
328
|
+
instance.select.dispatchEvent(new Event("change", { bubbles: true }));
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function position(instance) {
|
|
333
|
+
const { trigger, panel } = instance;
|
|
334
|
+
const zoom = zoomOf();
|
|
335
|
+
const rect = trigger.getBoundingClientRect(); // visual px
|
|
336
|
+
|
|
337
|
+
// offsetWidth is a LAYOUT length, already in the same space as the panel's own
|
|
338
|
+
// min-width, so this one must NOT be divided. Mixing the two is the trap.
|
|
339
|
+
panel.style.minWidth = `${trigger.offsetWidth}px`;
|
|
340
|
+
panel.style.maxWidth = `${(window.innerWidth - EDGE * 2) / zoom}px`;
|
|
341
|
+
panel.style.maxHeight = "";
|
|
342
|
+
|
|
343
|
+
const spaceBelow = window.innerHeight - rect.bottom - EDGE - GAP;
|
|
344
|
+
const spaceAbove = rect.top - EDGE - GAP;
|
|
345
|
+
const wanted = panel.getBoundingClientRect().height;
|
|
346
|
+
// Flip only when flipping actually helps. A long list near the bottom of a tall
|
|
347
|
+
// page has room in neither direction, and flipping it there just moves the
|
|
348
|
+
// clipping to the other end.
|
|
349
|
+
const flip = wanted > spaceBelow && spaceAbove > spaceBelow;
|
|
350
|
+
const room = Math.max(flip ? spaceAbove : spaceBelow, 72);
|
|
351
|
+
if (wanted > room) panel.style.maxHeight = `${room / zoom}px`;
|
|
352
|
+
|
|
353
|
+
const box = panel.getBoundingClientRect(); // visual px, after the clamp
|
|
354
|
+
const x = Math.min(Math.max(rect.left, EDGE), Math.max(EDGE, window.innerWidth - box.width - EDGE));
|
|
355
|
+
const y = flip ? rect.top - GAP - box.height : rect.bottom + GAP;
|
|
356
|
+
panel.style.left = `${x / zoom}px`;
|
|
357
|
+
panel.style.top = `${y / zoom}px`;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/* ── keyboard ───────────────────────────────────────────────────────────── */
|
|
361
|
+
|
|
362
|
+
const isPrintable = (event) =>
|
|
363
|
+
event.key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey;
|
|
364
|
+
|
|
365
|
+
function step(instance, direction) {
|
|
366
|
+
const options = optionsOf(instance);
|
|
367
|
+
for (let i = instance.active + direction; i >= 0 && i < options.length; i += direction) {
|
|
368
|
+
if (!options[i].disabled) {
|
|
369
|
+
setActive(instance, i);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function typeahead(instance, character) {
|
|
376
|
+
const now = Date.now();
|
|
377
|
+
if (now - instance.typedAt > TYPEAHEAD_MS) instance.typed = "";
|
|
378
|
+
instance.typedAt = now;
|
|
379
|
+
instance.typed += character.toLowerCase();
|
|
380
|
+
|
|
381
|
+
// One character repeated CYCLES through the options starting with it, which is
|
|
382
|
+
// what a native select does — pressing "c" three times walks three c-options
|
|
383
|
+
// rather than hunting for "ccc".
|
|
384
|
+
const repeated = instance.typed.length > 1 && new Set(instance.typed).size === 1;
|
|
385
|
+
const needle = repeated ? instance.typed[0] : instance.typed;
|
|
386
|
+
const advance = repeated || instance.typed.length === 1;
|
|
387
|
+
const options = optionsOf(instance);
|
|
388
|
+
const from = advance ? instance.active + 1 : Math.max(instance.active, 0);
|
|
389
|
+
for (let i = 0; i < options.length; i += 1) {
|
|
390
|
+
const index = (from + i + options.length) % options.length;
|
|
391
|
+
if (options[index].disabled) continue;
|
|
392
|
+
if (label(options[index]).toLowerCase().startsWith(needle)) {
|
|
393
|
+
setActive(instance, index);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function onKeydown(instance, event) {
|
|
400
|
+
const isOpen = Boolean(instance.panel);
|
|
401
|
+
const key = event.key;
|
|
402
|
+
|
|
403
|
+
if (key === "Escape") {
|
|
404
|
+
if (!isOpen) return;
|
|
405
|
+
// BOTH, and both matter. preventDefault stops the UA's close-request, and
|
|
406
|
+
// stopPropagation stops the page's own handler: without them, dismissing this
|
|
407
|
+
// list inside a modal <dialog> closes the dialog as well, losing the form.
|
|
408
|
+
event.preventDefault();
|
|
409
|
+
event.stopPropagation();
|
|
410
|
+
close(instance, true);
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
if (key === "Tab") {
|
|
414
|
+
if (isOpen) close(instance, false); // Tab moves on and commits nothing
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (!isOpen) {
|
|
419
|
+
if (key === "Enter" || key === " " || key === "ArrowDown" || key === "ArrowUp" || key === "Home" || key === "End") {
|
|
420
|
+
event.preventDefault();
|
|
421
|
+
open(instance);
|
|
422
|
+
if (key === "Home") setActive(instance, firstEnabled(instance));
|
|
423
|
+
else if (key === "End") setActive(instance, lastEnabled(instance));
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
if (isPrintable(event)) {
|
|
427
|
+
event.preventDefault();
|
|
428
|
+
open(instance);
|
|
429
|
+
typeahead(instance, key);
|
|
430
|
+
}
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// A space CONTINUES a live typeahead rather than selecting — option labels here
|
|
435
|
+
// contain spaces ("public — posts on the PR"), so treating it as Enter would
|
|
436
|
+
// make half of them untypeable.
|
|
437
|
+
if (key === " " && instance.typed && Date.now() - instance.typedAt <= TYPEAHEAD_MS) {
|
|
438
|
+
event.preventDefault();
|
|
439
|
+
typeahead(instance, key);
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
switch (key) {
|
|
443
|
+
case "Enter":
|
|
444
|
+
case " ":
|
|
445
|
+
event.preventDefault();
|
|
446
|
+
commit(instance, instance.active);
|
|
447
|
+
return;
|
|
448
|
+
case "ArrowDown":
|
|
449
|
+
event.preventDefault();
|
|
450
|
+
step(instance, 1);
|
|
451
|
+
return;
|
|
452
|
+
case "ArrowUp":
|
|
453
|
+
event.preventDefault();
|
|
454
|
+
step(instance, -1);
|
|
455
|
+
return;
|
|
456
|
+
case "Home":
|
|
457
|
+
event.preventDefault();
|
|
458
|
+
setActive(instance, firstEnabled(instance));
|
|
459
|
+
return;
|
|
460
|
+
case "End":
|
|
461
|
+
event.preventDefault();
|
|
462
|
+
setActive(instance, lastEnabled(instance));
|
|
463
|
+
return;
|
|
464
|
+
default:
|
|
465
|
+
if (isPrintable(event)) {
|
|
466
|
+
event.preventDefault();
|
|
467
|
+
typeahead(instance, key);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/* ── document-level wiring, installed once ──────────────────────────────── */
|
|
473
|
+
|
|
474
|
+
function installGlobals() {
|
|
475
|
+
if (globalsInstalled) return;
|
|
476
|
+
globalsInstalled = true;
|
|
477
|
+
|
|
478
|
+
document.addEventListener(
|
|
479
|
+
"pointerdown",
|
|
480
|
+
(event) => {
|
|
481
|
+
if (!openInstance) return;
|
|
482
|
+
const target = event.target;
|
|
483
|
+
if (openInstance.panel.contains(target) || openInstance.field.contains(target)) return;
|
|
484
|
+
close(openInstance, false);
|
|
485
|
+
},
|
|
486
|
+
true,
|
|
487
|
+
);
|
|
488
|
+
document.addEventListener("focusin", (event) => {
|
|
489
|
+
if (!openInstance) return;
|
|
490
|
+
if (openInstance.field.contains(event.target) || openInstance.panel.contains(event.target)) return;
|
|
491
|
+
close(openInstance, false);
|
|
492
|
+
});
|
|
493
|
+
// The trigger moves when the page or a scroll container moves under it. Capture,
|
|
494
|
+
// because most of these selects sit in a `.tablewrap` that scrolls on its own and
|
|
495
|
+
// a scroll event there does not bubble.
|
|
496
|
+
addEventListener("resize", () => openInstance && position(openInstance));
|
|
497
|
+
addEventListener("scroll", () => openInstance && position(openInstance), true);
|
|
498
|
+
// form.reset() rewinds selectedIndex without firing an event or touching the DOM,
|
|
499
|
+
// so nothing else here would notice. One delegated listener rather than one per
|
|
500
|
+
// select: these selects are re-created on every render and the <form> is not, so
|
|
501
|
+
// per-select listeners would pile up on it for the life of the page.
|
|
502
|
+
document.addEventListener("reset", (event) => {
|
|
503
|
+
const form = event.target;
|
|
504
|
+
queueMicrotask(() => {
|
|
505
|
+
for (const select of form.querySelectorAll?.("select") ?? []) {
|
|
506
|
+
const instance = enhanced.get(select);
|
|
507
|
+
if (instance) syncTrigger(instance);
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Replace every native `<select>` dropdown on the page with the design system's
|
|
515
|
+
* own, and keep doing so for selects rendered later.
|
|
516
|
+
*
|
|
517
|
+
* @param {ParentNode} [root=document] Where to look for the initial pass.
|
|
518
|
+
*/
|
|
519
|
+
export function initSelects(root = document) {
|
|
520
|
+
installGlobals();
|
|
521
|
+
for (const select of root.querySelectorAll("select")) enhance(select);
|
|
522
|
+
|
|
523
|
+
if (documentObserver) return;
|
|
524
|
+
documentObserver = new MutationObserver((records) => {
|
|
525
|
+
// A re-render can take the open panel's trigger out of the document from
|
|
526
|
+
// underneath it — a background poll rewriting the table it sits in. The panel
|
|
527
|
+
// is on <body>, so it would be left floating over a control that no longer
|
|
528
|
+
// exists.
|
|
529
|
+
if (openInstance && !openInstance.trigger.isConnected) close(openInstance, false);
|
|
530
|
+
for (const record of records) {
|
|
531
|
+
for (const node of record.addedNodes) {
|
|
532
|
+
if (node.nodeType !== 1) continue;
|
|
533
|
+
if (node.tagName === "SELECT") enhance(node);
|
|
534
|
+
else for (const select of node.querySelectorAll("select")) enhance(select);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
});
|
|
538
|
+
documentObserver.observe(document.documentElement, { childList: true, subtree: true });
|
|
539
|
+
}
|
package/src/components.css
CHANGED
|
@@ -362,6 +362,207 @@ html[data-theme="warm"] .dropdown-item[data-theme-value="warm"] {
|
|
|
362
362
|
text-shadow: 0 0 8px var(--glow);
|
|
363
363
|
}
|
|
364
364
|
|
|
365
|
+
/* ─────────────────────────────────────────────────────────────────────────
|
|
366
|
+
Select — the estate's own dropdown, because a native one's list CANNOT be
|
|
367
|
+
styled. Pairs with runtime/select.js.
|
|
368
|
+
|
|
369
|
+
THE CONSTRAINT THAT DECIDES EVERYTHING HERE: a `<select>`'s option list is
|
|
370
|
+
drawn by the OPERATING SYSTEM, outside the page, and no stylesheet reaches
|
|
371
|
+
it. So every surface in the estate has been rendering a rounded, blue-
|
|
372
|
+
highlighted, system-font menu out of a terminal UI — the one component that
|
|
373
|
+
could not be made to look like the rest, no matter what the page declared.
|
|
374
|
+
`appearance: base-select` would fix it in Chrome 135+ and nowhere else, so
|
|
375
|
+
Safari would keep the system menu: the estate would then be inconsistent
|
|
376
|
+
WITH ITSELF, which is worse than being consistently wrong.
|
|
377
|
+
|
|
378
|
+
So the list is rebuilt in the page. runtime/select.js keeps the real
|
|
379
|
+
`<select>` as the value (it is still what a form submits and what page code
|
|
380
|
+
reads) and paints a button + listbox over it. THE MARKUP CONTRACT IS
|
|
381
|
+
NOTHING: author a plain `<select>`, exactly as with `<table>`.
|
|
382
|
+
|
|
383
|
+
TWO RENDERINGS, ONE DECLARATION. The bare `<select>` (JS off, or before the
|
|
384
|
+
runtime has run) and the `.select-trigger` that replaces it must be the same
|
|
385
|
+
control at the same size — a reader who tabs into a page mid-enhancement
|
|
386
|
+
must not see the box change shape. Splitting their look across two rules is
|
|
387
|
+
how they would drift, so the closed control is declared ONCE for both, by
|
|
388
|
+
ELEMENT and by class together. Five copies of `.cfg-sel` in cockpit are what
|
|
389
|
+
this replaces; none of them agreed about `:disabled`.
|
|
390
|
+
|
|
391
|
+
THE BORDER IS 60% OF --foreground, MEASURED, NOT --border. A control's edge
|
|
392
|
+
is the thing that says "this is a control" and WCAG 1.4.11 wants 3:1 for
|
|
393
|
+
that; `--border` is a CONTAINER hairline and measures 1.37 / 2.00 / 1.61 /
|
|
394
|
+
1.42 against `--background` on the four themes — invisible as a control
|
|
395
|
+
edge, and correct for the cards it was made for. The estate's hand-rolled
|
|
396
|
+
selects used `currentColor 30%`, which is 1.70 at worst. 60% is the first
|
|
397
|
+
step that clears 3:1 on all four themes against ALL THREE surfaces a control
|
|
398
|
+
can land on (worst: warm 3.24 over --background, 3.24 over --muted). Mixed
|
|
399
|
+
with `transparent` rather than with a surface so it composites correctly
|
|
400
|
+
wherever it is dropped, and derived from a token so it stays per-theme.
|
|
401
|
+
|
|
402
|
+
THE CARET IS TWO GRADIENT HALVES, not an SVG. It has to be `currentColor` —
|
|
403
|
+
a data-URI with a baked fill would need declaring four times or be wrong on
|
|
404
|
+
two themes (the luminance gap in tokens.css), and a `::after` is impossible
|
|
405
|
+
because a `<select>` cannot have generated content. Two abutting squares,
|
|
406
|
+
each half-filled on the diagonal, draw the same ▾ on both renderings.
|
|
407
|
+
───────────────────────────────────────────────────────────────────────── */
|
|
408
|
+
select,
|
|
409
|
+
.select-trigger {
|
|
410
|
+
appearance: none;
|
|
411
|
+
-webkit-appearance: none;
|
|
412
|
+
max-width: 100%;
|
|
413
|
+
padding: 0.28rem 1.45rem 0.28rem 0.5rem;
|
|
414
|
+
font: inherit;
|
|
415
|
+
font-size: var(--fs-sm);
|
|
416
|
+
line-height: var(--lh-tight);
|
|
417
|
+
text-align: left;
|
|
418
|
+
color: var(--foreground);
|
|
419
|
+
background-color: transparent;
|
|
420
|
+
background-image:
|
|
421
|
+
linear-gradient(45deg, transparent 50%, currentColor 50%),
|
|
422
|
+
linear-gradient(135deg, currentColor 50%, transparent 50%);
|
|
423
|
+
background-position:
|
|
424
|
+
right 0.78rem center,
|
|
425
|
+
right 0.5rem center;
|
|
426
|
+
background-size: 0.28rem 0.28rem;
|
|
427
|
+
background-repeat: no-repeat;
|
|
428
|
+
border: 1px solid color-mix(in srgb, var(--foreground) 60%, transparent);
|
|
429
|
+
cursor: pointer;
|
|
430
|
+
transition: border-color 0.15s ease;
|
|
431
|
+
}
|
|
432
|
+
select:hover,
|
|
433
|
+
.select-trigger:hover,
|
|
434
|
+
.select-trigger[aria-expanded="true"] {
|
|
435
|
+
border-color: var(--primary);
|
|
436
|
+
}
|
|
437
|
+
select:disabled,
|
|
438
|
+
.select-trigger:disabled {
|
|
439
|
+
opacity: 0.45;
|
|
440
|
+
cursor: default;
|
|
441
|
+
}
|
|
442
|
+
/* Open: the caret turns over, so the control says "the list is out" without
|
|
443
|
+
relying on the panel being in view — it may have flipped above the trigger. */
|
|
444
|
+
.select-trigger[aria-expanded="true"] {
|
|
445
|
+
background-image:
|
|
446
|
+
linear-gradient(135deg, transparent 50%, currentColor 50%),
|
|
447
|
+
linear-gradient(45deg, currentColor 50%, transparent 50%);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/* The wrapper the runtime puts around the pair. It is `relative` so the real
|
|
451
|
+
`<select>` can be laid exactly over the trigger: NOT `display: none`, and not
|
|
452
|
+
`visibility: hidden` either — Chrome refuses to show a validation bubble on a
|
|
453
|
+
control it cannot focus and then blocks the submit with no message at all, so
|
|
454
|
+
a `required` select would silently stop working. Transparent and over the
|
|
455
|
+
trigger, the bubble still points at the right box. */
|
|
456
|
+
.select-field {
|
|
457
|
+
position: relative;
|
|
458
|
+
display: inline-flex;
|
|
459
|
+
max-width: 100%;
|
|
460
|
+
vertical-align: middle;
|
|
461
|
+
}
|
|
462
|
+
.select-field > select {
|
|
463
|
+
position: absolute;
|
|
464
|
+
inset: 0;
|
|
465
|
+
width: 100%;
|
|
466
|
+
height: 100%;
|
|
467
|
+
opacity: 0;
|
|
468
|
+
pointer-events: none;
|
|
469
|
+
}
|
|
470
|
+
.select-trigger {
|
|
471
|
+
display: inline-flex;
|
|
472
|
+
align-items: center;
|
|
473
|
+
width: 100%;
|
|
474
|
+
}
|
|
475
|
+
.select-value {
|
|
476
|
+
/* min-width: 0 is what lets a flex item shrink below its content — without it
|
|
477
|
+
`text-overflow: ellipsis` never fires and a long option label blows the
|
|
478
|
+
control out of its column instead of being clipped. */
|
|
479
|
+
min-width: 0;
|
|
480
|
+
overflow: hidden;
|
|
481
|
+
text-overflow: ellipsis;
|
|
482
|
+
white-space: nowrap;
|
|
483
|
+
}
|
|
484
|
+
/* An empty flex item has no line box, so a select whose current option has no
|
|
485
|
+
text (`<option value="">`) would render a control HALF THE HEIGHT of the one
|
|
486
|
+
beside it. A zero-width space gives it a line and no width, and screen readers
|
|
487
|
+
ignore it — unlike an ` `, which they read out. */
|
|
488
|
+
.select-value::after {
|
|
489
|
+
content: "\200b";
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/* THE PANEL IS position: fixed AND APPENDED OUT OF THE FLOW, for two reasons
|
|
493
|
+
this estate hits constantly: selects live inside `.tablewrap` (which scrolls,
|
|
494
|
+
so it clips) and inside modal `<dialog>`s (a top layer, which nothing outside
|
|
495
|
+
can paint above). An absolutely-positioned panel would be cut off in the
|
|
496
|
+
first and hidden behind the second. runtime/select.js appends it to the
|
|
497
|
+
nearest open <dialog> when there is one and to <body> otherwise.
|
|
498
|
+
z-index 60 clears the scanline overlay (40), .dropdown-panel (50) and
|
|
499
|
+
footer.status (50). */
|
|
500
|
+
.select-panel {
|
|
501
|
+
position: fixed;
|
|
502
|
+
z-index: 60;
|
|
503
|
+
margin: 0;
|
|
504
|
+
padding: 0.15rem 0;
|
|
505
|
+
overflow-y: auto;
|
|
506
|
+
overscroll-behavior: contain;
|
|
507
|
+
list-style: none;
|
|
508
|
+
font-size: var(--fs-sm);
|
|
509
|
+
line-height: var(--lh-tight);
|
|
510
|
+
color: var(--popover-foreground);
|
|
511
|
+
background: var(--popover);
|
|
512
|
+
border: 1px solid color-mix(in srgb, var(--foreground) 60%, transparent);
|
|
513
|
+
box-shadow: 0 0 20px var(--glow-soft);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/* SELECTION CANNOT BE A COLOUR HERE, and that is measured, not felt. `--primary`
|
|
517
|
+
against `--popover-foreground` — the selected option's ink against its
|
|
518
|
+
neighbours' — is 1.65:1 on warm, 1.31 on green, 1.48 on mono, 1.27 on paper.
|
|
519
|
+
Two inks a reader cannot tell apart is not a marking. Same finding as the
|
|
520
|
+
rail's current row (0.19.0), same answer: POSITION AND AREA. The selected
|
|
521
|
+
option is the only one with a left edge marker, and it is bold; the colour is
|
|
522
|
+
the third signal, not the only one.
|
|
523
|
+
|
|
524
|
+
The keyboard's active option and the mouse's hover are the SAME state on
|
|
525
|
+
purpose — there is one pointer of attention, and showing two would ask the
|
|
526
|
+
reader which one Enter is going to take. */
|
|
527
|
+
.select-option {
|
|
528
|
+
display: flex;
|
|
529
|
+
align-items: center;
|
|
530
|
+
gap: 0.4rem;
|
|
531
|
+
padding: 0.28rem 0.9rem 0.28rem 0.55rem;
|
|
532
|
+
border-inline-start: 2px solid transparent;
|
|
533
|
+
white-space: nowrap;
|
|
534
|
+
cursor: pointer;
|
|
535
|
+
}
|
|
536
|
+
.select-option:hover,
|
|
537
|
+
.select-option[data-active="true"] {
|
|
538
|
+
background: var(--muted);
|
|
539
|
+
}
|
|
540
|
+
.select-option[aria-selected="true"] {
|
|
541
|
+
color: var(--primary);
|
|
542
|
+
font-weight: 700;
|
|
543
|
+
border-inline-start-color: var(--primary);
|
|
544
|
+
}
|
|
545
|
+
/* 65% of the ink, not 45%: below that warm lands at 2.94:1 and the option stops
|
|
546
|
+
being readable at all, and "unavailable" still has to be legible to be
|
|
547
|
+
understood as a choice that exists. 65% measures 3.75 at worst (warm). */
|
|
548
|
+
.select-option[aria-disabled="true"] {
|
|
549
|
+
color: color-mix(in srgb, var(--popover-foreground) 65%, var(--popover));
|
|
550
|
+
cursor: default;
|
|
551
|
+
}
|
|
552
|
+
.select-option[aria-disabled="true"]:hover {
|
|
553
|
+
background: none;
|
|
554
|
+
}
|
|
555
|
+
/* <optgroup> label. Nothing in the estate uses one today; it is here because a
|
|
556
|
+
listbox that silently drops the grouping a consumer wrote is worse than one
|
|
557
|
+
that never supported it. */
|
|
558
|
+
.select-group {
|
|
559
|
+
padding: 0.35rem 0.9rem 0.15rem;
|
|
560
|
+
color: var(--muted-foreground);
|
|
561
|
+
font-size: var(--fs-xs);
|
|
562
|
+
text-transform: uppercase;
|
|
563
|
+
letter-spacing: 0.06em;
|
|
564
|
+
}
|
|
565
|
+
|
|
365
566
|
/* ─────────────────────────────────────────────────────────────────────────
|
|
366
567
|
ELI5 — an "explain like I'm 5" callout box (first-time-term explainers).
|
|
367
568
|
───────────────────────────────────────────────────────────────────────── */
|
package/src/print.css
CHANGED
|
@@ -98,13 +98,13 @@
|
|
|
98
98
|
on docs.danieldeusing.de, NOT for a doc opened from disk over file://. -->
|
|
99
99
|
<link
|
|
100
100
|
rel="stylesheet"
|
|
101
|
-
href="https://cdn.jsdelivr.net/npm/@danieldeusing/design@0.
|
|
102
|
-
onerror="this.onerror=null;this.href='/_design/danieldeusing-design-0.
|
|
101
|
+
href="https://cdn.jsdelivr.net/npm/@danieldeusing/design@0.21.0/dist/danieldeusing-design.min.css"
|
|
102
|
+
onerror="this.onerror=null;this.href='/_design/danieldeusing-design-0.21.0.min.css'"
|
|
103
103
|
/>
|
|
104
104
|
<link
|
|
105
105
|
rel="stylesheet"
|
|
106
|
-
href="https://cdn.jsdelivr.net/npm/@danieldeusing/design@0.
|
|
107
|
-
onerror="this.onerror=null;this.href='/_design/danieldeusing-design-0.
|
|
106
|
+
href="https://cdn.jsdelivr.net/npm/@danieldeusing/design@0.21.0/src/fonts.css"
|
|
107
|
+
onerror="this.onerror=null;this.href='/_design/danieldeusing-design-0.21.0.fonts.css'"
|
|
108
108
|
/>
|
|
109
109
|
|
|
110
110
|
<style>
|
|
@@ -340,8 +340,8 @@ flowchart TD
|
|
|
340
340
|
</footer>
|
|
341
341
|
|
|
342
342
|
<script type="module">
|
|
343
|
-
import { applyStoredTheme, initThemeSwitcher, initDropdowns, initTerminal, initAnimToggle, initBurgerNav, initLsNav, initTableScroll, initDiagramZoom, initMinimap, initTooltips } from
|
|
344
|
-
"https://cdn.jsdelivr.net/npm/@danieldeusing/design@0.
|
|
343
|
+
import { applyStoredTheme, initThemeSwitcher, initDropdowns, initSelects, initTerminal, initAnimToggle, initBurgerNav, initLsNav, initTableScroll, initDiagramZoom, initMinimap, initTooltips } from
|
|
344
|
+
"https://cdn.jsdelivr.net/npm/@danieldeusing/design@0.21.0/runtime/index.js";
|
|
345
345
|
applyStoredTheme();
|
|
346
346
|
initThemeSwitcher();
|
|
347
347
|
initDropdowns();
|
|
@@ -354,6 +354,11 @@ flowchart TD
|
|
|
354
354
|
// Every table gets a .tablewrap parent, so a table wider than its column scrolls
|
|
355
355
|
// itself instead of pushing the whole page sideways. Already-wrapped ones are skipped.
|
|
356
356
|
initTableScroll();
|
|
357
|
+
// Every <select> gets the estate's dropdown instead of the OS one. Called
|
|
358
|
+
// unconditionally, beside initTableScroll() and for the same reason: both have no
|
|
359
|
+
// markup contract, so both are a no-op on a page that has none of their element —
|
|
360
|
+
// and neither can be forgotten on the day a page grows one.
|
|
361
|
+
initSelects();
|
|
357
362
|
|
|
358
363
|
// Mermaid diagrams — themed from the design tokens, re-rendered on theme switch.
|
|
359
364
|
// Delete this block (and the pre.mermaid CSS) if the page has no diagrams.
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
jumps. The runtime export exists for surfaces that genuinely cannot inline.
|
|
49
49
|
2. At the end of <body>, import and call the runtime:
|
|
50
50
|
import { initLsNav, initBurgerNav, initThemeSwitcher, initDropdowns,
|
|
51
|
-
initAnimToggle, initTableScroll }
|
|
51
|
+
initAnimToggle, initTableScroll, initSelects }
|
|
52
52
|
from "@danieldeusing/design/runtime";
|
|
53
53
|
initBurgerNav() is not optional here: the markup below ships a .nav-burger, and
|
|
54
54
|
below the 48rem breakpoint that button is the ONLY way to reach the navigation.
|
|
@@ -59,6 +59,11 @@
|
|
|
59
59
|
4. `initTableScroll()` wraps every table so a wide one scrolls itself rather than
|
|
60
60
|
scrolling the whole page sideways. The markup contract is nothing — author a
|
|
61
61
|
plain <table>. Call it again after rendering more rows.
|
|
62
|
+
5. `initSelects()` replaces the OS dropdown on every <select> with the estate's own —
|
|
63
|
+
the one component CSS alone can never fix, because the option list is drawn outside
|
|
64
|
+
the page. The markup contract is nothing here either: author a plain <select>, and
|
|
65
|
+
it stays the element that holds the value and fires input/change. Selects rendered
|
|
66
|
+
later are picked up on their own, so there is nothing to call again.
|
|
62
67
|
-->
|
|
63
68
|
|
|
64
69
|
<!-- ─────────────── <head> ─────────────── -->
|