shelving 1.210.0 → 1.211.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shelving",
3
- "version": "1.210.0",
3
+ "version": "1.211.0",
4
4
  "author": "Dave Houlbrooke <dave@shax.com>",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,4 +1,4 @@
1
- import type { ReactElement, ReactNode } from "react";
1
+ import { type ReactElement, type ReactNode } from "react";
2
2
  import SIDEBAR_LAYOUT_CSS from "./SidebarLayout.module.css";
3
3
  export interface SidebarLayoutProps {
4
4
  /** Content rendered in the fixed-width side column. */
@@ -11,7 +11,8 @@ export interface SidebarLayoutProps {
11
11
  /**
12
12
  * Layout with a fixed-width side column (typically navigation) next to a scrollable main content column.
13
13
  * - The sidebar is rendered as `<nav>` — it almost always contains the page's primary navigation.
14
- * - The sidebar collapses above the main content on narrow viewports.
14
+ * - On narrow viewports the sidebar slides off the left of the screen and is toggled with a "show menu" button.
15
+ * - The toggle is driven by a hidden checkbox and pure CSS, so it works even when the page ships no client-side JavaScript.
15
16
  * - Use the `--sidebar-layout-width` and `--sidebar-layout-bg` custom properties to override defaults.
16
17
  */
17
18
  export declare function SidebarLayout({ sidebar, children, right }: SidebarLayoutProps): ReactElement;
@@ -1,16 +1,21 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Bars3Icon, XMarkIcon } from "@heroicons/react/24/solid";
3
+ import { useId } from "react";
2
4
  import { getClass } from "../util/css.js";
3
5
  import { LAYOUT_CSS } from "./Layout.js";
4
6
  import SIDEBAR_LAYOUT_CSS from "./SidebarLayout.module.css";
5
7
  /**
6
8
  * Layout with a fixed-width side column (typically navigation) next to a scrollable main content column.
7
9
  * - The sidebar is rendered as `<nav>` — it almost always contains the page's primary navigation.
8
- * - The sidebar collapses above the main content on narrow viewports.
10
+ * - On narrow viewports the sidebar slides off the left of the screen and is toggled with a "show menu" button.
11
+ * - The toggle is driven by a hidden checkbox and pure CSS, so it works even when the page ships no client-side JavaScript.
9
12
  * - Use the `--sidebar-layout-width` and `--sidebar-layout-bg` custom properties to override defaults.
10
13
  */
11
14
  export function SidebarLayout({ sidebar, children, right = false }) {
12
- const sidebarEl = (_jsx("nav", { className: SIDEBAR_LAYOUT_CSS.sidebar, children: sidebar }, "sidebar"));
13
- const contentEl = (_jsx("div", { className: getClass(LAYOUT_CSS.layout, SIDEBAR_LAYOUT_CSS.content), children: _jsx("div", { className: SIDEBAR_LAYOUT_CSS.contentInner, children: children }) }, "content"));
14
- return (_jsx("main", { className: getClass(SIDEBAR_LAYOUT_CSS.main, LAYOUT_CSS.layout), children: right ? [contentEl, sidebarEl] : [sidebarEl, contentEl] }));
15
+ // Ties the toggle checkbox to its `<label>` buttons `useId()` is stable during static (non-hydrated) rendering.
16
+ const id = useId();
17
+ const sidebarEl = (_jsxs("nav", { className: SIDEBAR_LAYOUT_CSS.sidebar, children: [_jsx("label", { htmlFor: id, title: "Close menu", className: SIDEBAR_LAYOUT_CSS.close, children: _jsx(XMarkIcon, {}) }), sidebar] }, "sidebar"));
18
+ const contentEl = (_jsxs("div", { className: getClass(LAYOUT_CSS.layout, SIDEBAR_LAYOUT_CSS.content), children: [_jsx("label", { htmlFor: id, title: "Show menu", className: SIDEBAR_LAYOUT_CSS.show, children: _jsx(Bars3Icon, {}) }), _jsx("div", { className: SIDEBAR_LAYOUT_CSS.contentInner, children: children })] }, "content"));
19
+ return (_jsxs("main", { className: getClass(SIDEBAR_LAYOUT_CSS.main, LAYOUT_CSS.layout), children: [_jsx("input", { type: "checkbox", id: id, className: SIDEBAR_LAYOUT_CSS.toggle, "aria-label": "Show or hide menu" }), right ? [contentEl, sidebarEl] : [sidebarEl, contentEl]] }));
15
20
  }
16
21
  export { SIDEBAR_LAYOUT_CSS };
@@ -5,6 +5,10 @@
5
5
  * here we override its block-level behaviour so the sidebar and content each scroll independently.
6
6
  * The inner `.content` element reuses the `.layout` class too so it picks up the standard
7
7
  * layout padding, safe-area insets, and `overflow: hidden auto` scroll behaviour.
8
+ *
9
+ * On narrow viewports the sidebar becomes an off-canvas drawer: it slides off the left edge of the
10
+ * screen and is toggled by the hidden `.toggle` checkbox via its `.show` / `.close` `<label>` buttons.
11
+ * Driving the drawer with a checkbox + CSS means it works even when the page ships no client-side JS.
8
12
  */
9
13
 
10
14
  .main {
@@ -34,14 +38,77 @@
34
38
  margin: 0 auto;
35
39
  }
36
40
 
37
- /* On narrow viewports collapse to a single column with the sidebar above. */
41
+ /* Toggle checkbox hidden entirely on wide viewports (see media query for the narrow-viewport state). */
42
+ .toggle {
43
+ display: none;
44
+ }
45
+
46
+ /* Menu toggle buttons — hidden by default, only shown on narrow viewports (see media query below). */
47
+ .show,
48
+ .close {
49
+ display: none;
50
+ width: fit-content;
51
+ margin: 0;
52
+ align-items: center;
53
+ justify-content: center;
54
+ padding: var(--space-small);
55
+ border-radius: var(--radius-xsmall);
56
+ color: var(--color-text);
57
+ cursor: pointer;
58
+
59
+ & svg {
60
+ width: var(--size-icon);
61
+ height: var(--size-icon);
62
+ }
63
+
64
+ &:hover {
65
+ background: var(--color-surface);
66
+ }
67
+ }
68
+
69
+ /* The close button sits at the top of the sidebar, aligned to its trailing edge. */
70
+ .close {
71
+ margin-inline-start: auto;
72
+ margin-bottom: var(--space-xsmall);
73
+ }
74
+
75
+ /* On narrow viewports the sidebar becomes an off-canvas drawer that slides in from the left. */
38
76
  @media (max-width: 48rem) {
39
77
  .main {
40
78
  grid-template-columns: 1fr;
41
79
  }
42
80
  .sidebar {
43
- border-right: none;
44
- border-bottom: 1px solid var(--color-border);
45
- max-height: 50vh;
81
+ position: fixed;
82
+ z-index: 100;
83
+ inset-block: 0;
84
+ left: 0;
85
+ width: var(--sidebar-layout-width, 17.5rem);
86
+ max-width: 85vw;
87
+ transform: translateX(-100%);
88
+ transition: transform var(--duration-normal) ease-in-out;
89
+ }
90
+ /* Checked checkbox → drawer slides on screen. */
91
+ .toggle:checked ~ .sidebar {
92
+ transform: translateX(0);
93
+ box-shadow: 0 0 1.5rem var(--color-overlay);
94
+ }
95
+ /* Keep the checkbox visually hidden but still focusable for keyboard users. */
96
+ .toggle {
97
+ display: block;
98
+ position: absolute;
99
+ width: 1px;
100
+ height: 1px;
101
+ overflow: hidden;
102
+ clip-path: inset(50%);
103
+ }
104
+ .show,
105
+ .close {
106
+ display: flex;
107
+ }
108
+ /* Forward the checkbox's keyboard focus ring to whichever toggle button is on screen. */
109
+ .toggle:focus-visible ~ .content .show,
110
+ .toggle:focus-visible ~ .sidebar .close {
111
+ outline: var(--stroke-focus) solid var(--color-focus);
112
+ outline-offset: 2px;
46
113
  }
47
114
  }
@@ -1,4 +1,5 @@
1
- import type { ReactElement, ReactNode } from "react";
1
+ import { Bars3Icon, XMarkIcon } from "@heroicons/react/24/solid";
2
+ import { type ReactElement, type ReactNode, useId } from "react";
2
3
  import { getClass } from "../util/css.js";
3
4
  import { LAYOUT_CSS } from "./Layout.js";
4
5
  import SIDEBAR_LAYOUT_CSS from "./SidebarLayout.module.css";
@@ -15,22 +16,35 @@ export interface SidebarLayoutProps {
15
16
  /**
16
17
  * Layout with a fixed-width side column (typically navigation) next to a scrollable main content column.
17
18
  * - The sidebar is rendered as `<nav>` — it almost always contains the page's primary navigation.
18
- * - The sidebar collapses above the main content on narrow viewports.
19
+ * - On narrow viewports the sidebar slides off the left of the screen and is toggled with a "show menu" button.
20
+ * - The toggle is driven by a hidden checkbox and pure CSS, so it works even when the page ships no client-side JavaScript.
19
21
  * - Use the `--sidebar-layout-width` and `--sidebar-layout-bg` custom properties to override defaults.
20
22
  */
21
23
  export function SidebarLayout({ sidebar, children, right = false }: SidebarLayoutProps): ReactElement {
24
+ // Ties the toggle checkbox to its `<label>` buttons — `useId()` is stable during static (non-hydrated) rendering.
25
+ const id = useId();
26
+
22
27
  const sidebarEl = (
23
28
  <nav key="sidebar" className={SIDEBAR_LAYOUT_CSS.sidebar}>
29
+ <label htmlFor={id} title="Close menu" className={SIDEBAR_LAYOUT_CSS.close}>
30
+ <XMarkIcon />
31
+ </label>
24
32
  {sidebar}
25
33
  </nav>
26
34
  );
27
35
  const contentEl = (
28
36
  <div key="content" className={getClass(LAYOUT_CSS.layout, SIDEBAR_LAYOUT_CSS.content)}>
37
+ <label htmlFor={id} title="Show menu" className={SIDEBAR_LAYOUT_CSS.show}>
38
+ <Bars3Icon />
39
+ </label>
29
40
  <div className={SIDEBAR_LAYOUT_CSS.contentInner}>{children}</div>
30
41
  </div>
31
42
  );
32
43
  return (
33
- <main className={getClass(SIDEBAR_LAYOUT_CSS.main, LAYOUT_CSS.layout)}>{right ? [contentEl, sidebarEl] : [sidebarEl, contentEl]}</main>
44
+ <main className={getClass(SIDEBAR_LAYOUT_CSS.main, LAYOUT_CSS.layout)}>
45
+ <input type="checkbox" id={id} className={SIDEBAR_LAYOUT_CSS.toggle} aria-label="Show or hide menu" />
46
+ {right ? [contentEl, sidebarEl] : [sidebarEl, contentEl]}
47
+ </main>
34
48
  );
35
49
  }
36
50