@danieldeusing/design 0.20.0 → 0.22.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.
@@ -133,6 +133,28 @@
133
133
  transform: translateY(-2px);
134
134
  }
135
135
 
136
+ /* THE BUTTON HAD NO DISABLED STATE — the select next to it did, and they disagreed.
137
+ `select:disabled` has been `opacity: 0.45; cursor: default` since 0.21.0, so a control row
138
+ holding a greyed dropdown beside a fully-lit button that does nothing when pressed was saying
139
+ two different things about the same condition. A pager's `prev` on page one is the case that
140
+ made it unavoidable: it is disabled far more often than not.
141
+
142
+ BELOW the hover rules on purpose. `:disabled` and `--ghost:hover` are both (0,2,0), so equal
143
+ specificity is settled by source order — declared above them, a disabled button would light up
144
+ under the pointer and reinstate exactly the lie this removes. Same trap that made every compact
145
+ button render at CTA size in 0.13.0.
146
+
147
+ Opacity and nothing else about the fill: a disabled PRIMARY button must stay filled and just go
148
+ quiet. Setting `background: transparent` here would strip it to `--primary-foreground` text on
149
+ the page background — near-invisible on all four themes — so the one property that reads as
150
+ "off" on both variants is the only one touched. */
151
+ .btn-terminal:disabled {
152
+ opacity: 0.45;
153
+ cursor: default;
154
+ box-shadow: none;
155
+ transform: none;
156
+ }
157
+
136
158
  /* A ROW-SIZED BUTTON. The system's button is a landing-page CTA at 12px/24px; a table row that
137
159
  offers `remove` needs the same button at the size of its text. This is the size ONLY — colour,
138
160
  corners and the `> ` prefix still come from .btn-terminal / --ghost, so a compact button cannot
@@ -191,6 +213,49 @@
191
213
  .btn-terminal--destructive { min-width: 44px; min-height: 44px; }
192
214
  }
193
215
 
216
+ /* THE EDIT ROW ACTION — the same icon button as the bin, in the ordinary colour.
217
+
218
+ `edit →` was a word and an arrow in a table cell wide enough to hold them, next to a bin that
219
+ is 22px square. Two controls doing the same job in one column, one of them four times the
220
+ width of the other, and at 375px the label broke mid-word into "edi / t →" — which is why
221
+ cockpit carried a `white-space: nowrap` rule to prop it up. An icon says it in the space the
222
+ bin already proved is enough.
223
+
224
+ IT IS NOT DESTRUCTIVE AND MUST NOT BORROW THAT COLOUR. Editing is an ordinary action; red is
225
+ reserved for the one press that cannot be taken back. So this class carries NO colour at all —
226
+ compose it with `--ghost` and it is `--primary` on a `--border` outline like every other
227
+ secondary control, and with nothing and it is the filled primary button. That is also why it is
228
+ not `--destructive` with the fill swapped: colour is the bin's whole warning, and the delta
229
+ here is only the glyph and the box that holds it.
230
+
231
+ THE GLYPH IS A MASK, for the reason the bin's comment gives: `currentColor` re-takes whatever
232
+ colour the composition landed on, on all four themes, with nothing declared per-theme. A pasted
233
+ SVG would be one `fill=` that is wrong on two of them, once per surface that adopts it.
234
+
235
+ AN ACCESSIBLE NAME IS MANDATORY — see the bin. Dropping the word "edit" leaves a button whose
236
+ entire content is a mask, and a mask has no text: a screen reader announces "button", and a
237
+ column of them announces "button" a dozen times. `aria-label` must name the TARGET
238
+ ("edit poi/vu3"), not repeat the verb, or the column is a dozen identical announcements
239
+ instead. `bin/design-conformance` fails an icon button with no accessible name. */
240
+ .btn-terminal--edit {
241
+ display: inline-flex; align-items: center; justify-content: center;
242
+ padding: 0.3rem;
243
+ }
244
+ /* Kills the `> ` prefix, which would otherwise be the entire text content of an icon button.
245
+ --ghost already blanks it; repeated here so the class is correct on a filled button too. */
246
+ .btn-terminal--edit::before { content: ""; }
247
+ .btn-terminal--edit::after {
248
+ content: ""; display: block; width: 0.875rem; height: 0.875rem;
249
+ background: currentColor;
250
+ -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04l-3.75-3.75-1.83 1.83 3.75 3.75 1.83-1.83z'/%3E%3C/svg%3E") center / contain no-repeat;
251
+ mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04l-3.75-3.75-1.83 1.83 3.75 3.75 1.83-1.83z'/%3E%3C/svg%3E") center / contain no-repeat;
252
+ }
253
+ /* 44px under a thumb, same as the bin: `min-*` grows the tap target without moving the glyph,
254
+ which the flexbox keeps centred. Never set a width on it. */
255
+ @media (pointer: coarse) {
256
+ .btn-terminal--edit { min-width: 44px; min-height: 44px; }
257
+ }
258
+
194
259
  /* Quiet, underlined link that warms to the primary colour on hover. */
