@waveso/docs 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +655 -0
- package/README.md +80 -3
- package/dist/code-frame.d.ts +28 -1
- package/dist/code-frame.js +36 -1
- package/dist/frontmatter.d.ts +5 -0
- package/dist/frontmatter.js +5 -0
- package/dist/nav-order.d.ts +14 -1
- package/dist/nav-order.js +15 -1
- package/dist/next.js +26 -1
- package/dist/plugins/rehype-code-frame.js +41 -11
- package/dist/react/explore.d.ts +53 -0
- package/dist/react/explore.js +86 -0
- package/dist/react/markdown-components.js +14 -0
- package/dist/react/nav.js +14 -11
- package/dist/react/search-dialog.js +87 -15
- package/dist/react/shell-labels.d.ts +8 -1
- package/dist/react/shell-labels.js +1 -0
- package/dist/styles.css +1144 -168
- package/dist/types.d.ts +31 -1
- package/package.json +5 -1
package/dist/styles.css
CHANGED
|
@@ -142,8 +142,51 @@
|
|
|
142
142
|
* no shadow, so the dark ramp deepens it rather than reusing it. */
|
|
143
143
|
--wave-docs-shadow: oklch(0 0 0 / 0.3);
|
|
144
144
|
|
|
145
|
-
|
|
146
|
-
|
|
145
|
+
/*
|
|
146
|
+
* ⚠️ ONE ROOT AND THREE TIERS, DERIVED — AND THE NUMBERS ARE `@waveso/ui`'s.
|
|
147
|
+
*
|
|
148
|
+
* A site running both this package and the component library should not
|
|
149
|
+
* have two radius scales a few pixels apart, and the way to guarantee that
|
|
150
|
+
* is to take theirs rather than to keep ours in step by hand. `--radius`
|
|
151
|
+
* there is `0.625rem` with `--radius-sm/md/lg` at `calc(root ∓ --spacing)`;
|
|
152
|
+
* this is the same construction with the step written out, because
|
|
153
|
+
* `--spacing` is Tailwind's and this package does not depend on Tailwind.
|
|
154
|
+
*
|
|
155
|
+
* ⚠️ AND THE ROOT IS THE OVERRIDE POINT, WHICH IS THE WHOLE REASON IT
|
|
156
|
+
* EXISTS. A host already running `@waveso/ui` writes one line —
|
|
157
|
+
* `--wave-docs-radius-base: var(--radius)` — and the docs follow their
|
|
158
|
+
* app's corners, including any theme that moves them. Overriding three
|
|
159
|
+
* tokens separately is three chances to break the concentric arithmetic
|
|
160
|
+
* below.
|
|
161
|
+
*
|
|
162
|
+
* Which tier a box takes is decided by what *kind* of box it is, never by
|
|
163
|
+
* how big it happens to be:
|
|
164
|
+
*
|
|
165
|
+
* `-sm` inline chips, small controls, and focus rings drawn on those
|
|
166
|
+
* base controls, overlays, and the panel's inset surface
|
|
167
|
+
* `-lg` every block in the reading flow, and the panel's outer edge
|
|
168
|
+
*
|
|
169
|
+
* ⚠️ THE BLOCK TIER WAS SPLIT BEFORE THIS. Callouts, images and video
|
|
170
|
+
* embeds sat at the base radius while a code frame and a table sat at 19px,
|
|
171
|
+
* so two blocks a paragraph apart disagreed by eleven pixels. 19 was
|
|
172
|
+
* measured off a reference site, which is a fine way to pick a number and a
|
|
173
|
+
* bad way to pick a system.
|
|
174
|
+
*/
|
|
175
|
+
--wave-docs-radius-base: 0.625rem;
|
|
176
|
+
/*
|
|
177
|
+
* The step between tiers, and the panel's padding is the same number
|
|
178
|
+
* *by construction* below rather than by coincidence: the panel's inset
|
|
179
|
+
* surface has to be its frame's radius minus its own padding, or the two
|
|
180
|
+
* corners run at different curvatures.
|
|
181
|
+
*/
|
|
182
|
+
--wave-docs-radius-step: 0.25rem;
|
|
183
|
+
--wave-docs-radius-sm: calc(
|
|
184
|
+
var(--wave-docs-radius-base) - var(--wave-docs-radius-step)
|
|
185
|
+
);
|
|
186
|
+
--wave-docs-radius: var(--wave-docs-radius-base);
|
|
187
|
+
--wave-docs-radius-lg: calc(
|
|
188
|
+
var(--wave-docs-radius-base) + var(--wave-docs-radius-step)
|
|
189
|
+
);
|
|
147
190
|
/* The shell. These five plus `--wave-docs-measure` are the whole settable
|
|
148
191
|
* layout surface; the gutter and the drawer width are literals, because each
|
|
149
192
|
* appears once and an ordinary override is already the cleanest tool for
|
|
@@ -217,6 +260,25 @@
|
|
|
217
260
|
}
|
|
218
261
|
|
|
219
262
|
/* Only for a host that has explicitly delegated the choice to the OS. */
|
|
263
|
+
/*
|
|
264
|
+
* ⚠️ A SQUIRCLE READS TIGHTER THAN A CIRCULAR ARC AT THE SAME RADIUS, so
|
|
265
|
+
* where `corner-shape` is live the root moves up to restore the roundness
|
|
266
|
+
* the numbers were chosen for. `@waveso/ui` does the same, to the same
|
|
267
|
+
* value, and the two have to agree or a page running both shows two
|
|
268
|
+
* different corners on the same screen.
|
|
269
|
+
*
|
|
270
|
+
* Only the root moves. Every tier is a `calc()` off it and the panel's
|
|
271
|
+
* padding is the step, so the concentric arithmetic survives untouched — and
|
|
272
|
+
* a browser without squircles keeps the original scale rather than a scale
|
|
273
|
+
* tuned for a shape it cannot draw.
|
|
274
|
+
*/
|
|
275
|
+
@supports (corner-shape: squircle) {
|
|
276
|
+
:root {
|
|
277
|
+
--wave-docs-radius-base: 1rem;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
|
|
220
282
|
@media (prefers-color-scheme: dark) {
|
|
221
283
|
:root[data-theme='system'] {
|
|
222
284
|
color-scheme: dark;
|
|
@@ -394,6 +456,7 @@
|
|
|
394
456
|
.wave-docs-toc,
|
|
395
457
|
.wave-docs-skip-link,
|
|
396
458
|
.wave-docs-hero,
|
|
459
|
+
.wave-docs-explore,
|
|
397
460
|
.wave-docs-pager,
|
|
398
461
|
.wave-docs-layout__sidebar,
|
|
399
462
|
.wave-docs-search-trigger,
|
|
@@ -659,11 +722,27 @@
|
|
|
659
722
|
accent-color: var(--wave-docs-accent);
|
|
660
723
|
}
|
|
661
724
|
|
|
725
|
+
/*
|
|
726
|
+
* ⚠️ A FULL BORDER, AND IT WAS A 3px RULE DOWN THE INLINE START.
|
|
727
|
+
*
|
|
728
|
+
* A quote is a block set apart from the prose, and every other block set
|
|
729
|
+
* apart from the prose here — a callout, a code frame, a table, an embed —
|
|
730
|
+
* is a box. A single edge made it the one exception, and next to a callout
|
|
731
|
+
* two paragraphs away it read as a different kind of thing rather than as a
|
|
732
|
+
* quieter one.
|
|
733
|
+
*
|
|
734
|
+
* The same box as `.wave-docs-callout`, minus the hue: same padding, same
|
|
735
|
+
* block radius, a plain border instead of a tinted one and no accent edge.
|
|
736
|
+
* That is the relationship — a callout is a quote with a colour, and now it
|
|
737
|
+
* looks like one.
|
|
738
|
+
*/
|
|
662
739
|
.wave-docs-prose blockquote {
|
|
663
|
-
padding
|
|
664
|
-
|
|
740
|
+
/* The callout's padding, so the two line their text up with each other. */
|
|
741
|
+
padding: 0.875rem 1rem;
|
|
742
|
+
border: 1px solid var(--wave-docs-border);
|
|
743
|
+
border-radius: var(--wave-docs-radius-lg);
|
|
665
744
|
color: var(--wave-docs-fg-muted);
|
|
666
|
-
/* Not italic. The
|
|
745
|
+
/* Not italic. The box and the muted colour already say "quotation"; a
|
|
667
746
|
* long italic passage is measurably slower to read, and markdown authors
|
|
668
747
|
* use blockquotes for asides and notes, not only for speech. */
|
|
669
748
|
font-style: normal;
|
|
@@ -693,7 +772,7 @@
|
|
|
693
772
|
.wave-docs-image {
|
|
694
773
|
max-width: 100%;
|
|
695
774
|
height: auto;
|
|
696
|
-
border-radius: var(--wave-docs-radius);
|
|
775
|
+
border-radius: var(--wave-docs-radius-lg);
|
|
697
776
|
}
|
|
698
777
|
|
|
699
778
|
/* Inline code only. `.shiki` owns everything inside a `<pre>`. */
|
|
@@ -709,6 +788,49 @@
|
|
|
709
788
|
}
|
|
710
789
|
}
|
|
711
790
|
|
|
791
|
+
/* ---------------------------------------------------------------------------
|
|
792
|
+
* Squircle corners
|
|
793
|
+
*
|
|
794
|
+
* `corner-shape` draws every `border-radius` as a continuous superellipse
|
|
795
|
+
* rather than a circular arc — softer, and what `@waveso/ui` renders — and
|
|
796
|
+
* degrades to ordinary rounding where it is unsupported. Chrome has it today;
|
|
797
|
+
* Safari and Firefox do not, so this is an enhancement and never a dependency.
|
|
798
|
+
* ------------------------------------------------------------------------ */
|
|
799
|
+
|
|
800
|
+
@layer components {
|
|
801
|
+
@supports (corner-shape: squircle) {
|
|
802
|
+
/*
|
|
803
|
+
* ⚠️ SCOPED TO ELEMENTS THIS PACKAGE OWNS, AND NOT `*`.
|
|
804
|
+
*
|
|
805
|
+
* `@waveso/ui` can say `*` because it is the application's own stylesheet.
|
|
806
|
+
* This one is mounted inside somebody else's page, and a bare `*` would
|
|
807
|
+
* reshape every corner the host drew — the same class of trespass as
|
|
808
|
+
* claiming `html` or `body`, which this file already refuses.
|
|
809
|
+
*
|
|
810
|
+
* The attribute pair matches our classes wherever they sit in a class
|
|
811
|
+
* list. The prose elements are named separately because they are the
|
|
812
|
+
* markdown author's tags rather than ours: an inline `<code>` chip, a
|
|
813
|
+
* `<kbd>`, an excluded `<pre>`. They are rounded by rules scoped to
|
|
814
|
+
* `.wave-docs-prose`, so they are ours to shape.
|
|
815
|
+
*/
|
|
816
|
+
:is([class^='wave-docs-'], [class*=' wave-docs-']),
|
|
817
|
+
.wave-docs-prose :is(blockquote, code, kbd, pre) {
|
|
818
|
+
corner-shape: squircle;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/*
|
|
822
|
+
* ⚠️ PILLS AND DOTS OPT BACK OUT. A squircle at `9999px` is a lozenge with
|
|
823
|
+
* flattened ends rather than a pill, and a squircled dot is a rounded
|
|
824
|
+
* square. Both are pseudo-elements, which `[class]` selectors cannot
|
|
825
|
+
* reach, so they are named.
|
|
826
|
+
*/
|
|
827
|
+
.wave-docs-layout__sidebar-trigger::before,
|
|
828
|
+
.wave-docs-layout__sidebar-trigger::after {
|
|
829
|
+
corner-shape: round;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
712
834
|
/* ---------------------------------------------------------------------------
|
|
713
835
|
* Tables
|
|
714
836
|
* ------------------------------------------------------------------------ */
|
|
@@ -744,7 +866,21 @@
|
|
|
744
866
|
grid-template-columns: minmax(max-content, 1fr);
|
|
745
867
|
overflow-x: auto;
|
|
746
868
|
border: 1px solid var(--wave-docs-border);
|
|
747
|
-
|
|
869
|
+
/*
|
|
870
|
+
* ⚠️ THE PANEL'S OUTER RADIUS ON A BOX THAT IS NOT A PANEL, AND THAT IS
|
|
871
|
+
* DELIBERATE.
|
|
872
|
+
*
|
|
873
|
+
* A table wore `.wave-docs-panel` for a while — frame, band, inset card.
|
|
874
|
+
* The panel exists to separate *chrome* from *content*, and a table's
|
|
875
|
+
* header row is content: setting the body into a card away from its own
|
|
876
|
+
* header cost three vertical rules down each side, stopped the dividers
|
|
877
|
+
* short of the box and narrowed the reading width, on the densest element
|
|
878
|
+
* on a page. Full-width dividers are what let an eye track a row across.
|
|
879
|
+
*
|
|
880
|
+
* The radius is what is kept, so a table and a code block still read as two
|
|
881
|
+
* of one family without the table pretending to chrome it has not got.
|
|
882
|
+
*/
|
|
883
|
+
border-radius: var(--wave-docs-radius-lg);
|
|
748
884
|
scrollbar-width: thin;
|
|
749
885
|
scrollbar-color: var(--wave-docs-border-strong) transparent;
|
|
750
886
|
/*
|
|
@@ -777,8 +913,21 @@
|
|
|
777
913
|
right / 0.75rem 100% no-repeat scroll;
|
|
778
914
|
}
|
|
779
915
|
|
|
916
|
+
/*
|
|
917
|
+
* A table wears the panel: the outer frame, and its scroll region as the
|
|
918
|
+
* inset surface. There is no header row — a GFM table has no caption to put
|
|
919
|
+
* in one — so this is the frame alone, which is the point. It is what makes
|
|
920
|
+
* a table, a code block and "where to go next" read as three of one thing
|
|
921
|
+
* rather than three boxes that happen to be near each other.
|
|
922
|
+
*/
|
|
780
923
|
.wave-docs-table-scroll:focus-visible {
|
|
781
924
|
outline: 2px solid var(--wave-docs-accent);
|
|
925
|
+
/*
|
|
926
|
+
* Positive, unlike the `<pre>`'s. This element *is* the scroll container,
|
|
927
|
+
* and an element's own overflow does not clip its own outline — so the ring
|
|
928
|
+
* draws into the frame's 4px band around it, which is exactly where there
|
|
929
|
+
* is room for it.
|
|
930
|
+
*/
|
|
782
931
|
outline-offset: 2px;
|
|
783
932
|
}
|
|
784
933
|
|
|
@@ -999,6 +1148,24 @@
|
|
|
999
1148
|
white-space: nowrap;
|
|
1000
1149
|
}
|
|
1001
1150
|
|
|
1151
|
+
/*
|
|
1152
|
+
* ⚠️ AN EMPTY HEADER ROW DRAWS NO BAND, AND GFM PRODUCES ONE ROUTINELY.
|
|
1153
|
+
*
|
|
1154
|
+
* A GFM table *always* has a `<thead>` — the delimiter row is what makes it a
|
|
1155
|
+
* table at all — so an author who wants a plain two-column list of facts
|
|
1156
|
+
* writes `| | |` and gets a header of empty `<th>`s. With the card starting
|
|
1157
|
+
* at the body, that painted a bare strip of the frame's ground above it with
|
|
1158
|
+
* nothing in it: a panel with an empty header, which reads as a rendering
|
|
1159
|
+
* fault rather than as a table without column names.
|
|
1160
|
+
*
|
|
1161
|
+
* `:empty` and not a text check: GFM emits `<th></th>` for a blank cell, with
|
|
1162
|
+
* no whitespace inside it. A header with even one named column keeps its
|
|
1163
|
+
* band, which is the case the band exists for.
|
|
1164
|
+
*/
|
|
1165
|
+
.wave-docs-table thead:not(:has(th:not(:empty))) {
|
|
1166
|
+
display: none;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1002
1169
|
/*
|
|
1003
1170
|
* ⚠️ `tbody tr + tr` ONLY. `thead` WAS IN THIS SELECTOR AND DOUBLED THE FRAME.
|
|
1004
1171
|
*
|
|
@@ -1008,9 +1175,9 @@
|
|
|
1008
1175
|
* `--wave-docs-border` instead of one, which reads as a thick or doubled
|
|
1009
1176
|
* line rather than as a frame.
|
|
1010
1177
|
*
|
|
1011
|
-
* The header's separator is the one below it,
|
|
1012
|
-
*
|
|
1013
|
-
*
|
|
1178
|
+
* The header's separator is the one below it, drawn by the `inset 0 -1px 0`
|
|
1179
|
+
* box-shadow on `thead th` — a shadow rather than a border so it survives
|
|
1180
|
+
* the cell being `position: sticky`.
|
|
1014
1181
|
*/
|
|
1015
1182
|
.wave-docs-table tbody tr + tr {
|
|
1016
1183
|
border-block-start: 1px solid var(--wave-docs-border);
|
|
@@ -1076,12 +1243,37 @@
|
|
|
1076
1243
|
* ---------------------------------------------------------------------- */
|
|
1077
1244
|
|
|
1078
1245
|
.wave-docs-code {
|
|
1079
|
-
position: relative;
|
|
1080
1246
|
/*
|
|
1081
|
-
* ⚠️
|
|
1082
|
-
*
|
|
1083
|
-
*
|
|
1084
|
-
*
|
|
1247
|
+
* ⚠️ A GRID ON THE FIGURE ITSELF, NOT A `.wave-docs-panel__header` WRAPPER.
|
|
1248
|
+
*
|
|
1249
|
+
* A `<figcaption>` has to be a direct child of its `<figure>`. Wrapped in
|
|
1250
|
+
* the header `<div>` "where to go next" uses, it captions nothing: the
|
|
1251
|
+
* markup is invalid and a titled block loses the accessible name it had.
|
|
1252
|
+
* So the frame wears `.wave-docs-panel`, its surface wears
|
|
1253
|
+
* `.wave-docs-panel__body`, and the header row is laid out here — sharing
|
|
1254
|
+
* the primitive's insets rather than its header element.
|
|
1255
|
+
*
|
|
1256
|
+
* ⚠️ AND THE FIRST ROW IS FLOORED RATHER THAN LEFT TO ITS CONTENT. With a
|
|
1257
|
+
* label it is the label's height; with none it is the copy button's, and
|
|
1258
|
+
* those differ by 4px — enough that a page mixing titled and untitled
|
|
1259
|
+
* fences shows two header heights and reads as a rendering bug.
|
|
1260
|
+
*/
|
|
1261
|
+
display: grid;
|
|
1262
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
1263
|
+
grid-template-rows: minmax(var(--wave-docs-panel-header-row), auto) auto;
|
|
1264
|
+
align-items: center;
|
|
1265
|
+
/*
|
|
1266
|
+
* The code's own ground rather than the panel's default. A code block is
|
|
1267
|
+
* part of the surface ramp and a reader expects it darker than the page;
|
|
1268
|
+
* the panel paints `--wave-docs-bg` for a widget made of prose, which is
|
|
1269
|
+
* the right default and the wrong one here. Set on the frame so it reaches
|
|
1270
|
+
* the surface by inheritance — see the property's note on the panel.
|
|
1271
|
+
*/
|
|
1272
|
+
--wave-docs-panel-surface: var(--wave-docs-code-block-bg);
|
|
1273
|
+
/*
|
|
1274
|
+
* ⚠️ `margin-inline`, NOT `margin`. A default `<figure>` margin would
|
|
1275
|
+
* indent the whole block by 40px — which is what this rule was written
|
|
1276
|
+
* for.
|
|
1085
1277
|
*
|
|
1086
1278
|
* Zeroing all four also took the *block* margin, and `.wave-docs-prose > *
|
|
1087
1279
|
* + *` is the same specificity and declared earlier, so this won. Every
|
|
@@ -1092,68 +1284,113 @@
|
|
|
1092
1284
|
margin-inline: 0;
|
|
1093
1285
|
}
|
|
1094
1286
|
|
|
1095
|
-
.wave-docs-code__title {
|
|
1096
|
-
display: flex;
|
|
1097
|
-
align-items: center;
|
|
1098
|
-
/* Room for the button, which is absolutely positioned over this row. */
|
|
1099
|
-
padding: 0.5rem 3rem 0.5rem 1.125rem;
|
|
1100
|
-
border: 1px solid var(--wave-docs-border);
|
|
1101
|
-
border-block-end: 0;
|
|
1102
|
-
border-start-start-radius: var(--wave-docs-radius);
|
|
1103
|
-
border-start-end-radius: var(--wave-docs-radius);
|
|
1104
|
-
background-color: var(--wave-docs-bg-subtle);
|
|
1105
|
-
color: var(--wave-docs-fg-muted);
|
|
1106
|
-
font-family: var(--wave-docs-font-mono);
|
|
1107
|
-
font-size: 0.8125rem;
|
|
1108
|
-
}
|
|
1109
|
-
|
|
1110
|
-
/* Square off the top of a `<pre>` that sits under a title bar.
|
|
1111
|
-
*
|
|
1112
|
-
* `.wave-docs-prose` first, even though `.wave-docs-code` is our class and
|
|
1113
|
-
* only ever appears inside it: `styles.test.ts` requires every `.shiki` rule
|
|
1114
|
-
* to name the prose scope, so the package can never style a code block it
|
|
1115
|
-
* did not render. One form for that invariant is worth the extra selector. */
|
|
1116
1287
|
/*
|
|
1117
|
-
* ⚠️
|
|
1288
|
+
* ⚠️ AN UNTITLED FENCE HAS NO FRAME AT ALL, AND THAT IS THE WHOLE RULE.
|
|
1118
1289
|
*
|
|
1119
|
-
*
|
|
1120
|
-
*
|
|
1121
|
-
*
|
|
1122
|
-
*
|
|
1123
|
-
* a caption floating over a gap, which is the one arrangement the frame was
|
|
1124
|
-
* designed not to be.
|
|
1290
|
+
* With a title the figure is a panel: a band carrying the filename and the
|
|
1291
|
+
* copy button, and the code set into a card below it. With none there is
|
|
1292
|
+
* nothing to put in a band — so the frame flattens away, the surface becomes
|
|
1293
|
+
* the block, and the button sits on the code.
|
|
1125
1294
|
*
|
|
1126
|
-
*
|
|
1127
|
-
*
|
|
1128
|
-
*
|
|
1295
|
+
* The language does not count as a title. A fence that declares `ts` and no
|
|
1296
|
+
* filename is still an untitled fence, and a band holding a two-letter badge
|
|
1297
|
+
* is the empty-header problem with a word in it. `data-lang` stays on the
|
|
1298
|
+
* figure for anyone selecting on it.
|
|
1129
1299
|
*
|
|
1130
|
-
*
|
|
1131
|
-
* and
|
|
1132
|
-
*
|
|
1300
|
+
* One shape of markup, switched here rather than in the pipeline: two markup
|
|
1301
|
+
* paths mean two fixtures, and the one that is not on screen is the one that
|
|
1302
|
+
* rots.
|
|
1133
1303
|
*/
|
|
1134
|
-
.wave-docs-
|
|
1135
|
-
|
|
1304
|
+
.wave-docs-code:not(:has(.wave-docs-code__title)) {
|
|
1305
|
+
/* The containing block for the button below. The grid has one child left
|
|
1306
|
+
* in flow, so it has nothing to lay out. */
|
|
1307
|
+
position: relative;
|
|
1308
|
+
display: block;
|
|
1309
|
+
padding: 0;
|
|
1310
|
+
border: 0;
|
|
1311
|
+
background: transparent;
|
|
1136
1312
|
}
|
|
1137
1313
|
|
|
1138
|
-
.wave-docs-
|
|
1139
|
-
|
|
1140
|
-
|
|
1314
|
+
.wave-docs-code:not(:has(.wave-docs-code__title)) .wave-docs-code__body {
|
|
1315
|
+
/* It is the outer box now, so it takes the outer radius. Leaving it at the
|
|
1316
|
+
* card's would round an unframed block more tightly than a framed one. */
|
|
1317
|
+
border-radius: var(--wave-docs-radius-lg);
|
|
1141
1318
|
}
|
|
1142
1319
|
|
|
1143
|
-
.wave-docs-code__copy {
|
|
1320
|
+
.wave-docs-code:not(:has(.wave-docs-code__title)) .wave-docs-code__copy {
|
|
1321
|
+
/*
|
|
1322
|
+
* On the code rather than beside it. `z-index` because the `<pre>` is a
|
|
1323
|
+
* scroll container and paints its own content above a static sibling; the
|
|
1324
|
+
* button would otherwise disappear under a wide line rather than over it.
|
|
1325
|
+
*/
|
|
1144
1326
|
position: absolute;
|
|
1145
|
-
|
|
1146
|
-
inset-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1327
|
+
z-index: 1;
|
|
1328
|
+
inset-block-start: 0.375rem;
|
|
1329
|
+
inset-inline-end: 0.375rem;
|
|
1330
|
+
margin-inline-end: 0;
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
/*
|
|
1334
|
+
* The label slot: the fence's `title="…"`, which is the only thing that goes
|
|
1335
|
+
* in it.
|
|
1336
|
+
One or the other and never both — `swap.ts` beside a `ts` badge
|
|
1337
|
+
* is the same fact twice, and the filename is the more precise half.
|
|
1338
|
+
*/
|
|
1339
|
+
.wave-docs-code__title {
|
|
1340
|
+
grid-row: 1;
|
|
1341
|
+
grid-column: 1;
|
|
1342
|
+
/* The track is `minmax(0, 1fr)` so a long path can shrink it; these two let
|
|
1343
|
+
* the path wrap inside that track instead of pushing the button out of the
|
|
1344
|
+
* frame. */
|
|
1345
|
+
min-width: 0;
|
|
1346
|
+
overflow-wrap: anywhere;
|
|
1347
|
+
/*
|
|
1348
|
+
* ⚠️ THE `+ 1px` IS THE SURFACE'S BORDER, AND IT IS THE SAME SUBTRACTION
|
|
1349
|
+
* `.wave-docs-panel__header` MAKES — the reason the inset is exported at
|
|
1350
|
+
* all. The label's first character and the code's first character sit on
|
|
1351
|
+
* one column; without it they miss by exactly one border, which appears in
|
|
1352
|
+
* no rule and reads as a design decision.
|
|
1353
|
+
*/
|
|
1354
|
+
padding-inline-start: calc(var(--wave-docs-panel-inset) + 1px);
|
|
1355
|
+
color: var(--wave-docs-fg-muted);
|
|
1356
|
+
font-family: var(--wave-docs-font-mono);
|
|
1357
|
+
font-size: 0.8125rem;
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
.wave-docs-code__body {
|
|
1361
|
+
grid-column: 1 / -1;
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
.wave-docs-code__copy {
|
|
1365
|
+
grid-row: 1;
|
|
1366
|
+
grid-column: 2;
|
|
1367
|
+
/*
|
|
1368
|
+
* Optically inset, not aligned to the label's column: a 2rem hit target
|
|
1369
|
+
* held 17px off the frame's edge reads as floating in the middle of the
|
|
1370
|
+
* row rather than as the control that ends it.
|
|
1371
|
+
*/
|
|
1372
|
+
margin-inline-end: calc(var(--wave-docs-panel-inset) - 0.5rem);
|
|
1373
|
+
/* A grid, so the three state icons stack in one cell. */
|
|
1374
|
+
display: grid;
|
|
1375
|
+
place-items: center;
|
|
1376
|
+
/*
|
|
1377
|
+
* A 2rem hit target around a 1rem glyph. The box stays even though nothing
|
|
1378
|
+
* draws it at rest — a control smaller than the icon plus its padding is a
|
|
1379
|
+
* control people miss on a touch screen.
|
|
1380
|
+
*/
|
|
1150
1381
|
width: 2rem;
|
|
1151
1382
|
height: 2rem;
|
|
1152
|
-
|
|
1383
|
+
/*
|
|
1384
|
+
* ⚠️ NO BORDER AND NO GROUND, IN ANY STATE. It had both, and a bordered,
|
|
1385
|
+
* filled 2rem box on the frame's own band is a third framed rectangle
|
|
1386
|
+
* inside a frame that already has two — for a control secondary to
|
|
1387
|
+
* everything around it. The glyph is the whole control; hover moves its
|
|
1388
|
+
* ink, not a box behind it.
|
|
1389
|
+
*/
|
|
1390
|
+
border: 0;
|
|
1153
1391
|
border-radius: var(--wave-docs-radius-sm);
|
|
1154
|
-
background-color:
|
|
1155
|
-
color: var(--wave-docs-fg-
|
|
1156
|
-
font-size: 0.875rem;
|
|
1392
|
+
background-color: transparent;
|
|
1393
|
+
color: var(--wave-docs-fg-subtle);
|
|
1157
1394
|
cursor: pointer;
|
|
1158
1395
|
/*
|
|
1159
1396
|
* ⚠️ HIDDEN UNTIL THE RUNTIME SAYS OTHERWISE, AND THIS IS STRUCTURAL.
|
|
@@ -1165,71 +1402,88 @@
|
|
|
1165
1402
|
* a control that is invisible, focusable, and does nothing.
|
|
1166
1403
|
*/
|
|
1167
1404
|
visibility: hidden;
|
|
1168
|
-
opacity: 0;
|
|
1169
|
-
transition: opacity 120ms ease;
|
|
1170
|
-
}
|
|
1171
|
-
|
|
1172
|
-
@media (prefers-reduced-motion: reduce) {
|
|
1173
|
-
.wave-docs-code__copy {
|
|
1174
|
-
transition: none;
|
|
1175
|
-
}
|
|
1176
1405
|
}
|
|
1177
1406
|
|
|
1178
1407
|
html[data-wave-docs-code-ready] .wave-docs-code__copy {
|
|
1179
1408
|
visibility: visible;
|
|
1180
1409
|
}
|
|
1181
1410
|
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
* button sitting flush on the bar's own border, which reads as misaligned
|
|
1195
|
-
* rather than as inset.
|
|
1196
|
-
*
|
|
1197
|
-
* The bar is 40px and the button is 32px, so 4px is the centre. A `top:
|
|
1198
|
-
* 50%` would be wrong here: the percentage resolves against the figure,
|
|
1199
|
-
* which includes the code block below.
|
|
1200
|
-
*/
|
|
1201
|
-
top: 0.25rem;
|
|
1202
|
-
}
|
|
1411
|
+
/*
|
|
1412
|
+
* ⚠️ NO HOVER REVEAL, AND ITS ABSENCE IS THE POINT.
|
|
1413
|
+
*
|
|
1414
|
+
* The button used to fade in on `:hover` or `:focus-within`, because it was
|
|
1415
|
+
* absolutely positioned over the code and had nowhere of its own to be. It
|
|
1416
|
+
* has a slot in the header row now, and a reserved slot that is empty until
|
|
1417
|
+
* you point at it reads as a rendering fault rather than as restraint — so
|
|
1418
|
+
* the reveal, the `@media (hover: none)` exception that existed because a
|
|
1419
|
+
* hover-only control does not exist on a phone, and the reduced-motion guard
|
|
1420
|
+
* on its transition all went with it.
|
|
1421
|
+
*/
|
|
1422
|
+
|
|
1203
1423
|
|
|
1204
1424
|
/*
|
|
1205
|
-
*
|
|
1206
|
-
*
|
|
1425
|
+
* ⚠️ INK ONLY — NO GROUND ON HOVER EITHER. The accent is the same signal
|
|
1426
|
+
* every other interactive surface in this package gives, and it needs no box
|
|
1427
|
+
* behind it to be read. A ground here also had nowhere to come from: the
|
|
1428
|
+
* subtle ramp *is* the frame's colour, so the hover state this used to have
|
|
1429
|
+
* was the colour the button was already sitting on.
|
|
1207
1430
|
*/
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
opacity: 1;
|
|
1211
|
-
}
|
|
1431
|
+
.wave-docs-code__copy:hover {
|
|
1432
|
+
color: var(--wave-docs-accent);
|
|
1212
1433
|
}
|
|
1213
1434
|
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1435
|
+
/*
|
|
1436
|
+
* ⚠️ AND IT HAD NO FOCUS RULE AT ALL, WHICH THE BORDER WAS QUIETLY COVERING
|
|
1437
|
+
* FOR.
|
|
1438
|
+
*
|
|
1439
|
+
* A 1px box around a control is not a focus indicator — it is there whether
|
|
1440
|
+
* the control is focused or not — so a keyboard reader tabbing onto this
|
|
1441
|
+
* button got a `color` change and nothing else, and the package's own
|
|
1442
|
+
* inventory of focusable surfaces did not list it. Taking the border away
|
|
1443
|
+
* makes that visible; it does not create it.
|
|
1444
|
+
*/
|
|
1445
|
+
.wave-docs-code__copy:focus-visible {
|
|
1446
|
+
outline: 2px solid var(--wave-docs-accent);
|
|
1447
|
+
outline-offset: 2px;
|
|
1217
1448
|
}
|
|
1218
1449
|
|
|
1219
1450
|
/* The success state, swapped by CSS rather than by React: no component owns
|
|
1220
1451
|
* a button, so there is no state to re-render and nothing to hydrate. */
|
|
1452
|
+
/*
|
|
1453
|
+
* ⚠️ ONE OF THREE ICONS, SWAPPED BY `display` AND NOT BY `visibility`.
|
|
1454
|
+
*
|
|
1455
|
+
* All three ship in the markup — no component owns this button, so there is
|
|
1456
|
+
* no state to re-render and nothing to hydrate; the runtime writes one
|
|
1457
|
+
* attribute and the stylesheet picks.
|
|
1458
|
+
*
|
|
1459
|
+
* `visibility` is the wrong property here and would have been a real defect:
|
|
1460
|
+
* the button is `visibility: hidden` until the runtime attaches, and
|
|
1461
|
+
* `visibility` inherits, so a child setting it back to `visible` shows an
|
|
1462
|
+
* icon inside a button that is meant to be invisible and out of the tab
|
|
1463
|
+
* order. `display` does not inherit, so the button's own rule still wins.
|
|
1464
|
+
*/
|
|
1465
|
+
.wave-docs-code__copy-icon {
|
|
1466
|
+
/* Stacked in one cell, so the button does not resize as the state
|
|
1467
|
+
* changes and the glyph does not shift by a pixel between them. */
|
|
1468
|
+
grid-area: 1 / 1;
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
.wave-docs-code__copy-icon:not([data-state='idle']) {
|
|
1472
|
+
display: none;
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
/* The success state, swapped by CSS rather than by React. */
|
|
1221
1476
|
.wave-docs-code__copy[data-copied='true'] {
|
|
1222
|
-
border-color: var(--wave-docs-callout-tip);
|
|
1223
1477
|
color: var(--wave-docs-callout-tip);
|
|
1224
1478
|
}
|
|
1225
1479
|
|
|
1226
|
-
.wave-docs-code__copy[data-copied='true']
|
|
1227
|
-
|
|
1480
|
+
.wave-docs-code__copy[data-copied='true'] .wave-docs-code__copy-icon {
|
|
1481
|
+
display: none;
|
|
1228
1482
|
}
|
|
1229
1483
|
|
|
1230
|
-
.wave-docs-code__copy[data-copied='true']
|
|
1231
|
-
|
|
1232
|
-
|
|
1484
|
+
.wave-docs-code__copy[data-copied='true']
|
|
1485
|
+
.wave-docs-code__copy-icon[data-state='copied'] {
|
|
1486
|
+
display: block;
|
|
1233
1487
|
}
|
|
1234
1488
|
|
|
1235
1489
|
/*
|
|
@@ -1242,21 +1496,21 @@
|
|
|
1242
1496
|
* phone over `http://192.168.x.x:3000` is not a secure context, so
|
|
1243
1497
|
* `navigator.clipboard` is undefined and no amount of pressing helps. A
|
|
1244
1498
|
* silent no-op is the worst possible answer to that.
|
|
1499
|
+
*
|
|
1500
|
+
* A cross, not a warning triangle: it pairs with the tick at the same
|
|
1501
|
+
* stroke weight, and the triangle reads as a page-level alert.
|
|
1245
1502
|
*/
|
|
1246
1503
|
.wave-docs-code__copy[data-copied='false'] {
|
|
1247
|
-
border-color: var(--wave-docs-callout-caution);
|
|
1248
1504
|
color: var(--wave-docs-callout-caution);
|
|
1249
1505
|
}
|
|
1250
1506
|
|
|
1251
|
-
.wave-docs-code__copy[data-copied='false']
|
|
1252
|
-
|
|
1507
|
+
.wave-docs-code__copy[data-copied='false'] .wave-docs-code__copy-icon {
|
|
1508
|
+
display: none;
|
|
1253
1509
|
}
|
|
1254
1510
|
|
|
1255
|
-
.wave-docs-code__copy[data-copied='false']
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
content: '\00d7';
|
|
1259
|
-
position: absolute;
|
|
1511
|
+
.wave-docs-code__copy[data-copied='false']
|
|
1512
|
+
.wave-docs-code__copy-icon[data-state='failed'] {
|
|
1513
|
+
display: block;
|
|
1260
1514
|
}
|
|
1261
1515
|
|
|
1262
1516
|
/*
|
|
@@ -1287,7 +1541,7 @@
|
|
|
1287
1541
|
.wave-docs-prose pre:not(.shiki) {
|
|
1288
1542
|
padding: 1rem 1.125rem;
|
|
1289
1543
|
border: 1px solid var(--wave-docs-border);
|
|
1290
|
-
border-radius: var(--wave-docs-radius);
|
|
1544
|
+
border-radius: var(--wave-docs-radius-lg);
|
|
1291
1545
|
background-color: var(--wave-docs-code-block-bg);
|
|
1292
1546
|
overflow-x: auto;
|
|
1293
1547
|
font-family: var(--wave-docs-font-mono);
|
|
@@ -1297,13 +1551,37 @@
|
|
|
1297
1551
|
}
|
|
1298
1552
|
|
|
1299
1553
|
.wave-docs-prose .shiki {
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
*
|
|
1305
|
-
|
|
1554
|
+
/*
|
|
1555
|
+
* ⚠️ NO BORDER, NO RADIUS AND NO BACKGROUND OF ITS OWN — THE PANEL'S
|
|
1556
|
+
* SURFACE CARRIES ALL THREE NOW. `rehypeCodeFrame` puts every highlighted
|
|
1557
|
+
* fence inside a `.wave-docs-panel__body`, so declaring them here draws a
|
|
1558
|
+
* second frame one pixel inside the first. `pre:not(.shiki)` above keeps
|
|
1559
|
+
* its own, because an excluded fence is never wrapped.
|
|
1560
|
+
*
|
|
1561
|
+
* ⚠️ AND THE INLINE PADDING IS `1rem` BECAUSE THAT IS
|
|
1562
|
+
* `--wave-docs-panel-inset`, NOT BECAUSE IT IS A ROUND NUMBER. The frame's
|
|
1563
|
+
* label sits at that inset plus the surface's border; the first character
|
|
1564
|
+
* of the code sits at the surface's border plus this. Equal, they land on
|
|
1565
|
+
* one column. The `1.125rem` this was put the code 2px right of the
|
|
1566
|
+
* filename above it — visible, and attributable to nothing.
|
|
1567
|
+
*/
|
|
1568
|
+
padding: 1rem;
|
|
1569
|
+
/*
|
|
1570
|
+
* ⚠️ THE `<pre>` SCROLLS AND THE SURFACE AROUND IT CLIPS, WHICH IS WHY
|
|
1571
|
+
* THEY ARE TWO BOXES. One box cannot both round its corners with
|
|
1572
|
+
* `overflow: hidden` and scroll a wide line: the first value wins and the
|
|
1573
|
+
* line is cut off with no way to reach it.
|
|
1574
|
+
*/
|
|
1306
1575
|
overflow-x: auto;
|
|
1576
|
+
/*
|
|
1577
|
+
* ⚠️ THE `<pre>`'s USER-AGENT MARGIN, WHICH UNDOES THE FRAME.
|
|
1578
|
+
*
|
|
1579
|
+
* A `<pre>` defaults to `margin-block: 1em`, and at this block's 0.875rem
|
|
1580
|
+
* that is 14px — measured in Chromium on the real site, not reasoned
|
|
1581
|
+
* about. Left in, it pushes the code away from the surface holding it and
|
|
1582
|
+
* the frame reads as a caption floating over a gap.
|
|
1583
|
+
*/
|
|
1584
|
+
margin: 0;
|
|
1307
1585
|
font-family: var(--wave-docs-font-mono);
|
|
1308
1586
|
font-size: 0.875rem;
|
|
1309
1587
|
line-height: 1.7;
|
|
@@ -1320,7 +1598,17 @@
|
|
|
1320
1598
|
* had no focus style. */
|
|
1321
1599
|
.wave-docs-prose .shiki:focus-visible {
|
|
1322
1600
|
outline: 2px solid var(--wave-docs-accent);
|
|
1323
|
-
|
|
1601
|
+
/*
|
|
1602
|
+
* ⚠️ NEGATIVE, BECAUSE THE SURFACE AROUND THIS ONE CLIPS.
|
|
1603
|
+
*
|
|
1604
|
+
* The `<pre>` fills `.wave-docs-panel__body`, which is `overflow: hidden`
|
|
1605
|
+
* so a square corner cannot poke through the frame's rounded one. An
|
|
1606
|
+
* outline drawn *outside* the `<pre>`'s border box is outside the
|
|
1607
|
+
* surface's content box as well, so the `2px` this was got clipped away
|
|
1608
|
+
* to nothing and the one focusable element in a code block had a focus
|
|
1609
|
+
* style that could not be seen. Inset, it lands on the code's own edge.
|
|
1610
|
+
*/
|
|
1611
|
+
outline-offset: -2px;
|
|
1324
1612
|
}
|
|
1325
1613
|
|
|
1326
1614
|
.wave-docs-prose .shiki code {
|
|
@@ -1357,9 +1645,22 @@
|
|
|
1357
1645
|
--callout-bg: var(--wave-docs-callout-note-bg);
|
|
1358
1646
|
|
|
1359
1647
|
padding: 0.875rem 1rem;
|
|
1648
|
+
/*
|
|
1649
|
+
* ⚠️ ONE UNIFORM EDGE, AND THERE WAS A 3px ACCENT DOWN THE INLINE START.
|
|
1650
|
+
*
|
|
1651
|
+
* Every other block set apart from the prose here is a box with one border
|
|
1652
|
+
* all the way round; the accent edge made a callout the exception, and a
|
|
1653
|
+
* thick rule on one side of a squircled box also fights the corner it runs
|
|
1654
|
+
* into. A blockquote is this box without the hue, which is the relationship
|
|
1655
|
+
* — and a stripe on one of them broke it.
|
|
1656
|
+
*
|
|
1657
|
+
* ⚠️ AND THE TYPE IS STILL NOT CONVEYED BY COLOUR ALONE. The stripe was
|
|
1658
|
+
* never what carried it: the icon and the label — "Note", "Warning" — are
|
|
1659
|
+
* the non-colour signals, and they are unchanged. The tinted border and
|
|
1660
|
+
* ground remain as reinforcement rather than as the whole message.
|
|
1661
|
+
*/
|
|
1360
1662
|
border: 1px solid color-mix(in oklab, var(--callout-accent) 35%, transparent);
|
|
1361
|
-
border-
|
|
1362
|
-
border-radius: var(--wave-docs-radius);
|
|
1663
|
+
border-radius: var(--wave-docs-radius-lg);
|
|
1363
1664
|
background: var(--callout-bg);
|
|
1364
1665
|
}
|
|
1365
1666
|
|
|
@@ -1412,7 +1713,7 @@
|
|
|
1412
1713
|
position: relative;
|
|
1413
1714
|
aspect-ratio: 16 / 9;
|
|
1414
1715
|
overflow: hidden;
|
|
1415
|
-
border-radius: var(--wave-docs-radius);
|
|
1716
|
+
border-radius: var(--wave-docs-radius-lg);
|
|
1416
1717
|
background: oklch(0 0 0);
|
|
1417
1718
|
}
|
|
1418
1719
|
|
|
@@ -1872,7 +2173,16 @@
|
|
|
1872
2173
|
*/
|
|
1873
2174
|
.wave-docs-sidebar__link,
|
|
1874
2175
|
.wave-docs-sidebar__group-button,
|
|
1875
|
-
.wave-docs-sidebar__group-toggle
|
|
2176
|
+
.wave-docs-sidebar__group-toggle,
|
|
2177
|
+
/*
|
|
2178
|
+
* A search result is the same object as a sidebar row — a link that tints
|
|
2179
|
+
* under the pointer — so it eases the same way. Moving over a result
|
|
2180
|
+
* *activates* it, which is why there is no separate hover rule to ease:
|
|
2181
|
+
* the state below is both.
|
|
2182
|
+
*/
|
|
2183
|
+
.wave-docs-search-result,
|
|
2184
|
+
.wave-docs-search-result-heading,
|
|
2185
|
+
.wave-docs-search-result-icon {
|
|
1876
2186
|
transition:
|
|
1877
2187
|
background-color 150ms ease-out,
|
|
1878
2188
|
color 150ms ease-out;
|
|
@@ -1888,6 +2198,247 @@
|
|
|
1888
2198
|
}
|
|
1889
2199
|
}
|
|
1890
2200
|
|
|
2201
|
+
/* ---------------------------------------------------------------------------
|
|
2202
|
+
* Panel
|
|
2203
|
+
*
|
|
2204
|
+
* A framed block with a header and an inset surface: the outer card names the
|
|
2205
|
+
* thing and carries its controls, the inner one holds the content.
|
|
2206
|
+
*
|
|
2207
|
+
* Its own section because it is not one component's furniture — "where to go
|
|
2208
|
+
* next" is the first thing to wear it and a code frame is the next, and two
|
|
2209
|
+
* copies of the same three rules is how they drift apart.
|
|
2210
|
+
* ------------------------------------------------------------------------ */
|
|
2211
|
+
|
|
2212
|
+
@layer components {
|
|
2213
|
+
/*
|
|
2214
|
+
* ⚠️ THE TWO RADII ARE NOT INDEPENDENT NUMBERS. A rounded box inside a
|
|
2215
|
+
* rounded box only looks right when the inner radius is the outer one minus
|
|
2216
|
+
* the gap between them — otherwise the corners run at different curvatures
|
|
2217
|
+
* and the inner box reads as *pasted onto* the frame rather than set into it.
|
|
2218
|
+
*
|
|
2219
|
+
* The tokens are chosen so that arithmetic lands on one of them: `1rem` outer
|
|
2220
|
+
* minus `0.5rem` of padding is `0.5rem`, which is `--wave-docs-radius`. Change
|
|
2221
|
+
* the padding and the body's radius has to move with it.
|
|
2222
|
+
*/
|
|
2223
|
+
.wave-docs-panel {
|
|
2224
|
+
/*
|
|
2225
|
+
* ⚠️ THE STEP, NOT A LITERAL `4px` THAT HAPPENS TO MATCH IT. The inset
|
|
2226
|
+
* surface below takes the base radius and this frame takes `-lg`, which is
|
|
2227
|
+
* the base plus one step — so the surface's corner is concentric with the
|
|
2228
|
+
* frame's only while this padding *is* that step. Written as the token, a
|
|
2229
|
+
* host moving `--wave-docs-radius-base` keeps both true; written as `4px`,
|
|
2230
|
+
* the two drift the first time anyone retunes the scale.
|
|
2231
|
+
*/
|
|
2232
|
+
padding: var(--wave-docs-radius-step);
|
|
2233
|
+
border: 1px solid var(--wave-docs-border);
|
|
2234
|
+
border-radius: var(--wave-docs-radius-lg);
|
|
2235
|
+
background: var(--wave-docs-bg-subtle);
|
|
2236
|
+
/*
|
|
2237
|
+
* ⚠️ THE INSET THE BODY'S CONTENT USES, AND IT IS NOT THE SAME NUMBER AS
|
|
2238
|
+
* THE HEADER'S. The title sits at the frame's padding; anything inside the
|
|
2239
|
+
* body sits at the frame's padding *plus the body's own border*, so the two
|
|
2240
|
+
* columns miss each other by a pixel per border. Measured before this: the
|
|
2241
|
+
* rows of "where to go next" started 9px right of the heading above them —
|
|
2242
|
+
* a number that appears in no rule, and looks like a design decision.
|
|
2243
|
+
*
|
|
2244
|
+
* Exported rather than repeated, because the next component to wear the
|
|
2245
|
+
* panel has to make the same subtraction and will not think to — and the
|
|
2246
|
+
* correction is applied to the *header* below rather than baked in here, so
|
|
2247
|
+
* a component wearing the panel writes the clean value and the panel keeps
|
|
2248
|
+
* its own arithmetic to itself.
|
|
2249
|
+
*/
|
|
2250
|
+
--wave-docs-panel-inset: 1rem;
|
|
2251
|
+
/*
|
|
2252
|
+
* The header row's floor, exported for the same reason the inset is: the
|
|
2253
|
+
* code frame lays its header out on a grid rather than in
|
|
2254
|
+
* `.wave-docs-panel__header`, because a `<figcaption>` cannot live inside a
|
|
2255
|
+
* wrapper, and two components drawing the same row to two different heights
|
|
2256
|
+
* is how a primitive stops being one.
|
|
2257
|
+
*/
|
|
2258
|
+
--wave-docs-panel-header-row: 2.25rem;
|
|
2259
|
+
/*
|
|
2260
|
+
* The surface's ground is a property a wearer sets on the frame — see the
|
|
2261
|
+
* `var()` fallback on `__body`, which is where its default lives.
|
|
2262
|
+
*/
|
|
2263
|
+
}
|
|
2264
|
+
|
|
2265
|
+
/*
|
|
2266
|
+
* The header lives in the frame's padding rather than in a band of its own:
|
|
2267
|
+
* the inset surface below is what draws the line between them, so a border
|
|
2268
|
+
* here would be a second one.
|
|
2269
|
+
*/
|
|
2270
|
+
.wave-docs-panel__header {
|
|
2271
|
+
display: flex;
|
|
2272
|
+
align-items: center;
|
|
2273
|
+
justify-content: space-between;
|
|
2274
|
+
gap: 1rem;
|
|
2275
|
+
/*
|
|
2276
|
+
* ⚠️ THE `+ 1px` IS THE BODY'S BORDER, AND IT IS WHAT PUTS THE TITLE ON THE
|
|
2277
|
+
* SAME COLUMN AS THE CONTENT BELOW IT. The title's box starts at the
|
|
2278
|
+
* frame's padding; the body's content starts at that padding plus the
|
|
2279
|
+
* body's own border, so equal insets miss each other by exactly one border.
|
|
2280
|
+
* Derived rather than written out, so moving `--wave-docs-panel-inset`
|
|
2281
|
+
* moves both.
|
|
2282
|
+
*/
|
|
2283
|
+
min-height: var(--wave-docs-panel-header-row);
|
|
2284
|
+
padding-block: 0.5rem;
|
|
2285
|
+
padding-inline: calc(var(--wave-docs-panel-inset) + 1px);
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2288
|
+
.wave-docs-panel__title {
|
|
2289
|
+
margin: 0;
|
|
2290
|
+
min-width: 0;
|
|
2291
|
+
color: var(--wave-docs-fg);
|
|
2292
|
+
/*
|
|
2293
|
+
* ⚠️ THE TABLE HEADER'S SIZE, DELIBERATELY — they are the same kind of
|
|
2294
|
+
* thing and a page shows both. This and a `<th>` are the label a reader's
|
|
2295
|
+
* eye lands on before the content under it, and 14px against the table's
|
|
2296
|
+
* 15px read as two different levels rather than as one. `styles.test.ts`
|
|
2297
|
+
* pins the pair, because two literals that have to agree are two literals
|
|
2298
|
+
* that drift.
|
|
2299
|
+
*
|
|
2300
|
+
* The weight does not follow. A panel title names the block; a column
|
|
2301
|
+
* header names a column inside one, so the title sits one step heavier.
|
|
2302
|
+
*/
|
|
2303
|
+
font-size: 0.9375rem;
|
|
2304
|
+
font-weight: 650;
|
|
2305
|
+
letter-spacing: -0.005em;
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
/* Whatever the header's right-hand controls turn out to be — a copy button, a
|
|
2309
|
+
* language switcher — they are quiet next to the title. */
|
|
2310
|
+
.wave-docs-panel__actions {
|
|
2311
|
+
flex: none;
|
|
2312
|
+
display: flex;
|
|
2313
|
+
align-items: center;
|
|
2314
|
+
gap: 0.5rem;
|
|
2315
|
+
color: var(--wave-docs-fg-subtle);
|
|
2316
|
+
font-size: 0.8125rem;
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
.wave-docs-panel__body {
|
|
2320
|
+
/* See the radius note above: outer minus this box's inset. */
|
|
2321
|
+
border: 1px solid var(--wave-docs-border);
|
|
2322
|
+
/* The frame minus its padding: 12 - 4 = 8, which is the base radius. See
|
|
2323
|
+
* the token block for why that is one number and not two. */
|
|
2324
|
+
border-radius: var(--wave-docs-radius);
|
|
2325
|
+
/*
|
|
2326
|
+
* ⚠️ THE DEFAULT LIVES IN THE FALLBACK, NOT IN A DECLARATION ON
|
|
2327
|
+
* `.wave-docs-panel` — AND THAT IS THE WHOLE FIX, TWICE OVER.
|
|
2328
|
+
*
|
|
2329
|
+
* First: `.wave-docs-code__body` and `.wave-docs-panel__body` are both one
|
|
2330
|
+
* class, so source order decides and the panel is declared later. A code
|
|
2331
|
+
* frame asking for the darker code ground got the panel's white, on the
|
|
2332
|
+
* page, with both rules present and correct.
|
|
2333
|
+
*
|
|
2334
|
+
* Then, with the ground moved to a property: a frame wears
|
|
2335
|
+
* `.wave-docs-panel` *and* `.wave-docs-code`, so both rules set that
|
|
2336
|
+
* property on the same element at the same specificity — and source order
|
|
2337
|
+
* handed it back to the panel. Measured twice as `oklch(1 0 0)` where
|
|
2338
|
+
* `oklch(0.975 0.003 262)` was written.
|
|
2339
|
+
*
|
|
2340
|
+
* In the fallback there is no declaration to lose to: a wearer that sets
|
|
2341
|
+
* the property wins by having set it, and one that does not gets this.
|
|
2342
|
+
*/
|
|
2343
|
+
background: var(--wave-docs-panel-surface, var(--wave-docs-bg));
|
|
2344
|
+
/* The frame already clips to its own corners; this keeps a child's square
|
|
2345
|
+
* corner — a `<pre>` — from poking through the inner one. */
|
|
2346
|
+
overflow: hidden;
|
|
2347
|
+
}
|
|
2348
|
+
}
|
|
2349
|
+
|
|
2350
|
+
/* ---------------------------------------------------------------------------
|
|
2351
|
+
* Where to go next
|
|
2352
|
+
*
|
|
2353
|
+
* A question per row and the page that answers it, from a page's `next`
|
|
2354
|
+
* frontmatter. Emitted by `docs.Page` above the pager.
|
|
2355
|
+
* ------------------------------------------------------------------------ */
|
|
2356
|
+
|
|
2357
|
+
@layer components {
|
|
2358
|
+
/*
|
|
2359
|
+
* One block, not a stack of cards. The rows belong to each other — they are
|
|
2360
|
+
* the same question asked seven ways — and seven bordered boxes says seven
|
|
2361
|
+
* unrelated things. One frame with hairlines between reads as a table of
|
|
2362
|
+
* routes, which is what it is.
|
|
2363
|
+
*
|
|
2364
|
+
* The measure and the centring, both: this belongs to the prose, and the
|
|
2365
|
+
* pager below it learned that the hard way.
|
|
2366
|
+
*/
|
|
2367
|
+
.wave-docs-explore {
|
|
2368
|
+
max-width: var(--wave-docs-measure);
|
|
2369
|
+
margin-inline: auto;
|
|
2370
|
+
margin-block-start: 3rem;
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
.wave-docs-explore__list {
|
|
2374
|
+
margin: 0;
|
|
2375
|
+
padding: 0;
|
|
2376
|
+
list-style: none;
|
|
2377
|
+
}
|
|
2378
|
+
|
|
2379
|
+
/*
|
|
2380
|
+
* ⚠️ THE RULE IS A TOP BORDER ON EVERY ROW BUT THE FIRST, NOT A BOTTOM ONE ON
|
|
2381
|
+
* EVERY ROW BUT THE LAST. `:last-child` is the wrong hook here: the list is
|
|
2382
|
+
* authored, and a `:last-child` rule leaves a doubled line under the final
|
|
2383
|
+
* row the moment anything is appended to it — including by a consumer's own
|
|
2384
|
+
* CSS. `+` says "between", which is the thing being described.
|
|
2385
|
+
*/
|
|
2386
|
+
.wave-docs-explore__item {
|
|
2387
|
+
display: flex;
|
|
2388
|
+
align-items: baseline;
|
|
2389
|
+
justify-content: space-between;
|
|
2390
|
+
gap: 1rem 1.5rem;
|
|
2391
|
+
/* The inline half is the panel's, so a question lines up with the heading
|
|
2392
|
+
* above it — see `--wave-docs-panel-inset`. */
|
|
2393
|
+
padding: 1rem var(--wave-docs-panel-inset);
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2396
|
+
.wave-docs-explore__item + .wave-docs-explore__item {
|
|
2397
|
+
border-block-start: 1px solid var(--wave-docs-border);
|
|
2398
|
+
}
|
|
2399
|
+
|
|
2400
|
+
/*
|
|
2401
|
+
* ⚠️ `min-width: 0` ON THE QUESTION, WHICH IS WHAT LETS IT WRAP. A flex item
|
|
2402
|
+
* floors at its content's min-content width, so a sentence-length question
|
|
2403
|
+
* pushes the answer off the row's inline end instead of taking two lines.
|
|
2404
|
+
*/
|
|
2405
|
+
.wave-docs-explore__question {
|
|
2406
|
+
min-width: 0;
|
|
2407
|
+
color: var(--wave-docs-fg-muted);
|
|
2408
|
+
}
|
|
2409
|
+
|
|
2410
|
+
.wave-docs-explore__answer {
|
|
2411
|
+
flex: none;
|
|
2412
|
+
color: var(--wave-docs-accent);
|
|
2413
|
+
font-weight: 600;
|
|
2414
|
+
text-decoration: none;
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2417
|
+
.wave-docs-explore__answer:hover {
|
|
2418
|
+
text-decoration: underline;
|
|
2419
|
+
}
|
|
2420
|
+
|
|
2421
|
+
.wave-docs-explore__answer:focus-visible {
|
|
2422
|
+
outline: 2px solid var(--wave-docs-accent);
|
|
2423
|
+
outline-offset: 2px;
|
|
2424
|
+
border-radius: var(--wave-docs-radius-sm);
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2427
|
+
/*
|
|
2428
|
+
* ⚠️ A CONTAINER QUERY, AND THE CONTAINER IS THE SHELL — the same reasoning
|
|
2429
|
+
* as every other breakpoint in this file. Two columns of sentence-length
|
|
2430
|
+
* questions are cramped in a narrow *box*, which a host can hand us on a
|
|
2431
|
+
* 1920px monitor.
|
|
2432
|
+
*/
|
|
2433
|
+
@container wave-docs (max-width: 40rem) {
|
|
2434
|
+
.wave-docs-explore__item {
|
|
2435
|
+
flex-direction: column;
|
|
2436
|
+
align-items: start;
|
|
2437
|
+
gap: 0.25rem;
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
|
|
1891
2442
|
/* ---------------------------------------------------------------------------
|
|
1892
2443
|
* Pager
|
|
1893
2444
|
*
|
|
@@ -2721,7 +3272,17 @@
|
|
|
2721
3272
|
flex: none;
|
|
2722
3273
|
box-sizing: border-box;
|
|
2723
3274
|
width: var(--wave-docs-sidebar-width);
|
|
2724
|
-
|
|
3275
|
+
/*
|
|
3276
|
+
* ⚠️ NO PADDING AND NO OVERFLOW HERE — BOTH MOVED INTO
|
|
3277
|
+
* `.wave-docs-layout__sidebar-scroll`, AND THAT SPLIT IS THE POINT.
|
|
3278
|
+
*
|
|
3279
|
+
* This panel is now an ordinary positioned box that happens to be sticky,
|
|
3280
|
+
* so the edge shadows below resolve against it and have nothing to scroll
|
|
3281
|
+
* with. While it was the scroll container itself they had to be
|
|
3282
|
+
* `position: sticky` and cancel its padding by hand, and every version of
|
|
3283
|
+
* that fought the scroller rather than standing outside it.
|
|
3284
|
+
*/
|
|
3285
|
+
overflow: hidden;
|
|
2725
3286
|
border: 0;
|
|
2726
3287
|
/*
|
|
2727
3288
|
* Cover mode only, and it is a different thing from the divider in push
|
|
@@ -2737,13 +3298,210 @@
|
|
|
2737
3298
|
position: sticky;
|
|
2738
3299
|
top: var(--wave-docs-chrome-offset);
|
|
2739
3300
|
height: calc(100dvh - var(--wave-docs-chrome-offset));
|
|
3301
|
+
/*
|
|
3302
|
+
* ⚠️ NO `scrollbar-gutter: stable`, AND IT USED TO HAVE ONE.
|
|
3303
|
+
*
|
|
3304
|
+
* It reserved the scrollbar's width whether or not it was showing, so the
|
|
3305
|
+
* column would not jump when the nav grew past a screen. The jump it
|
|
3306
|
+
* prevents is rare and transient; what it cost was permanent and on every
|
|
3307
|
+
* page — measured at 1600px, the nav's content sat 16px from its inline
|
|
3308
|
+
* start and 31px from its end, because the reserved 15px sits *inside* the
|
|
3309
|
+
* padding. A sidebar visibly lopsided on every Windows and Linux machine,
|
|
3310
|
+
* to spare an occasional 15px shift on the same ones.
|
|
3311
|
+
*
|
|
3312
|
+
* It also does nothing where overlay scrollbars are the default — macOS —
|
|
3313
|
+
* so the asymmetry was invisible to anyone who built it.
|
|
3314
|
+
*
|
|
3315
|
+
* `thin`, so the scrollbar that does appear is narrow, and coloured rather
|
|
3316
|
+
* than left as the UA's default slab against the panel.
|
|
3317
|
+
*
|
|
3318
|
+
* ⚠️ AND IT IS HIDDEN ONLY WHERE SOMETHING REPLACES IT — see the `@supports`
|
|
3319
|
+
* block below. A bar is the one cue that a column has more below it, so
|
|
3320
|
+
* taking it away before the shadow exists trades a slab for nothing.
|
|
3321
|
+
*/
|
|
3322
|
+
scrollbar-width: thin;
|
|
3323
|
+
scrollbar-color: var(--wave-docs-border-strong) transparent;
|
|
3324
|
+
}
|
|
3325
|
+
|
|
3326
|
+
.wave-docs-layout__sidebar-scroll {
|
|
3327
|
+
height: 100%;
|
|
2740
3328
|
overflow-y: auto;
|
|
2741
3329
|
/* Without this, reaching the end of the nav keeps scrolling — the article
|
|
2742
3330
|
* jumps under the reader's cursor while they are looking at the sidebar. */
|
|
2743
3331
|
overscroll-behavior: contain;
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
scrollbar-
|
|
3332
|
+
padding: 2rem 1rem;
|
|
3333
|
+
scrollbar-width: inherit;
|
|
3334
|
+
scrollbar-color: inherit;
|
|
3335
|
+
}
|
|
3336
|
+
|
|
3337
|
+
/*
|
|
3338
|
+
* ⚠️ ABSOLUTE AGAINST THE PANEL, WHICH IS NOT THE SCROLLER — AND THAT IS THE
|
|
3339
|
+
* WHOLE REASON THIS FINALLY HOLDS STILL.
|
|
3340
|
+
*
|
|
3341
|
+
* These were `position: sticky` inside the scroll container, cancelling its
|
|
3342
|
+
* padding by hand, and they still read as moving. An absolutely positioned
|
|
3343
|
+
* child of a scroll container is laid out against that container's padding
|
|
3344
|
+
* box and joins its scrollable overflow, so nothing placed inside the
|
|
3345
|
+
* scroller is ever truly pinned to the panel; sticky only clamps it, and a
|
|
3346
|
+
* clamp is still a thing that travels until it catches.
|
|
3347
|
+
*
|
|
3348
|
+
* With the overflow moved to `.wave-docs-layout__sidebar-scroll`, the panel is
|
|
3349
|
+
* an ordinary positioned box. `top: 0` and `bottom: 0` are its own edges, and
|
|
3350
|
+
* there is no scroll for these to have a position within.
|
|
3351
|
+
*
|
|
3352
|
+
* ⚠️ AND THE SHADOW STILL BELONGS TO THE PANEL RATHER THAN THE TREE, because
|
|
3353
|
+
* `.wave-docs-sidebar` paints an opaque ground — it is in the theme's opt-in
|
|
3354
|
+
* rule, and the shell has to be one surface — so a background on the scroller
|
|
3355
|
+
* would be painted underneath it. Measured 255/255 when it was.
|
|
3356
|
+
*/
|
|
3357
|
+
.wave-docs-layout__sidebar-nav::before,
|
|
3358
|
+
.wave-docs-layout__sidebar-nav::after {
|
|
3359
|
+
content: '';
|
|
3360
|
+
position: absolute;
|
|
3361
|
+
inset-inline: 0;
|
|
3362
|
+
z-index: 1;
|
|
3363
|
+
height: 0.75rem;
|
|
3364
|
+
opacity: 0;
|
|
3365
|
+
pointer-events: none;
|
|
3366
|
+
}
|
|
3367
|
+
|
|
3368
|
+
.wave-docs-layout__sidebar-nav::before {
|
|
3369
|
+
top: 0;
|
|
3370
|
+
/*
|
|
3371
|
+
* Radial, the shape the table and the search list use: strongest against
|
|
3372
|
+
* the edge it belongs to and falling away from it, rather than a flat ramp
|
|
3373
|
+
* across the band.
|
|
3374
|
+
*/
|
|
3375
|
+
background: radial-gradient(
|
|
3376
|
+
farthest-side at 50% 0,
|
|
3377
|
+
var(--wave-docs-scroll-shadow),
|
|
3378
|
+
transparent
|
|
3379
|
+
);
|
|
3380
|
+
}
|
|
3381
|
+
|
|
3382
|
+
.wave-docs-layout__sidebar-nav::after {
|
|
3383
|
+
bottom: 0;
|
|
3384
|
+
background: radial-gradient(
|
|
3385
|
+
farthest-side at 50% 100%,
|
|
3386
|
+
var(--wave-docs-scroll-shadow),
|
|
3387
|
+
transparent
|
|
3388
|
+
);
|
|
3389
|
+
}
|
|
3390
|
+
|
|
3391
|
+
/*
|
|
3392
|
+
* ⚠️ AN INACTIVE TIMELINE IS WHAT MAKES THIS CORRECT. A scroll timeline is
|
|
3393
|
+
* inactive when its container has no scrollable overflow in the axis, and an
|
|
3394
|
+
* animation with an inactive timeline does not apply — so `opacity: 0` wins
|
|
3395
|
+
* and a nav that fits shows nothing at all. No query asks whether it scrolls;
|
|
3396
|
+
* the answer falls out of whether there is anything to scroll.
|
|
3397
|
+
*
|
|
3398
|
+
* ⚠️ `nearest`, NOT `self`: the animation is on a pseudo-element *inside* the
|
|
3399
|
+
* scroll container rather than on the container itself, and `self` would name
|
|
3400
|
+
* the pseudo — inactive at every height, which looks exactly like the feature
|
|
3401
|
+
* being unsupported.
|
|
3402
|
+
*
|
|
3403
|
+
* The scrollbar is hidden in here and nowhere else. Where this does not run —
|
|
3404
|
+
* Firefox, until scroll-driven animations ship — the thin bar above stays and
|
|
3405
|
+
* a reader keeps the cue rather than losing both.
|
|
3406
|
+
*/
|
|
3407
|
+
@supports (animation-timeline: scroll()) {
|
|
3408
|
+
.wave-docs-layout__sidebar-scroll {
|
|
3409
|
+
scrollbar-width: none;
|
|
3410
|
+
}
|
|
3411
|
+
|
|
3412
|
+
.wave-docs-layout__sidebar-scroll::-webkit-scrollbar {
|
|
3413
|
+
display: none;
|
|
3414
|
+
}
|
|
3415
|
+
|
|
3416
|
+
/*
|
|
3417
|
+
* ⚠️ A NAMED TIMELINE, AND `scroll(nearest block)` WAS A REAL BUG RATHER
|
|
3418
|
+
* THAN A TIDIER SPELLING.
|
|
3419
|
+
*
|
|
3420
|
+
* `nearest` means the nearest ancestor *scroll container*, and a box is
|
|
3421
|
+
* only one when it actually has scrollable overflow. This nav is a screen
|
|
3422
|
+
* tall and on most sites its tree fits — measured `scrollHeight -
|
|
3423
|
+
* clientHeight === 0` at 1440×900 on this very site — so `nearest` walked
|
|
3424
|
+
* straight past it and found the document, which always scrolls.
|
|
3425
|
+
*
|
|
3426
|
+
* The result was the shadow keyed to the *page*: it faded in as the reader
|
|
3427
|
+
* scrolled the article, on a navigation with nothing hidden, while the
|
|
3428
|
+
* sticky panel was still travelling to its pinned position. Appearing and
|
|
3429
|
+
* moving at once, which is precisely what it looked like.
|
|
3430
|
+
*
|
|
3431
|
+
* Named, it can only ever be this element's own scroll. And the
|
|
3432
|
+
* inactive-timeline rule then does the right thing for free: a nav that
|
|
3433
|
+
* fits declares a timeline with no scroll range, the animation does not
|
|
3434
|
+
* apply, and `opacity: 0` wins.
|
|
3435
|
+
*/
|
|
3436
|
+
/*
|
|
3437
|
+
* ⚠️ THE TIMELINE IS DECLARED ON THE SCROLLER AND *SCOPED* UP TO THE PANEL,
|
|
3438
|
+
* because the two are different elements now. A `scroll-timeline` name is
|
|
3439
|
+
* visible to the declaring element's descendants, and these pseudo-elements
|
|
3440
|
+
* belong to its parent — so without `timeline-scope` they would not find it
|
|
3441
|
+
* and would sit at `opacity: 0` for ever, looking exactly like the feature
|
|
3442
|
+
* being unsupported.
|
|
3443
|
+
*/
|
|
3444
|
+
.wave-docs-layout__sidebar-nav {
|
|
3445
|
+
timeline-scope: --wave-docs-sidebar-nav-scroll;
|
|
3446
|
+
}
|
|
3447
|
+
|
|
3448
|
+
.wave-docs-layout__sidebar-scroll {
|
|
3449
|
+
scroll-timeline: --wave-docs-sidebar-nav-scroll block;
|
|
3450
|
+
}
|
|
3451
|
+
|
|
3452
|
+
.wave-docs-layout__sidebar-nav::before,
|
|
3453
|
+
.wave-docs-layout__sidebar-nav::after {
|
|
3454
|
+
animation-timeline: --wave-docs-sidebar-nav-scroll;
|
|
3455
|
+
animation-timing-function: linear;
|
|
3456
|
+
animation-fill-mode: both;
|
|
3457
|
+
}
|
|
3458
|
+
|
|
3459
|
+
/*
|
|
3460
|
+
* ⚠️ THE RANGE IS A LENGTH, NOT THE WHOLE SCROLL — AND THAT IS WHAT STOPS
|
|
3461
|
+
* THIS RAMPING WHILE THE READER SCROLLS.
|
|
3462
|
+
*
|
|
3463
|
+
* `wave-docs-scroll-shadow-start` shapes its fade in *percentages*, which
|
|
3464
|
+
* are percentages of the container's total scroll range. That is fine on a
|
|
3465
|
+
* table, which scrolls a few hundred pixels; on a navigation it is not. A
|
|
3466
|
+
* nav with 1000px of scroll turns the same `2%…8%` into 20px…80px, so the
|
|
3467
|
+
* shadow spends eighty pixels of scrolling getting to full strength —
|
|
3468
|
+
* arriving gradually, which reads exactly like it is moving with the
|
|
3469
|
+
* content rather than pinned to the edge.
|
|
3470
|
+
*
|
|
3471
|
+
* A fixed `1rem` makes it the same short distance whatever the nav's
|
|
3472
|
+
* height: present almost at once, and identical on a six-page site and a
|
|
3473
|
+
* three-hundred-page one. The keyframes are plain fades because the shaping
|
|
3474
|
+
* now lives in the range.
|
|
3475
|
+
*/
|
|
3476
|
+
.wave-docs-layout__sidebar-nav::before {
|
|
3477
|
+
animation-name: wave-docs-scroll-shadow-appear;
|
|
3478
|
+
animation-range: 0 1rem;
|
|
3479
|
+
}
|
|
3480
|
+
|
|
3481
|
+
.wave-docs-layout__sidebar-nav::after {
|
|
3482
|
+
animation-name: wave-docs-scroll-shadow-vanish;
|
|
3483
|
+
animation-range: calc(100% - 1rem) 100%;
|
|
3484
|
+
}
|
|
3485
|
+
}
|
|
3486
|
+
|
|
3487
|
+
/* Absent at the start edge, present as soon as it is not. */
|
|
3488
|
+
@keyframes wave-docs-scroll-shadow-appear {
|
|
3489
|
+
from {
|
|
3490
|
+
opacity: 0;
|
|
3491
|
+
}
|
|
3492
|
+
to {
|
|
3493
|
+
opacity: 1;
|
|
3494
|
+
}
|
|
3495
|
+
}
|
|
3496
|
+
|
|
3497
|
+
/* Present until the end edge is reached. */
|
|
3498
|
+
@keyframes wave-docs-scroll-shadow-vanish {
|
|
3499
|
+
from {
|
|
3500
|
+
opacity: 1;
|
|
3501
|
+
}
|
|
3502
|
+
to {
|
|
3503
|
+
opacity: 0;
|
|
3504
|
+
}
|
|
2747
3505
|
}
|
|
2748
3506
|
|
|
2749
3507
|
/* First child of the navigation, above the tree. */
|
|
@@ -3355,34 +4113,87 @@
|
|
|
3355
4113
|
box-shadow: 0 24px 48px -12px var(--wave-docs-shadow);
|
|
3356
4114
|
}
|
|
3357
4115
|
|
|
4116
|
+
/*
|
|
4117
|
+
* ⚠️ A FIELD SET INTO THE DIALOG, AND IT WAS A FULL-BLEED BAND WITH TWO
|
|
4118
|
+
* ROUNDED CORNERS AND A RULE UNDER IT.
|
|
4119
|
+
*
|
|
4120
|
+
* Flush to the frame it could only round the corners it shared with the
|
|
4121
|
+
* dialog, so it read as part of the chrome rather than as the one control in
|
|
4122
|
+
* it. Inset by the same margin the results list uses, it is a box like they
|
|
4123
|
+
* are, and the rule under it is redundant — the gap does that work.
|
|
4124
|
+
*
|
|
4125
|
+
* ⚠️ AND THE `- 1px` IS THIS BOX'S OWN BORDER, WHICH IS WHAT PUTS ITS GLYPH
|
|
4126
|
+
* ON THE SAME COLUMN AS EVERYTHING ELSE. Content inside a bordered box starts
|
|
4127
|
+
* a border further in than content inside an unbordered one, so equal padding
|
|
4128
|
+
* misses by exactly that. The same subtraction `--wave-docs-panel-inset`
|
|
4129
|
+
* documents, for the same reason.
|
|
4130
|
+
*/
|
|
3358
4131
|
.wave-docs-search-input-row {
|
|
3359
4132
|
display: flex;
|
|
3360
4133
|
align-items: center;
|
|
3361
4134
|
gap: 0.5rem;
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
*
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
4135
|
+
margin: 0.375rem;
|
|
4136
|
+
/*
|
|
4137
|
+
* ⚠️ THE TRIGGER, EXPANDED — SAME BORDER, SAME FILL, SAME RADIUS.
|
|
4138
|
+
*
|
|
4139
|
+
* A reader clicks a bordered, filled control and lands in this dialog, so
|
|
4140
|
+
* the thing their cursor is now in should be the same object. It was a
|
|
4141
|
+
* border with no fill (two frames a few pixels apart), then a fill with no
|
|
4142
|
+
* border (a tinted band, not a control); the trigger has always been both,
|
|
4143
|
+
* and both together are what reads as a field.
|
|
4144
|
+
*
|
|
4145
|
+
* ⚠️ THE PADDING IS NOT COPIED WITH THEM. The trigger pays
|
|
4146
|
+
* `calc(0.5rem - 1px)` because it is a compact control in a sidebar; this
|
|
4147
|
+
* pays `calc(0.75rem - 1px)` because its glyph has to land on the column
|
|
4148
|
+
* the results and the footer sit on. The `- 1px` is the border either way:
|
|
4149
|
+
* content inside a bordered box starts a border further in.
|
|
4150
|
+
*
|
|
4151
|
+
* ⚠️ AND THE RADIUS IS THE TRIGGER'S TIER, NOT THE ROWS'. Both are controls
|
|
4152
|
+
* on the base tier; a result row is a list item on `-sm`. So the field is
|
|
4153
|
+
* 16px and the rows below it are 12px, which is the tier system doing what
|
|
4154
|
+
* it says rather than two boxes disagreeing.
|
|
4155
|
+
*/
|
|
4156
|
+
padding: 0.5rem calc(0.75rem - 1px);
|
|
4157
|
+
border: 1px solid var(--wave-docs-border);
|
|
4158
|
+
border-radius: var(--wave-docs-radius);
|
|
4159
|
+
background: var(--wave-docs-bg-subtle);
|
|
4160
|
+
}
|
|
4161
|
+
|
|
4162
|
+
/*
|
|
4163
|
+
* ⚠️ THE FIELD DARKENS ITS EDGE ON FOCUS — IT DOES NOT DRAW AN ACCENT RING,
|
|
4164
|
+
* AND IT CANNOT, BECAUSE IT COULD NEVER PUT ONE DOWN.
|
|
4165
|
+
*
|
|
4166
|
+
* A text input matches `:focus-visible` whenever it is focused, however
|
|
4167
|
+
* focus arrived — that is the spec, not a heuristic — and this dialog focuses
|
|
4168
|
+
* its input the moment it opens. So a 2px accent ring here is not a state, it
|
|
4169
|
+
* is the field's permanent appearance, and the field stops looking like the
|
|
4170
|
+
* bordered grey control the reader clicked to get here.
|
|
4171
|
+
*
|
|
4172
|
+
* ⚠️ AND THE INDICATOR IS NOT LOST WITH IT, BECAUSE A TEXT FIELD HAS ONE OF
|
|
4173
|
+
* ITS OWN. The caret is the platform's focus indication for a text box, it is
|
|
4174
|
+
* in this field the whole time the dialog is open, and it is the thing a
|
|
4175
|
+
* reader looks for when they want to know where typing goes. That is a
|
|
4176
|
+
* different argument from the one this rule used to reject — "the dialog
|
|
4177
|
+
* frame is the indicator" was a *static* border that looked identical focused
|
|
4178
|
+
* and unfocused, indicating nothing.
|
|
4179
|
+
*
|
|
4180
|
+
* The edge still changes, to the same `border-strong` the trigger takes under
|
|
4181
|
+
* the pointer, so the field is the trigger in both of its states.
|
|
4182
|
+
*/
|
|
3377
4183
|
.wave-docs-search-input-row:has(.wave-docs-search-input:focus-visible) {
|
|
3378
|
-
|
|
3379
|
-
outline-offset: -2px;
|
|
4184
|
+
border-color: var(--wave-docs-border-strong);
|
|
3380
4185
|
}
|
|
3381
4186
|
|
|
3382
4187
|
.wave-docs-search-input {
|
|
3383
4188
|
flex: 1 1 auto;
|
|
3384
4189
|
min-width: 0;
|
|
3385
|
-
|
|
4190
|
+
/*
|
|
4191
|
+
* ⚠️ NO INLINE PADDING, BECAUSE THIS BOX'S EDGE IS A COLUMN THE RESULTS
|
|
4192
|
+
* MEASURE TO. Four pixels here put the typed text four pixels right of
|
|
4193
|
+
* every result heading below it, which is the sort of misalignment that
|
|
4194
|
+
* looks like a bug in the list rather than a padding on the field.
|
|
4195
|
+
*/
|
|
4196
|
+
padding: 0.375rem 0;
|
|
3386
4197
|
border: 0;
|
|
3387
4198
|
background: none;
|
|
3388
4199
|
color: var(--wave-docs-fg);
|
|
@@ -3415,7 +4226,18 @@
|
|
|
3415
4226
|
display: flex;
|
|
3416
4227
|
align-items: center;
|
|
3417
4228
|
gap: 1rem;
|
|
3418
|
-
|
|
4229
|
+
/*
|
|
4230
|
+
* ⚠️ THE COLUMN EVERY OTHER ROW IN THIS DIALOG LINES UP ON, AND IT IS THE
|
|
4231
|
+
* ANCHOR RATHER THAN A FOLLOWER. The key caps are the one row whose left
|
|
4232
|
+
* edge is a drawn object, so the input's glyph and the results' text are
|
|
4233
|
+
* measured to it: `0.375rem` of inset plus `0.75rem` of padding is the same
|
|
4234
|
+
* 18px this pays in one go.
|
|
4235
|
+
*
|
|
4236
|
+
* Boxes, not ink. A cap's arrow sits its own border and `0.4em` of padding
|
|
4237
|
+
* inside the cap, so aligning the *glyphs* would put every other row on a
|
|
4238
|
+
* column that moves whenever the footer's font size does.
|
|
4239
|
+
*/
|
|
4240
|
+
padding: 0.5rem 1.125rem;
|
|
3419
4241
|
border-block-start: 1px solid var(--wave-docs-border);
|
|
3420
4242
|
color: var(--wave-docs-fg-subtle);
|
|
3421
4243
|
font-size: 0.75rem;
|
|
@@ -3501,7 +4323,75 @@
|
|
|
3501
4323
|
.wave-docs-search-results {
|
|
3502
4324
|
flex: 1 1 auto;
|
|
3503
4325
|
overflow-y: auto;
|
|
3504
|
-
|
|
4326
|
+
/*
|
|
4327
|
+
* ⚠️ THE TABLE'S SCROLL SHADOW, TURNED ON ITS SIDE — AND IT REPLACED A
|
|
4328
|
+
* MASK, WHICH WAS THE WRONG TOOL TWICE OVER.
|
|
4329
|
+
*
|
|
4330
|
+
* A mask fades content to *transparent*, so what showed through was the
|
|
4331
|
+
* dialog's own white: a hole rather than a shadow. And it cannot be
|
|
4332
|
+
* conditional — CSS has no way to ask whether there is anything above to
|
|
4333
|
+
* scroll to — so the first and last rows were softened even at rest.
|
|
4334
|
+
*
|
|
4335
|
+
* Four gradients, exactly as `.wave-docs-table-scroll` does it. The two
|
|
4336
|
+
* `local` covers are painted in the surface colour and travel *with* the
|
|
4337
|
+
* rows, so each sits over its shadow only while that edge is at rest; the
|
|
4338
|
+
* two `scroll` shadows are pinned to the box. A grey edge appears on
|
|
4339
|
+
* precisely the side that has rows off-screen, with no listener, no state
|
|
4340
|
+
* and no hydration.
|
|
4341
|
+
*/
|
|
4342
|
+
background:
|
|
4343
|
+
linear-gradient(to bottom, var(--wave-docs-bg) 30%, transparent) top /
|
|
4344
|
+
100% 2rem no-repeat local,
|
|
4345
|
+
linear-gradient(to top, var(--wave-docs-bg) 30%, transparent) bottom /
|
|
4346
|
+
100% 2rem no-repeat local,
|
|
4347
|
+
radial-gradient(
|
|
4348
|
+
farthest-side at 50% 0,
|
|
4349
|
+
var(--wave-docs-scroll-shadow),
|
|
4350
|
+
transparent
|
|
4351
|
+
)
|
|
4352
|
+
top / 100% 0.75rem no-repeat scroll,
|
|
4353
|
+
radial-gradient(
|
|
4354
|
+
farthest-side at 50% 100%,
|
|
4355
|
+
var(--wave-docs-scroll-shadow),
|
|
4356
|
+
transparent
|
|
4357
|
+
)
|
|
4358
|
+
bottom / 100% 0.75rem no-repeat scroll;
|
|
4359
|
+
/*
|
|
4360
|
+
* ⚠️ NO PADDING ON TOP, BECAUSE THE FIELD'S MARGIN IS ALREADY THAT GAP.
|
|
4361
|
+
* Both paid it, so the space between the input and the first result was
|
|
4362
|
+
* twice the space between the input and the top of the dialog — a
|
|
4363
|
+
* doubled gap in the one place a reader's eye travels on every keystroke.
|
|
4364
|
+
*
|
|
4365
|
+
* Kept on the *bottom*, where nothing else pays it, and on the sides,
|
|
4366
|
+
* where it is what lines a row's box up with the field's.
|
|
4367
|
+
*
|
|
4368
|
+
* ⚠️ AND IT IS THE FIELD THAT KEEPS ITS MARGIN RATHER THAN THIS THAT KEEPS
|
|
4369
|
+
* ITS PADDING. An empty result list is `display: none`, so a gap paid from
|
|
4370
|
+
* here would vanish on the query that matches nothing and leave the field
|
|
4371
|
+
* sitting on the footer's rule.
|
|
4372
|
+
*/
|
|
4373
|
+
padding: 0 0.375rem 0.375rem;
|
|
4374
|
+
/*
|
|
4375
|
+
* ⚠️ NO SCROLLBAR, AND NO CSS WAY TO SHOW ONE ONLY WHILE SCROLLING.
|
|
4376
|
+
*
|
|
4377
|
+
* That behaviour is the platform's: macOS draws overlay scrollbars that
|
|
4378
|
+
* fade in on scroll and out after it, and this list gets it for free there.
|
|
4379
|
+
* Windows and Linux draw a classic one that is always present, and nothing
|
|
4380
|
+
* in CSS can make it come and go — the only ways are a JavaScript timer
|
|
4381
|
+
* that toggles a class, or a scrollbar drawn from scratch in script, which
|
|
4382
|
+
* is what a `ScrollArea` component is. Neither belongs in a package whose
|
|
4383
|
+
* whole argument is what it does *not* ship to a reader.
|
|
4384
|
+
*
|
|
4385
|
+
* So it is hidden, which is what `@waveso/app` does with `.scrollbar-none`.
|
|
4386
|
+
* What replaces it here is not a fade but the keyboard: the footer says
|
|
4387
|
+
* `↑ ↓ Select`, the list is driven by `aria-activedescendant`, and arrowing
|
|
4388
|
+
* past the last visible row scrolls it and loads the next page.
|
|
4389
|
+
*/
|
|
4390
|
+
scrollbar-width: none;
|
|
4391
|
+
}
|
|
4392
|
+
|
|
4393
|
+
.wave-docs-search-results::-webkit-scrollbar {
|
|
4394
|
+
display: none;
|
|
3505
4395
|
}
|
|
3506
4396
|
|
|
3507
4397
|
/*
|
|
@@ -3518,29 +4408,104 @@
|
|
|
3518
4408
|
border-radius: var(--wave-docs-radius-sm);
|
|
3519
4409
|
}
|
|
3520
4410
|
|
|
3521
|
-
/*
|
|
3522
|
-
*
|
|
3523
|
-
*
|
|
3524
|
-
*
|
|
3525
|
-
*
|
|
3526
|
-
*
|
|
3527
|
-
*
|
|
4411
|
+
/*
|
|
4412
|
+
* The active row, driven by `aria-selected` through the class the dialog
|
|
4413
|
+
* sets — so the visual state and the announced state cannot drift apart.
|
|
4414
|
+
* Pointer and keyboard land on the same state: moving over a row activates
|
|
4415
|
+
* it, so hovering *is* this.
|
|
4416
|
+
*
|
|
4417
|
+
* ⚠️ A TINT AND AN INK, AND NO EDGE IN ANY STATE. It was a 2px accent ring,
|
|
4418
|
+
* and briefly the trigger's border pair. The ring read as a component
|
|
4419
|
+
* borrowed from somewhere else and one that comes and goes as a reader
|
|
4420
|
+
* arrows is worse than one that never moves; bordering *every* row to make
|
|
4421
|
+
* the active one's edge legible turned a list into a stack of cards. The
|
|
4422
|
+
* field above wears the trigger's border because the field is a control; a
|
|
4423
|
+
* row is a list item, and its state is a colour.
|
|
4424
|
+
*
|
|
4425
|
+
* ⚠️ THE INK IS NOT DECORATION — IT IS WHAT KEEPS THE STATE PERCEIVABLE. A
|
|
4426
|
+
* tint alone is 1.12:1 light and 1.19:1 dark, under the 3:1 WCAG 1.4.11 asks
|
|
4427
|
+
* of a state indicator. `accent` on `accent-subtle` is 4.60:1 / 6.30:1, which
|
|
4428
|
+
* is text contrast rather than non-text and is the same pair the sidebar's
|
|
4429
|
+
* current-page row has always shipped. What must not happen is the tint going
|
|
4430
|
+
* back to carrying this alone.
|
|
4431
|
+
*/
|
|
3528
4432
|
.wave-docs-search-result-active {
|
|
3529
4433
|
background: var(--wave-docs-accent-subtle);
|
|
3530
|
-
outline: 2px solid var(--wave-docs-accent);
|
|
3531
|
-
outline-offset: -2px;
|
|
3532
4434
|
}
|
|
3533
4435
|
|
|
4436
|
+
.wave-docs-search-result-active .wave-docs-search-result-heading,
|
|
4437
|
+
.wave-docs-search-result-active .wave-docs-search-result-icon {
|
|
4438
|
+
color: var(--wave-docs-accent);
|
|
4439
|
+
}
|
|
4440
|
+
|
|
4441
|
+
/*
|
|
4442
|
+
* ⚠️ A ROW OF TWO COLUMNS, AND IT WAS ONE STACKED COLUMN.
|
|
4443
|
+
*
|
|
4444
|
+
* The dialog reads as one grid only if the input and the results agree about
|
|
4445
|
+
* *both* their columns. With no icon, a result's heading sat on the same
|
|
4446
|
+
* column as the input's magnifier while the input's own text sat 24px right
|
|
4447
|
+
* of it — three left edges where a reader expects two, which is what made the
|
|
4448
|
+
* dialog look like stacked levels rather than a list under a field.
|
|
4449
|
+
*
|
|
4450
|
+
* The icon takes the magnifier's column and the text takes the input's, so
|
|
4451
|
+
* the two rows are the same shape and there are exactly two edges.
|
|
4452
|
+
*/
|
|
3534
4453
|
.wave-docs-search-result-link {
|
|
3535
4454
|
display: flex;
|
|
3536
|
-
|
|
3537
|
-
gap
|
|
3538
|
-
|
|
4455
|
+
align-items: flex-start;
|
|
4456
|
+
/* The input's own gap between its glyph and its text. */
|
|
4457
|
+
gap: 0.5rem;
|
|
4458
|
+
/* The list pays `0.375rem` around this, so `0.75rem` here lands the icon
|
|
4459
|
+
* on the dialog's 18px column. See the footer, which is the anchor. */
|
|
4460
|
+
padding: 0.5rem 0.75rem;
|
|
3539
4461
|
border-radius: var(--wave-docs-radius-sm);
|
|
3540
4462
|
color: var(--wave-docs-fg);
|
|
3541
4463
|
text-decoration: none;
|
|
3542
4464
|
}
|
|
3543
4465
|
|
|
4466
|
+
.wave-docs-search-result-icon {
|
|
4467
|
+
flex: none;
|
|
4468
|
+
/*
|
|
4469
|
+
* ⚠️ NUDGED DOWN, NOT CENTRED. `align-items: center` would centre this on
|
|
4470
|
+
* the *two-line* block, so it would sit between the heading and the route
|
|
4471
|
+
* rather than beside the heading — and it would move when a route wrapped.
|
|
4472
|
+
* A 1rem glyph on a 0.9375rem/1.4 line is 5px shorter than the line box, so
|
|
4473
|
+
* half of that puts it on the heading's optical centre and keeps it there.
|
|
4474
|
+
*/
|
|
4475
|
+
margin-block-start: 0.15625rem;
|
|
4476
|
+
color: var(--wave-docs-fg-subtle);
|
|
4477
|
+
}
|
|
4478
|
+
|
|
4479
|
+
.wave-docs-search-result-text {
|
|
4480
|
+
display: flex;
|
|
4481
|
+
flex-direction: column;
|
|
4482
|
+
gap: 0.125rem;
|
|
4483
|
+
/* So a long heading ellipsises inside the row rather than widening it. */
|
|
4484
|
+
min-width: 0;
|
|
4485
|
+
}
|
|
4486
|
+
|
|
4487
|
+
/*
|
|
4488
|
+
* The separator between two route segments — the package's own chevron
|
|
4489
|
+
* rather than a character, so there is no font for it to be missing from.
|
|
4490
|
+
*
|
|
4491
|
+
* The `viewBox` is cropped to the painted ink, so the box is the glyph: it
|
|
4492
|
+
* sits on the baseline the way a letter does, with no alignment to correct.
|
|
4493
|
+
*/
|
|
4494
|
+
.wave-docs-search-result-sep {
|
|
4495
|
+
height: 7px;
|
|
4496
|
+
width: auto;
|
|
4497
|
+
/*
|
|
4498
|
+
* The `viewBox` is exactly the stroke's extent, so the tip lands on the
|
|
4499
|
+
* box's own edge and an SVG clips at that edge by default — the point came
|
|
4500
|
+
* out flat. Nothing here overlaps: what spills is a fraction of a pixel of
|
|
4501
|
+
* anti-aliasing.
|
|
4502
|
+
*/
|
|
4503
|
+
overflow: visible;
|
|
4504
|
+
/* The air either side. */
|
|
4505
|
+
margin-inline: 4px;
|
|
4506
|
+
color: var(--wave-docs-fg-subtle);
|
|
4507
|
+
}
|
|
4508
|
+
|
|
3544
4509
|
.wave-docs-search-result-heading {
|
|
3545
4510
|
font-size: 0.9375rem;
|
|
3546
4511
|
font-weight: 600;
|
|
@@ -3575,10 +4540,21 @@
|
|
|
3575
4540
|
white-space: nowrap;
|
|
3576
4541
|
text-overflow: ellipsis;
|
|
3577
4542
|
color: var(--wave-docs-fg-subtle);
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
*
|
|
3581
|
-
|
|
4543
|
+
/*
|
|
4544
|
+
* The package's sans, the same face as everything else in the dialog.
|
|
4545
|
+
*
|
|
4546
|
+
* ⚠️ IT WAS MONOSPACE, AND THAT IS WHAT MADE THE SEPARATOR UNRELIABLE. A
|
|
4547
|
+
* mono face advances every glyph one cell, so the trail sat on a rigid
|
|
4548
|
+
* grid an icon could not join, and its metrics are whatever the machine
|
|
4549
|
+
* resolves — SF Mono here, Consolas on Windows, Liberation Mono on Linux —
|
|
4550
|
+
* so the cap height the chevron is sized against moved from reader to
|
|
4551
|
+
* reader. Set in the sans, the line has ordinary proportional spacing and
|
|
4552
|
+
* one set of metrics to match.
|
|
4553
|
+
*/
|
|
4554
|
+
font-family: var(--wave-docs-font-sans);
|
|
4555
|
+
/* A notch up from the mono's `0.6875rem`: a monospace face reads larger at
|
|
4556
|
+
* the same size, so holding the number would have shrunk this line. */
|
|
4557
|
+
font-size: 0.75rem;
|
|
3582
4558
|
line-height: 1.5;
|
|
3583
4559
|
}
|
|
3584
4560
|
|