@gemboss/website-ui 0.3.0 → 0.5.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.
@@ -3,8 +3,8 @@
3
3
  The full class list, including the CSS-only helpers added while building 0.1.0 (`.gw-h1`–`.gw-h4`, `.gw-lead`,
4
4
  `.gw-note`, `.gw-actions`, `.gw-stats__source`, `.gw-plan__price`, the mobile menu parts…), is `docs/components.md`.
5
5
 
6
- The CSS (`css/*.css`), the script (`js/website.js`) and the templates (`templates/`) agree on exactly this. Change it
7
- here first, then in all three.
6
+ The CSS (`css/*.css`), the script (`js/website.mjs`, and `js/website.js` built from it) and the templates
7
+ (`templates/`) agree on exactly this. Change it here first, then in all three.
8
8
 
9
9
  Prefixes: kit classes `gw-`, kit CSS variables `--gw-`, state classes `is-` (set by the script), hooks `data-gw-*`.
10
10
  BEM-ish: `.gw-block`, `.gw-block__part`, `.gw-block--variant`.
@@ -21,7 +21,84 @@ BEM-ish: `.gw-block`, `.gw-block__part`, `.gw-block--variant`.
21
21
  <header class="gw-header" data-gw-header> … </header>
22
22
  <main id="main"> sections </main>
23
23
  <footer class="gw-footer"> … </footer>
24
- <script src="…/website.js" defer></script>
24
+ <script src="…/website.js" defer></script> <!-- or the ESM entry, see "Script" below -->
25
+ ```
26
+
27
+ ## Script (0.4.0)
28
+
29
+ One script wires every part below: the mega menu, the mobile menu, the theme toggle, reveal, the header's scroll
30
+ state, the current page, the segmented control, the busy button, an article's contents list and the scroll box of its
31
+ prose tables (0.5.0). It comes in two entries built from one source
32
+ (`js/website.mjs`; `scripts/build.mjs` writes `js/website.js` from it and `--check` fails when they differ):
33
+
34
+ | entry | file | how it starts |
35
+ |---|---|---|
36
+ | classic | `js/website.js` (`@gemboss/website-ui/website.js`) | `<script src="…/website.js" defer>`: wires the page on `DOMContentLoaded` by itself, then `window.gwWebsite` |
37
+ | ESM | `js/website.mjs` (`@gemboss/website-ui/website.mjs`) | `import { init, destroy, version } from '@gemboss/website-ui/website.mjs'`: importing does nothing (safe on a server); the app calls `init()` |
38
+
39
+ - **`init(root = document)`** wires the kit markup under `root`, `root` included. It is idempotent: an element is
40
+ wired once, so call it again after every client-side navigation or insert, and only the new elements are wired.
41
+ Called on a fragment, it wires that fragment and nothing else. It also lets go of elements removed from the page
42
+ since the last call, and moves the script's own `aria-current` mark to the new path. The listeners that belong
43
+ to the page (Escape, the outside click, the busy-button guard, the system theme, the scroll) are installed once.
44
+ - **`destroy(root = document)`** takes back what `init` wired under `root`: its listeners and its marks. An open
45
+ mega menu or mobile menu is closed first, so an unmounted menu cannot leave the page locked.
46
+ - **`version`**: `'0.5.0'`. The classic script exposes the three as `window.gwWebsite`.
47
+ - **`data-gw-ready`**: on each wired element, the parts wired on it (`mega`, `menu`, `theme`, `reveal`, `header`,
48
+ `segmented`, `toc`, `table`). It is a mark for people and tests, not a switch: markup copied from a live page, attribute and all,
49
+ is still wired.
50
+ - **One copy per page.** The first copy that runs owns the page (`window.gwWebsite`); another one (the classic
51
+ script next to a bundle, two bundles) hands its calls to it, so nothing is wired twice.
52
+ - Never wire a kit part with the site's own script (a click handler on `.gw-nav__trigger`, a class toggled by hand):
53
+ insert the kit's markup, then call `init` on it.
54
+
55
+ ### After inserting markup
56
+
57
+ A Next.js app (App Router): one client component in the root layout, an effect on each route change. Effects run
58
+ after hydration, so the server's markup is never changed before React reads it.
59
+
60
+ ```jsx
61
+ // app/kit-script.jsx
62
+ 'use client';
63
+ import { useEffect } from 'react';
64
+ import { usePathname } from 'next/navigation';
65
+ import { init } from '@gemboss/website-ui/website.mjs';
66
+
67
+ export default function KitScript() {
68
+ const pathname = usePathname();
69
+ useEffect(() => { init(); }, [pathname]);
70
+ return null;
71
+ }
72
+ // app/layout.js: <body className="gw-page">{children}<KitScript /></body>
73
+ ```
74
+
75
+ A component that mounts and unmounts kit markup on its own (a dialog, a part that streams in later):
76
+ `useEffect(() => { const el = ref.current; init(el); return () => destroy(el); }, [])`, on a wrapper with
77
+ `style={{ display: 'contents' }}` so it does not change the layout.
78
+
79
+ A page that inserts HTML (a GemPages block rendered after load, a CMS preview), with the classic script:
80
+
81
+ ```js
82
+ var slot = document.getElementById('pricing-slot');
83
+ slot.innerHTML = html; // kit markup: a header, a segmented control, sections
84
+ window.gwWebsite.init(slot); // wires what is new in it; the rest of the page is left alone
85
+ ```
86
+
87
+ ### Events
88
+
89
+ The script tells the page's own code what changed. Every event is a `CustomEvent` that bubbles, so one listener on
90
+ `document` hears them all; none is fired by `init`.
91
+
92
+ | event | target | `detail` | fires when |
93
+ |---|---|---|---|
94
+ | `gw:mega` | the trigger (`[data-gw-mega]`) | `{ id, open }`: the panel's `id`, `true` when it opened | a panel opens or closes: click, hover, Escape, a click outside, focus leaving, a link inside, `destroy` of an open one. Moving to another menu fires a close, then an open |
95
+ | `gw:theme` | `<html>` | `{ theme: 'light' \| 'dark' }`: the theme the page now shows | a press on a theme toggle; also a change of the system's scheme while the page follows it (no `data-theme`) |
96
+ | `gw:segmented` | the group (`[data-gw-segmented]`) | `{ name, value }`: the group's name, the chosen option's `value` | the reader chooses an option (click, arrows, Home, End) |
97
+ | `gw:toc` | the contents (`[data-gw-toc]`) | `{ id }`: the id of the heading whose section is now marked, `''` above the first one | the section in view changes: scrolling, a contents link, a jump. Also once after load when the page opens inside a section (a link to `#id`): the observer reports after `init`, never during it |
98
+
99
+ ```js
100
+ document.addEventListener('gw:theme', function (e) { chart.setTheme(e.detail.theme); });
101
+ document.addEventListener('gw:toc', function (e) { analytics.track('section', e.detail.id); });
25
102
  ```