195
260
  .link-quiet {
196
261
  color: var(--muted-foreground);
@@ -362,6 +427,252 @@ html[data-theme="warm"] .dropdown-item[data-theme-value="warm"] {
362
427
  text-shadow: 0 0 8px var(--glow);
363
428
  }
364
429
 
430
+ /* ─────────────────────────────────────────────────────────────────────────
431
+ Select — the estate's own dropdown, because a native one's list CANNOT be
432
+ styled. Pairs with runtime/select.js.
433
+
434
+ THE CONSTRAINT THAT DECIDES EVERYTHING HERE: a `<select>`'s option list is
435
+ drawn by the OPERATING SYSTEM, outside the page, and no stylesheet reaches
436
+ it. So every surface in the estate has been rendering a rounded, blue-
437
+ highlighted, system-font menu out of a terminal UI — the one component that
438
+ could not be made to look like the rest, no matter what the page declared.
439
+ `appearance: base-select` would fix it in Chrome 135+ and nowhere else, so
440
+ Safari would keep the system menu: the estate would then be inconsistent
441
+ WITH ITSELF, which is worse than being consistently wrong.
442
+
443
+ So the list is rebuilt in the page. runtime/select.js keeps the real
444
+ `<select>` as the value (it is still what a form submits and what page code
445
+ reads) and paints a button + listbox over it. THE MARKUP CONTRACT IS
446
+ NOTHING: author a plain `<select>`, exactly as with `<table>`.
447
+
448
+ TWO RENDERINGS, ONE DECLARATION. The bare `<select>` (JS off, or before the
449
+ runtime has run) and the `.select-trigger` that replaces it must be the same
450
+ control at the same size — a reader who tabs into a page mid-enhancement
451
+ must not see the box change shape. Splitting their look across two rules is
452
+ how they would drift, so the closed control is declared ONCE for both, by
453
+ ELEMENT and by class together. Five copies of `.cfg-sel` in cockpit are what
454
+ this replaces; none of them agreed about `:disabled`.
455
+
456
+ THE BORDER IS 60% OF --foreground, MEASURED, NOT --border. A control's edge
457
+ is the thing that says "this is a control" and WCAG 1.4.11 wants 3:1 for
458
+ that; `--border` is a CONTAINER hairline and measures 1.37 / 2.00 / 1.61 /
459
+ 1.42 against `--background` on the four themes — invisible as a control
460
+ edge, and correct for the cards it was made for. The estate's hand-rolled
461
+ selects used `currentColor 30%`, which is 1.70 at worst. 60% is the first
462
+ step that clears 3:1 on all four themes against ALL THREE surfaces a control
463
+ can land on (worst: warm 3.24 over --background, 3.24 over --muted). Mixed
464
+ with `transparent` rather than with a surface so it composites correctly
465
+ wherever it is dropped, and derived from a token so it stays per-theme.
466
+
467
+ THE CARET IS TWO GRADIENT HALVES, not an SVG. It has to be `currentColor` —
468
+ a data-URI with a baked fill would need declaring four times or be wrong on
469
+ two themes (the luminance gap in tokens.css), and a `::after` is impossible
470
+ because a `<select>` cannot have generated content. Two abutting squares,
471
+ each half-filled on the diagonal, draw the same ▾ on both renderings.
472
+ ───────────────────────────────────────────────────────────────────────── */
473
+ select,
474
+ .select-trigger {
475
+ appearance: none;
476
+ -webkit-appearance: none;
477
+ max-width: 100%;
478
+ padding: 0.28rem 1.45rem 0.28rem 0.5rem;
479
+ font: inherit;
480
+ font-size: var(--fs-sm);
481
+ line-height: var(--lh-tight);
482
+ text-align: left;
483
+ color: var(--foreground);
484
+ background-color: transparent;
485
+ background-image:
486
+ linear-gradient(45deg, transparent 50%, currentColor 50%),
487
+ linear-gradient(135deg, currentColor 50%, transparent 50%);
488
+ background-position:
489
+ right 0.78rem center,
490
+ right 0.5rem center;
491
+ background-size: 0.28rem 0.28rem;
492
+ background-repeat: no-repeat;
493
+ border: 1px solid color-mix(in srgb, var(--foreground) 60%, transparent);
494
+ cursor: pointer;
495
+ transition: border-color 0.15s ease;
496
+ }
497
+ select:hover,
498
+ .select-trigger:hover,
499
+ .select-trigger[aria-expanded="true"] {
500
+ border-color: var(--primary);
501
+ }
502
+ select:disabled,
503
+ .select-trigger:disabled {
504
+ opacity: 0.45;
505
+ cursor: default;
506
+ }
507
+ /* Open: the caret turns over, so the control says "the list is out" without
508
+ relying on the panel being in view — it may have flipped above the trigger. */
509
+ .select-trigger[aria-expanded="true"] {
510
+ background-image:
511
+ linear-gradient(135deg, transparent 50%, currentColor 50%),
512
+ linear-gradient(45deg, currentColor 50%, transparent 50%);
513
+ }
514
+
515
+ /* The wrapper the runtime puts around the pair. It is `relative` so the real
516
+ `<select>` can be laid exactly over the trigger: NOT `display: none`, and not
517
+ `visibility: hidden` either — Chrome refuses to show a validation bubble on a
518
+ control it cannot focus and then blocks the submit with no message at all, so
519
+ a `required` select would silently stop working. Transparent and over the
520
+ trigger, the bubble still points at the right box. */
521
+ .select-field {
522
+ position: relative;
523
+ display: inline-flex;
524
+ max-width: 100%;
525
+ vertical-align: middle;
526
+ }
527
+ .select-field > select {
528
+ position: absolute;
529
+ inset: 0;
530
+ width: 100%;
531
+ height: 100%;
532
+ opacity: 0;
533
+ pointer-events: none;
534
+ }
535
+ .select-trigger {
536
+ display: inline-flex;
537
+ align-items: center;
538
+ width: 100%;
539
+ }
540
+ .select-value {
541
+ /* min-width: 0 is what lets a flex item shrink below its content — without it
542
+ `text-overflow: ellipsis` never fires and a long option label blows the
543
+ control out of its column instead of being clipped. */
544
+ min-width: 0;
545
+ overflow: hidden;
546
+ text-overflow: ellipsis;
547
+ white-space: nowrap;
548
+ }
549
+ /* An empty flex item has no line box, so a select whose current option has no
550
+ text (`<option value="">`) would render a control HALF THE HEIGHT of the one
551
+ beside it. A zero-width space gives it a line and no width, and screen readers
552
+ ignore it — unlike an `&nbsp;`, which they read out. */
553
+ .select-value::after {
554
+ content: "\200b";
555
+ }
556
+
557
+ /* THE PANEL IS position: fixed AND APPENDED OUT OF THE FLOW, for two reasons
558
+ this estate hits constantly: selects live inside `.tablewrap` (which scrolls,
559
+ so it clips) and inside modal `<dialog>`s (a top layer, which nothing outside
560
+ can paint above). An absolutely-positioned panel would be cut off in the
561
+ first and hidden behind the second. runtime/select.js appends it to the
562
+ nearest open <dialog> when there is one and to <body> otherwise.
563
+ z-index 60 clears the scanline overlay (40), .dropdown-panel (50) and
564
+ footer.status (50). */
565
+ .select-panel {
566
+ position: fixed;
567
+ z-index: 60;
568
+ margin: 0;
569
+ padding: 0.15rem 0;
570
+ overflow-y: auto;
571
+ overscroll-behavior: contain;
572
+ list-style: none;
573
+ font-size: var(--fs-sm);
574
+ line-height: var(--lh-tight);
575
+ color: var(--popover-foreground);
576
+ background: var(--popover);
577
+ border: 1px solid color-mix(in srgb, var(--foreground) 60%, transparent);
578
+ box-shadow: 0 0 20px var(--glow-soft);
579
+ }
580
+
581
+ /* SELECTION CANNOT BE A COLOUR HERE, and that is measured, not felt. `--primary`
582
+ against `--popover-foreground` — the selected option's ink against its
583
+ neighbours' — is 1.65:1 on warm, 1.31 on green, 1.48 on mono, 1.27 on paper.
584
+ Two inks a reader cannot tell apart is not a marking. Same finding as the
585
+ rail's current row (0.19.0), same answer: POSITION AND AREA. The selected
586
+ option is the only one with a left edge marker, and it is bold; the colour is
587
+ the third signal, not the only one.
588
+
589
+ The keyboard's active option and the mouse's hover are the SAME state on
590
+ purpose — there is one pointer of attention, and showing two would ask the
591
+ reader which one Enter is going to take. */
592
+ .select-option {
593
+ display: flex;
594
+ align-items: center;
595
+ gap: 0.4rem;
596
+ padding: 0.28rem 0.9rem 0.28rem 0.55rem;
597
+ border-inline-start: 2px solid transparent;
598
+ white-space: nowrap;
599
+ cursor: pointer;
600
+ }
601
+ .select-option:hover,
602
+ .select-option[data-active="true"] {
603
+ background: var(--muted);
604
+ }
605
+ .select-option[aria-selected="true"] {
606
+ color: var(--primary);
607
+ font-weight: 700;
608
+ border-inline-start-color: var(--primary);
609
+ }
610
+ /* 65% of the ink, not 45%: below that warm lands at 2.94:1 and the option stops
611
+ being readable at all, and "unavailable" still has to be legible to be
612
+ understood as a choice that exists. 65% measures 3.75 at worst (warm). */
613
+ .select-option[aria-disabled="true"] {
614
+ color: color-mix(in srgb, var(--popover-foreground) 65%, var(--popover));
615
+ cursor: default;
616
+ }
617
+ .select-option[aria-disabled="true"]:hover {
618
+ background: none;
619
+ }
620
+ /* <optgroup> label. Nothing in the estate uses one today; it is here because a
621
+ listbox that silently drops the grouping a consumer wrote is worse than one
622
+ that never supported it. */
623
+ .select-group {
624
+ padding: 0.35rem 0.9rem 0.15rem;
625
+ color: var(--muted-foreground);
626
+ font-size: var(--fs-xs);
627
+ text-transform: uppercase;
628
+ letter-spacing: 0.06em;
629
+ }
630
+
631
+ /* ─────────────────────────────────────────────────────────────────────────
632
+ TABLE PAGER — twenty rows at a time, and the reader can say otherwise.
633
+
634
+ Built and placed by runtime/pagination.js; a page writes no part of this. It
635
+ INVENTS NOTHING: the two buttons are `.btn-terminal--ghost.btn-terminal--compact`
636
+ (the same pair every refresh and cancel in the estate wears) and the size picker
637
+ is a bare `<select>` that `initSelects()` enhances. So there is no new colour
638
+ here, no new control, and nothing to keep in step at the next release — only a
639
+ row that decides where those three things sit.
640
+
641
+ Ghost, not filled: a pager is not the primary action of any view it appears in,
642
+ and two filled buttons side by side compete. The status text is
643
+ `--muted-foreground` because it is a measurement of the table, not part of it —
644
+ 4.84:1 at worst (warm on --card), so it clears AA while staying quiet.
645
+
646
+ The status is left and the controls are right (`margin-inline-start: auto` on the
647
+ picker pushes the pair over), which puts "where am I" at the start of the line and
648
+ "take me elsewhere" at the end. It wraps rather than scrolls: on a phone the three
649
+ parts stack, and a control that has gone off the side of a table is worse than a
650
+ control on a second line.
651
+ ───────────────────────────────────────────────────────────────────────── */
652
+ .table-pager {
653
+ display: flex;
654
+ flex-wrap: wrap;
655
+ align-items: center;
656
+ gap: 0.45rem 0.9rem;
657
+ margin-block-start: 0.6rem;
658
+ font-size: var(--fs-sm);
659
+ }
660
+ .table-pager-status {
661
+ margin: 0;
662
+ color: var(--muted-foreground);
663
+ }
664
+ .table-pager-size {
665
+ display: inline-flex;
666
+ align-items: center;
667
+ gap: 0.4rem;
668
+ margin-inline-start: auto;
669
+ color: var(--muted-foreground);
670
+ }
671
+ .table-pager-nav {
672
+ display: inline-flex;
673
+ gap: 0.4rem;
674
+ }
675
+
365
676
  /* ─────────────────────────────────────────────────────────────────────────
366
677
  ELI5 — an "explain like I'm 5" callout box (first-time-term explainers).
367
678
  ───────────────────────────────────────────────────────────────────────── */
package/src/print.css CHANGED
@@ -122,6 +122,7 @@
122
122
  .skip-link,
123
123
  .dropdown,
124
124
  .dropdown-panel,
125
+ .select-panel,
125
126
  .anim-toggle,
126
127
  .cursor-block,
127
128
  .term-caret,
@@ -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.20.0/dist/danieldeusing-design.min.css"
102
- onerror="this.onerror=null;this.href='/_design/danieldeusing-design-0.20.0.min.css'"
101
+ href="https://cdn.jsdelivr.net/npm/@danieldeusing/design@0.22.0/dist/danieldeusing-design.min.css"
102
+ onerror="this.onerror=null;this.href='/_design/danieldeusing-design-0.22.0.min.css'"
103
103
  />
104
104
  <link
105
105
  rel="stylesheet"
106
- href="https://cdn.jsdelivr.net/npm/@danieldeusing/design@0.20.0/src/fonts.css"
107
- onerror="this.onerror=null;this.href='/_design/danieldeusing-design-0.20.0.fonts.css'"
106
+ href="https://cdn.jsdelivr.net/npm/@danieldeusing/design@0.22.0/src/fonts.css"
107
+ onerror="this.onerror=null;this.href='/_design/danieldeusing-design-0.22.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.20.0/runtime/index.js";
343
+ import { applyStoredTheme, initThemeSwitcher, initDropdowns, initSelects, initTerminal, initAnimToggle, initBurgerNav, initLsNav, initTableScroll, initDiagramZoom, initMinimap, initTooltips } from
344
+ "https://cdn.jsdelivr.net/npm/@danieldeusing/design@0.22.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> ─────────────── -->