tosijs-ui 1.12.2 → 1.12.3

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 CHANGED
@@ -1,5 +1,94 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.12.3
4
+
5
+ ### Back now has layout coverage, because a reader noticed before the tests did
6
+
7
+ Reported from the keyboard as "sometimes the full-screen page and the full-screen page with the
8
+ nav open look like different history entries". Measured, it is neither history nor
9
+ inconsistent: the nav toggle creates **no** history entry (`history.length` unchanged across
10
+ repeated toggles, URL unchanged). What happens is that **Back re-asserts the page's declared
11
+ layout** and discards the override — so leaving a page with the nav open and returning finds it
12
+ closed.
13
+
14
+ That is defensible and it is what ships: a full-screen page is full-screen whenever you arrive
15
+ at it. Whether Back should instead restore what you left is now an open decision in `TODO.md`,
16
+ with the measurements attached, rather than whatever fell out of the implementation.
17
+
18
+ The gap the report exposed was in the tests, not the feature: `doc-system.pw.ts` already covered
19
+ Back for URL, title and content, but nothing checked its effect on **layout**, and every layout
20
+ test reached its pages by clicking links. Both directions are covered now — and labelled by what
21
+ they actually do, since one is a genuine regression test (dropping `removeAttribute('data-layout')`
22
+ on navigation fails it) and the other is a contract tripwire that survives every mutation tried
23
+ against it. A test that cannot fail is usually decoration; that one is deliberate, and saying so
24
+ is the difference.
25
+
26
+ ### The dev server sends `Cache-Control: no-store` on everything
27
+
28
+ It previously sent no `Cache-Control` at all, and that is not neutral: a browser may invent a
29
+ freshness lifetime when you decline to state one, and Safari is the most willing to. So the dev
30
+ server would rebuild correctly while the browser kept serving the previous build — which
31
+ presents as "my fix did not work", and is only escaped by emptying the cache by hand. It cost
32
+ exactly that here: a bug was reported against code that had already been fixed.
33
+
34
+ All three of the file-server's exits are covered — injected HTML, compressed assets, and
35
+ binaries that stream untouched — because they are separate responses and adding the header to
36
+ the obvious one is precisely how the other two get missed. Each is independently
37
+ mutation-verified.
38
+
39
+ **Dev server only.** The built site in `docs/` is unaffected; it is a static site for a real
40
+ host, and telling a CDN never to store it would be actively wrong.
41
+
42
+ ### `layout: "full-screen"` — and `<tosi-sidenav alwaysCompact>` underneath it
43
+
44
+ `full-width` shipped in 1.12.2 and `full-screen` did not, because it needed something from the
45
+ sidenav that did not exist. It exists now, and it is one line rather than a new layout.
46
+
47
+ **`<tosi-sidenav>` gained `navVisible`** — the one control a "show me the navigation" button
48
+ needs. Read it, flip it:
49
+
50
+ ```js
51
+ sidenav.navVisible = !sidenav.navVisible
52
+ ```
53
+
54
+ It resolves what hiding the nav _means_ at the current width — forcing compact on a wide screen,
55
+ simply showing the content on a narrow one — so no caller has to know, and a narrow-screen
56
+ request does not outstay itself when the window is widened. `layout: "full-screen"` uses the
57
+ same property, so the feature is general rather than something the doc-browser knows a trick
58
+ about.
59
+
60
+ **And `alwaysCompact` underneath it.** Compact mode already shows the nav _or_ the content
61
+ and takes turns between them — which is precisely what a full-screen page wants — so this just
62
+ removes the width test rather than adding a second way to lay out. It is a named state and not a
63
+ `minSize` no viewport can reach, because the second one works and reads as a bug to the next
64
+ person.
65
+
66
+ **`layout: "full-screen"`** then means: no reading measure, no nav, no gutter, and a content area
67
+ with a real height — so a demo, a canvas or an embedded app can ask for `height: 100%` and get
68
+ it. There is a [demo page](/full-screen-demo/), and the navigation button works there: it
69
+ returns you to the normal layout rather than replacing the content with a full-screen nav, and
70
+ it **stays offered** so you can go back. Riding `compact` directly, as it used to, meant the
71
+ button vanished on its own click and stranded you in the normal layout.
72
+
73
+ Two inline styles were quietly breaking the promise. `.doc-content` receives both its
74
+ `max-width` and its `padding` inline from the doc-browser, so no stylesheet could override
75
+ either — a full-screen page rendered inset by 32px, in a box that collapsed to its content
76
+ height instead of filling. `padding` now routes through `--doc-content-padding`, the way the
77
+ measure already did.
78
+
79
+ Navigating away restores the nav, and a reader's override lasts until they navigate rather than
80
+ beyond it — **including Back**, so a full-screen page is full-screen whenever you arrive at it.
81
+ That is predictable, but it does mean leaving a page with the nav open and returning finds it
82
+ closed; whether Back should instead restore what you left is noted in `TODO.md` as a decision
83
+ rather than left to whatever fell out of the implementation. Both directions are mutation-verified, which caught a test that _looked_ like it
84
+ covered the second one and did not: it routed through a prose page, which reset the state as a
85
+ side effect.
86
+
87
+ Before hydration there is no sidenav at all, just static markup, so a stylesheet rule covers the
88
+ first paint. Those rules select `.doc-nav.doc-nav` — the class deliberately repeated — because
89
+ the pre-hydration layout further down the same sheet has identical specificity, and winning on
90
+ source order works right up until someone reorders the spec.
91
+
3
92
  ## 1.12.2