26
103
 
27
104
  ## Header
@@ -61,24 +138,71 @@ BEM-ish: `.gw-block`, `.gw-block__part`, `.gw-block--variant`.
61
138
  Script behaviour:
62
139
  - **Mega menu**: click on a trigger toggles its panel (`hidden` removed, `aria-expanded="true"`, `.is-open` on the
63
140
  `.gw-nav__item`). Hover opens on fine pointers after 80ms and closes 160ms after leaving item and panel. Only one
64
- open at a time. `Escape` closes and returns focus to the trigger. A click outside closes. Arrow keys are not required.
141
+ open at a time on the page, across every header `init` wired. `Escape` closes and returns focus to the trigger. A
142
+ click outside closes. Arrow keys are not required. Each change fires `gw:mega`.
65
143
  - **Mobile menu** (below 1080px the `.gw-nav` and sign-in hide, `.gw-menu-toggle` shows): the toggle flips `hidden`
66
- on `#gw-mobile-menu`, `aria-expanded`, `.is-menu-open` on `<html>` (the CSS locks scroll), and its `aria-label`
67
- (Open menu / Close menu). `Escape` closes. Resizing above 1080px closes it.
144
+ on the menu its `aria-controls` names (`#gw-mobile-menu`), `aria-expanded`, `.is-menu-open` on `<html>` (the CSS
145
+ locks scroll), and its `aria-label` (Open menu / Close menu). `Escape` closes. Resizing above 1080px closes it.
68
146
  - **Labels in other languages**: the script never writes English over a translated page. Give the toggles their
69
147
  words with `data-gw-label-open` / `data-gw-label-close` (menu toggle) and `data-gw-label-dark` /
70
148
  `data-gw-label-light` (theme toggle); without them it keeps the markup's own `aria-label`, then falls back to English.
71
149
  - **Theme toggle**: cycles light ↔ dark by setting `data-theme` on `<html>`, stores the choice in `localStorage`
72
- (`gw-theme`, every access in try/catch), updates `aria-label`. On load, a stored choice is applied before paint by a
73
- one-line inline script in `<head>` that the template carries (`templates/page.html`).
74
- - **Reveal**: elements with `.gw-reveal` get `.is-in` when 15% visible (IntersectionObserver, once). Failsafe: after
75
- 2.5s, or if IntersectionObserver is missing, all get `.is-in`. Under `prefers-reduced-motion` the CSS shows them
76
- without motion.
150
+ (`gw-theme`, every access in try/catch), updates `aria-label` on every toggle, fires `gw:theme`. On load, a stored
151
+ choice is applied before paint by a one-line inline script in `<head>` that the template carries
152
+ (`templates/page.html`).
153
+ - **Reveal**: elements with `.gw-reveal` get `.is-in` when 15% visible (IntersectionObserver, once). Failsafe: 2.5s
154
+ after the `init` that found them, or at once if IntersectionObserver is missing, they get `.is-in`. Under
155
+ `prefers-reduced-motion` the CSS shows them without motion.
77
156
  - **Header**: `.is-scrolled` on `[data-gw-header]` once the page scrolled more than 8px (the CSS may use it for the
78
157
  hairline only; the bar does not change height or colour). The floating header needs nothing more from the script:
79
158
  it is the same element with one more class.