4
93
 
5
94
  ### `haltijaDev: 'tunnel'` — let an agent drive a page running in a headset (#104)
@@ -22,8 +22,8 @@ export interface Doc {
22
22
  path: string;
23
23
  pin?: string;
24
24
  hidden?: boolean;
25
- /** `'full-width'` drops the reading measure. See `Doc.layout` in docs.ts. */
26
- layout?: 'full-width';
25
+ /** `'full-width'` drops the reading measure; `'full-screen'` also puts the nav away. */
26
+ layout?: 'full-width' | 'full-screen';
27
27
  testStatus?: 'passed' | 'failed' | 'pending';
28
28
  bakes?: Array<[string, {
29
29
  dialect: string;
@@ -422,6 +422,16 @@ export function createDocBrowser(options) {
422
422
  docs,
423
423
  currentDoc,
424
424
  compact: false,
425
+ /*
426
+ Whether to OFFER the navigation button — deliberately not the same thing as `compact`.
427
+
428
+ The button used to ride `compact` directly, which is right for a responsive page: wide
429
+ enough to show the nav beside the content, and there is nothing to toggle. But opening the
430
+ nav on a full-screen page leaves compact mode by design, and the button would vanish with
431
+ it — stranding the reader in the normal layout with no way back to full screen. So it is
432
+ offered whenever the layout is compact OR the page asked to be full-screen.
433
+ */
434
+ navToggle: false,
425
435
  },
426
436
  });
427
437
  const app = state[stateKey];
@@ -576,13 +586,58 @@ export function createDocBrowser(options) {
576
586
  // sidenav')`, which returns the FIRST sidenav in the document (an OUTER instance
577
587
  // when this one is an embedded, memory-routed browser).
578
588
  let sidenav = null;
589
+ /*
590
+ `full-screen` uses the sidenav's OWN compact behaviour rather than a new state.
591
+
592
+ `<tosi-sidenav>` already does exactly what full-screen wants: compact mode shows the nav OR the
593
+ content, with `contentVisible` picking which. `alwaysCompact` keeps it in that mode at any
594
+ width, and `contentVisible` gives the page the whole box.
595
+
596
+ Remembered rather than applied directly, because the sidenav is not there yet when the first
597
+ doc renders — `sidenav` is captured from its own change event, which fires on a state CHANGE
598
+ and so may never fire at all on a normal-width page.
599
+ */
600
+ let wantsFullScreen = false;
601
+ /*
602
+ What we last PUSHED onto the sidenav, so the user's own toggle is not overwritten.
603
+
604
+ The first version applied the layout on every call, and `applyFullScreen` is called from the
605
+ sidenav's change handler — so clicking "navigation" flipped `contentVisible`, the sidenav
606
+ changed state, the handler fired, and the layout was re-applied on top. The nav button looked
607
+ live and did nothing. Applying only on TRANSITION means a full-screen page sets itself up once
608
+ and then leaves the reader alone.
609
+ */
610
+ let appliedFullScreen = null;
611
+ /** Offer the button when there is something to toggle: a compact layout, or a full-screen page. */
612
+ const refreshNavToggle = () => {
613
+ app.navToggle = (Boolean(sidenav?.compact) || wantsFullScreen);
614
+ };
615
+ const applyFullScreen = () => {
616
+ /*
617
+ Resolved from THIS browser's own container when the change event has not introduced it yet.
618
+ Scoped rather than `document.querySelector('tosi-sidenav')`, which returns the first in the
619
+ document — for a nested doc-browser, the HOST's.
620
+ */
621
+ if (!sidenav && typeof container !== 'undefined' && container) {
622
+ sidenav = container.querySelector(TosiSidenav.tagName);
623
+ }
624
+ if (!sidenav)
625
+ return;
626
+ if (appliedFullScreen === wantsFullScreen)
627
+ return;
628
+ // The same one control the nav button uses — the layout is just asking for the nav to be
629
+ // away, and the sidenav decides how that is achieved.
630
+ sidenav.navVisible = !wantsFullScreen;
631
+ appliedFullScreen = wantsFullScreen;
632
+ refreshNavToggle();
633
+ };
579
634
  const headerContent = [
580
635
  button({
581
636
  class: 'iconic',
582
637
  style: { color: vars.linkColor },
583
638
  title: 'navigation',
584
639
  bind: {
585
- value: app.compact,
640
+ value: app.navToggle,
586
641
  binding: {
587
642
  toDOM(element, compact) {
588
643
  element.style.display = compact ? '' : 'none';
@@ -593,8 +648,10 @@ export function createDocBrowser(options) {
593
648
  },
594
649
  },
595
650
  onClick() {
651
+ // Read it, flip it. `<tosi-sidenav>` resolves what that means at this width; a
652
+ // button should not have to know, and this one used to.
596
653
  if (sidenav)
597
- sidenav.contentVisible = !sidenav.contentVisible;
654
+ sidenav.navVisible = !sidenav.navVisible;
598
655
  },
599
656
  }, icons.menu()),
600
657
  span({ style: { flex: '0 0 10px' } }),
@@ -651,7 +708,13 @@ export function createDocBrowser(options) {
651
708
  // doc-browser (no doc-system stylesheet) working.
652
709
  maxWidth: 'var(--doc-content-max-width, 44em)',
653
710
  margin: 'auto',
654
- padding: '0 1em',
711
+ /*
712
+ Through a variable, for the same reason `maxWidth` is: this is an INLINE style, so a
713
+ stylesheet cannot override it, and `layout: "full-screen"` needs to. A full-screen page that
714
+ keeps a 1em gutter is not full screen — it is a demo inset by 32px, which is exactly what it
715
+ looked like.
716
+ */
717
+ padding: 'var(--doc-content-padding, 0 1em)',
655
718
  overflow: 'hidden',
656
719
  });
657
720
  // Adoption is zero-flash hydration of a statically pre-rendered page: it only
@@ -743,6 +806,25 @@ export function createDocBrowser(options) {
743
806
  else
744
807
  layoutHost.removeAttribute('data-layout');
745
808
  }
809
+ /*
810
+ `full-screen` also collapses the nav, and that has to go through the COMPONENT rather than
811
+ CSS: `<tosi-sidenav>` writes its column widths as inline properties, so a stylesheet has
812
+ nothing to override. The pre-hydration rule in doc-system-styles.ts handles the static
813
+ markup, which has a plain `<nav>` and no sidenav yet; this handles the hydrated page. Two
814
+ rules for two genuinely different DOMs, not two fixes for one bug.
815
+ */
816
+ wantsFullScreen = doc.layout === 'full-screen';
817
+ /*
818
+ Forced on every navigation. Without this, opening the nav on one full-screen page (which
819
+ turns `alwaysCompact` off) would leave the NEXT full-screen page not full-screen, because
820
+ the wish had not changed and the transition check would skip it. A reader's override is
821
+ meant to last until they navigate, not beyond it.
822
+ */
823
+ appliedFullScreen = null;
824
+ applyFullScreen();
825
+ // applyFullScreen returns early when there is no sidenav yet; the button state still has
826
+ // to follow the doc, so this does not ride on that path succeeding.
827
+ refreshNavToggle();
746
828
  }
747
829
  rewriteContentLinks();
748
830
  // Stamp each example with its source file (for the source↔doc map). doc.path
@@ -1249,6 +1331,9 @@ export function createDocBrowser(options) {
1249
1331
  sidenav = event.target.closest(TosiSidenav.tagName);
1250
1332
  if (sidenav)
1251
1333
  app.compact = sidenav.compact;
1334
+ refreshNavToggle();
1335
+ // The sidenav has just introduced itself; give it the layout the doc asked for.
1336
+ applyFullScreen();
1252
1337
  },
1253
1338
  }, searchField, navContent, div({
1254
1339
  style: {
@@ -108,6 +108,51 @@ export function docSystemStyleSpec(theme = {}) {
108
108
  'tosi-doc-system[data-layout="full-width"]': {
109
109
  _docContentMaxWidth: 'none',
110
110
  },
111
+ /*
112
+ `full-screen` — no measure, and no chrome.
113
+
114
+ These rules are for the PRE-HYDRATION page, whose markup is a plain `<nav class="doc-nav">`
115
+ with no inline styles, so hiding it here works and works on first paint. Once the bundle
116
+ loads, the nav becomes a slotted node inside `<tosi-sidenav>` carrying its own inline
117
+ `display`, and CSS can no longer touch it — the doc-browser sets `sidenav.navHidden`
118
+ instead. Different DOM before and after hydration, so genuinely two rules rather than a
119
+ belt-and-braces duplicate.
120
+ */
121
+ 'tosi-doc-system[data-layout="full-screen"]': {
122
+ _docContentMaxWidth: 'none',
123
+ },
124
+ 'tosi-doc-system[data-layout="full-screen"] .doc-content': {
125
+ padding: '0',
126
+ /*
127
+ A definite height, so a child asking for `height: 100%` gets one.
128
+
129
+ "The content is the viewport" is the promise this layout makes, and a demo, a canvas or an
130
+ embedded app is the reason to use it — all of which want to FILL, not to sit in a tall
131
+ box. Without a height here `100%` on a child resolves against `auto` and collapses to its
132
+ content.
133
+ */
134
+ height: '100%',
135
+ overflow: 'auto',
136
+ /*
137
+ The gutter goes too. `.doc-content`'s padding is an INLINE style set by the doc-browser,
138
+ so this is set through the variable it reads rather than declared here, where it would
139
+ simply lose.
140
+ */
141
+ _docContentPadding: '0',
142
+ },
143
+ /*
144
+ `.doc-nav.doc-nav` — the class twice, on purpose.
145
+
146
+ `tosi-doc-system:not(:defined) .doc-nav` (the pre-hydration layout, further down this same
147
+ sheet) has IDENTICAL specificity to the single-class form, so which one wins is decided by
148
+ source order, and it sits later. Winning on position works until someone reorders the spec,
149
+ at which point the nav quietly comes back on full-screen pages with nothing to point at.
150
+ Repeating the class costs one selector token and settles it on specificity, wherever these
151
+ rules end up living.
152
+ */
153
+ 'tosi-doc-system[data-layout="full-screen"] > .doc-nav.doc-nav, tosi-doc-system[data-layout="full-screen"] > .doc-navbar.doc-navbar': {
154
+ display: 'none',
155
+ },
111
156
  '.darkmode': {
112
157
  ...invertLuminance(colors),
113
158
  /*
@@ -190,7 +235,7 @@ export function docSystemStyleSpec(theme = {}) {
190
235
  display: 'block',
191
236
  maxWidth: vars.docContentMaxWidth,
192
237
  margin: 'auto',
193
- padding: '0 1em',
238
+ padding: 'var(--doc-content-padding, 0 1em)',
194
239
  // Must match doc-browser's inline style EXACTLY, `overflow` included: it
195
240
  // establishes a block formatting context, which stops the <h1>'s top margin
196
241
  // collapsing out of the box. Without it the static content sits 10px lower
@@ -832,6 +832,29 @@ export async function devServer(config, opts = {}) {
832
832
  });
833
833
  }
834
834
  // disk, and non-HTML assets are streamed untouched.
835
+ /*
836
+ NOTHING the dev server serves may be cached. Ever.
837
+
838
+ None of the file paths below sent `Cache-Control`, so browsers applied HEURISTIC caching —
839
+ they are free to invent a freshness lifetime when you decline to state one, and Safari is the
840
+ most willing to. The result is a dev server that rebuilds correctly while the browser keeps
841
+ showing the previous build, which presents as "the fix did not work" and costs whoever is
842
+ looking at it far more than a re-download. It cost exactly that here: a bug was reported
843
+ against code that had already been fixed.
844
+
845
+ `no-store` rather than `no-cache`: no-cache still stores and revalidates, which needs
846
+ validators we do not emit, and a dev server has nothing to gain from the round trip. The cost
847
+ is re-fetching the bundle on a full page load — a doc site navigates client-side after that,
848
+ and the responses are brotli/gzip compressed anyway.
849
+
850
+ Deliberately NOT applied to the built output: `docs/` is a static site for a real host, and
851
+ telling a CDN never to store it would be actively wrong. This is the dev server only.
852
+ */
853
+ const NO_CACHE = {
854
+ 'Cache-Control': 'no-store, must-revalidate',
855
+ Pragma: 'no-cache',
856
+ Expires: '0',
857
+ };
835
858
  async function respondFile(filePath, request, viaTunnel = false) {
836
859
  /*
837
860
  WHICH loader, decided per request rather than once at boot.
@@ -864,6 +887,7 @@ export async function devServer(config, opts = {}) {
864
887
  return new Response(body, {
865
888
  headers: {
866
889
  'Content-Type': 'text/html; charset=utf-8',
890
+ ...NO_CACHE,
867
891
  ...(encoding
868
892
  ? { 'Content-Encoding': encoding, Vary: 'Accept-Encoding' }
869
893
  : {}),
@@ -881,14 +905,16 @@ export async function devServer(config, opts = {}) {
881
905
  return new Response(bytes, {
882
906
  headers: {
883
907
  'Content-Type': file.type || 'application/octet-stream',
908
+ ...NO_CACHE,
884
909
  'Content-Encoding': encoding,
885
910
  Vary: 'Accept-Encoding',
886
911
  },
887
912
  });
888
913
  }
889
914
  // Everything else streams untouched — images, fonts, epubs and glb are already
890
- // compressed, and re-encoding them only makes them bigger.
891
- return new Response(Bun.file(filePath));
915
+ // compressed, and re-encoding them only makes them bigger. Still uncacheable: an icon or a
916
+ // font you just changed is no more welcome stale than a script is.
917
+ return new Response(Bun.file(filePath), { headers: { ...NO_CACHE } });
892
918
  }
893
919
  async function handleTestReport(request) {
894
920
  try {
@@ -45,14 +45,12 @@ export interface Doc {
45
45
  * frontmatter. Unset means the reading column, so nothing changes for a corpus that does not
46
46
  * ask.
47
47
  *
48
- * `'full-screen'` — chrome out of the way entirely — is NOT implemented yet and is rejected
49
- * rather than half-honoured. It needs `<tosi-side-nav>` to expose a collapsed state: the nav
50
- * width and the nav's own `display` are both set as INLINE styles, which a stylesheet cannot
51
- * override without `!important`, and a value that drops the measure while leaving the chrome
52
- * exactly where it was is the kind of partial success that gets reported as a bug. Tracked in
53
- * TODO.md.
48
+ * `'full-screen'` drops the measure AND puts the nav away, so the page is the whole viewport —
49
+ * for a demo, a canvas, an embedded app. It needed `<tosi-sidenav navHidden>` to exist first:
50
+ * the nav width and the nav's own `display` are both INLINE styles, so no stylesheet could
51
+ * reach them from out here.
54
52
  */
55
- layout?: 'full-width';
53
+ layout?: 'full-width' | 'full-screen';
56
54
  headTitle?: string;
57
55
  description?: string;
58
56
  keywords?: string | string[];
@@ -150,11 +150,8 @@ export function parseFrontmatter(content) {
150
150
  `wide` — and silently keeping the reading column is the behaviour that gets reported as
151
151
  "the metadata does nothing", so say so instead.
152
152
  */
153
- if (val === 'full-width')
153
+ if (val === 'full-width' || val === 'full-screen')
154
154
  data.layout = val;
155
- else if (val === 'full-screen')
156
- console.error(`layout: full-screen is not implemented yet (see TODO.md) — ignoring it rather than ` +
157
- `dropping the measure and leaving the chrome in place`);
158
155
  else if (val)
159
156
  console.error(`unknown layout ${JSON.stringify(val)} in frontmatter`);
160
157
  }