80
- - **Current page**: the script marks `aria-current="page"` on `.gw-nav__link` / `.gw-mega__link` whose `href`
81
- pathname equals `location.pathname` (when the markup did not already).
159
+ - **Current page**: the script marks `aria-current="page"` on `.gw-nav__link` / `.gw-mega__link` and the mobile
160
+ menu's links whose `href` pathname equals `location.pathname` (when the markup did not already). Each `init` takes
161
+ its own marks back first, so after a client-side navigation they follow the new path; a mark in the markup stays.
162
+
163
+ ## Article: contents and prose tables (0.5.0)
164
+
165
+ ```html
166
+ <article class="gw-wrap gw-article">
167
+ <header class="gw-article__head"> .gw-breadcrumbs, <h1>, .gw-article-meta </header>
168
+ <figure class="gw-article__hero"><img …></figure> <!-- optional -->
169
+ <nav class="gw-toc" aria-label="On this page" data-gw-toc>
170
+ <details> <!-- no open: the script opens it from 1080px -->
171
+ <summary>On this page</summary>
172
+ <ol><li><a href="#section-id">The section's heading</a><ol>…h3s…</ol></li> …</ol>
173
+ </details>
174
+ </nav>
175
+ <div class="gw-prose"> <h2 id="section-id">…</h2> … <table><caption>…</caption>…</table> … </div>
176
+ </article>
177
+ ```
178
+
179
+ - **Contents** (`[data-gw-toc]`, a `.gw-toc`): the script reads its links whose `href` is `#id` of an element on the
180
+ page (percent-encoded ids are decoded; links to other pages and to no id are ignored). With none, the list is not
181
+ wired yet and a later `init` tries again.
182
+ - The section in view is the one whose heading is the last above the line 30% down the window. Its link gets
183
+ `aria-current="true"`; the previous link loses it. Above the first heading no link is marked. Each change fires
184
+ `gw:toc` on the nav.
185
+ - One `IntersectionObserver` per list, observing the linked headings, with
186
+ `rootMargin: '100000px 0px -70% 0px'`: its root runs from far above the window down to the line, so a heading
187
+ is "intersecting" exactly while it is above the line, and a jump across many sections is seen as well as a
188
+ scroll. Each report measures every heading again. Without `IntersectionObserver` nothing is marked; the links
189
+ still work.
190
+ - The `<details>` inside: from 1080px (`matchMedia('(min-width: 1080px)')`, the kit's desktop width, where the CSS makes it sticky) the
191
+ script sets `open`; below it, the markup's own state (folded unless the markup says `open`), again on every
192
+ change of width. Below 1080px a click on a link folds it, in the click, before the browser moves to the section.
193
+ - `destroy`: the observer disconnects, the mark goes, the `<details>` takes the markup's state back.
194
+ - **Prose tables**: every `<table>` that is a child of a `.gw-prose` (not one inside the author's own `.gw-table`, a
195
+ figure or another element) is moved into `<div class="gw-prose__table" role="region" tabindex="0" aria-label="…">`
196
+ at its place, and gets `.is-wrapped`.
197
+ - The name is the text of the table's `<caption>`, else of its first `<th>`, else of its first `<td>`, spaces
198
+ collapsed. A table with no text there is left as it is.
199
+ - The CSS keeps a wrapped table `display: table` (some Safari versions stop announcing a table whose display
200
+ changed) and gives the box the scroll, the hairline and the radius. A table without `.is-wrapped` (no script,
201
+ or before it runs) scrolls by itself with `display: block`, as in 0.2.0. Both look the same.
202
+ - Once per table: a second `init` finds it inside its box and leaves it. `destroy` puts the table back in the
203
+ prose, removes the box, and removes `.is-wrapped` unless the markup had it.
204
+ - A framework that renders the prose's table itself (JSX, not HTML from the CMS) should call `destroy(prose)`
205
+ before it re-renders that part, as for every element the script changed.
82
206
 
83
207
  ## Brand profiles (0.3.0)
84
208
 
@@ -131,12 +255,19 @@ Script behaviour:
131
255
  | `.gw-segmented` (+ `--block`) | one choice out of two to four (script above) |
132
256
  | `.gw-badge` (+ `--accent`, `--success`, `--warning`) | small status, pill |
133
257
  | `.gw-card` (+ `--soft`, `--link`) | white box, 1px hairline, the card radius; `--link` makes the whole card an `<a>` |
258
+ | `.gw-card__media` (+ `--wide`, `--square`) | the card's picture, first child, edge to edge on top; 16:9, 1.91:1 or 1:1 (0.5.0) |
259
+ | `.gw-callout` (+ `--note`, `--tip`, `--warning`, `__label`) | a note, a tip or a warning in an article, `role="note"`, its kind in words (0.5.0) |
260
+ | `.gw-breadcrumbs` | `<nav aria-label>` + `<ol>`, the last item `aria-current="page"`; separators drawn by the CSS (0.5.0) |
261
+ | `.gw-article-meta` (+ `__avatar`) | author, category, `<time datetime>`, read time (0.5.0) |
262
+ | `.gw-article` (`__head`, `__hero`), `.gw-toc` | the article page and its contents (script above, 0.5.0) |
263
+ | `.gw-post-list` (+ `--compact`), `.gw-post` (`__title`) | post cards; the title's link covers the card (0.5.0) |
264
+ | `.gw-pagination` (`__status`) | previous / next links, `rel="prev"` / `rel="next"`, markup only (0.5.0) |
134
265
  | `.gw-window` (+ `__bar`, `__body`, `__caption`) | a framed screenshot/mock-up, like a browser or app window; `__caption` (last child) says what its numbers are: "Illustrative data." or their source |
135
266
  | `.gw-icon` | 20px inline stroke SVG wrapper |
136
267
  | `.gw-list--check` | list with check marks |
137
268
  | `.gw-table` (+ `--compare`) | data / comparison table; scrolls inside its box on phones |
138
269
  | `.gw-faq` (`<details class="gw-faq__item"><summary>`) | FAQ accordion with no script |
139
- | `.gw-prose` (+ `--sm`) | long-form reading: an article's own HTML, no classes inside |
270
+ | `.gw-prose` (+ `--sm`) | long-form reading: an article's own HTML, no classes inside; its bare tables get a `.gw-prose__table` box from the script (0.5.0) |
140
271
  | `.gw-tabs` (`role="tablist"`) | NOT in the kit yet (a reported gap would add it) |
141
272
 
142
273
  ## Sections