shelving 1.264.1 → 1.264.2

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.
Files changed (70) hide show
  1. package/package.json +1 -1
  2. package/ui/README.md +2 -2
  3. package/ui/block/Blockquote.module.css +0 -3
  4. package/ui/block/Card.md +3 -7
  5. package/ui/block/Card.module.css +0 -3
  6. package/ui/block/Heading.md +0 -1
  7. package/ui/block/Heading.module.css +0 -3
  8. package/ui/block/Label.module.css +0 -3
  9. package/ui/block/List.md +1 -2
  10. package/ui/block/List.module.css +0 -3
  11. package/ui/block/Panel.md +1 -2
  12. package/ui/block/Panel.module.css +0 -3
  13. package/ui/block/Paragraph.md +1 -2
  14. package/ui/block/Paragraph.module.css +0 -3
  15. package/ui/block/Preformatted.module.css +0 -3
  16. package/ui/block/Subheading.md +0 -1
  17. package/ui/block/Subheading.module.css +0 -3
  18. package/ui/block/Title.md +0 -1
  19. package/ui/block/Title.module.css +0 -3
  20. package/ui/button/Button.md +1 -2
  21. package/ui/button/Button.module.css +0 -3
  22. package/ui/docs/DocumentationPage.test.tsx +2 -4
  23. package/ui/docs/DocumentationParams.js +1 -1
  24. package/ui/docs/DocumentationParams.tsx +1 -1
  25. package/ui/docs/DocumentationProperties.js +1 -1
  26. package/ui/docs/DocumentationProperties.tsx +1 -1
  27. package/ui/docs/DocumentationReferences.js +1 -1
  28. package/ui/docs/DocumentationReferences.tsx +1 -1
  29. package/ui/docs/DocumentationReturns.js +1 -1
  30. package/ui/docs/DocumentationReturns.tsx +1 -1
  31. package/ui/docs/DocumentationThrows.js +1 -1
  32. package/ui/docs/DocumentationThrows.tsx +1 -1
  33. package/ui/form/Field.test.tsx +1 -1
  34. package/ui/inline/Deleted.md +4 -5
  35. package/ui/inline/Deleted.module.css +1 -3
  36. package/ui/inline/Inserted.md +4 -5
  37. package/ui/inline/Inserted.module.css +1 -3
  38. package/ui/inline/Link.md +2 -3
  39. package/ui/inline/Link.module.css +1 -3
  40. package/ui/inline/Mark.js +1 -2
  41. package/ui/inline/Mark.md +5 -5
  42. package/ui/inline/Mark.module.css +2 -3
  43. package/ui/inline/Mark.tsx +0 -2
  44. package/ui/input/SchemaInput.test.tsx +3 -2
  45. package/ui/layout/CenteredLayout.d.ts +5 -7
  46. package/ui/layout/CenteredLayout.js +8 -4
  47. package/ui/layout/CenteredLayout.md +9 -2
  48. package/ui/layout/CenteredLayout.module.css +16 -22
  49. package/ui/layout/CenteredLayout.tsx +18 -12
  50. package/ui/layout/SidebarLayout.js +12 -5
  51. package/ui/layout/SidebarLayout.md +4 -4
  52. package/ui/layout/SidebarLayout.module.css +6 -9
  53. package/ui/layout/SidebarLayout.tsx +20 -17
  54. package/ui/misc/MetaContext.test.tsx +2 -3
  55. package/ui/misc/Tag.md +1 -2
  56. package/ui/misc/Tag.module.css +0 -3
  57. package/ui/notice/Message.module.css +0 -3
  58. package/ui/notice/Notice.md +1 -2
  59. package/ui/notice/Notice.module.css +0 -3
  60. package/ui/router/Router.test.tsx +1 -4
  61. package/ui/style/TINT_CLASS.md +1 -1
  62. package/ui/style/Width.d.ts +2 -1
  63. package/ui/style/Width.module.css +47 -17
  64. package/ui/style/Width.tsx +22 -1
  65. package/ui/style/getWidthClass.md +5 -9
  66. package/ui/table/Table.md +1 -1
  67. package/ui/tree/TreeButton.test.tsx +2 -5
  68. package/ui/tree/TreeContext.test.tsx +2 -3
  69. package/ui/tree/TreeMarkup.test.tsx +2 -5
  70. package/ui/tree/TreeRouter.test.tsx +2 -5
@@ -1,16 +1,14 @@
1
1
  import type { ReactElement } from "react";
2
+ import type { IndentVariants } from "../style/Indent.js";
3
+ import type { PaddingVariants } from "../style/Padding.js";
4
+ import type { WidthVariants } from "../style/Width.js";
2
5
  import type { OptionalChildProps } from "../util/props.js";
3
6
  /**
4
7
  * Props for `<CenteredLayout>` — optional `children` and a `fullWidth` flag to drop the max-width.
5
8
  *
6
9
  * @see https://shelving.cc/ui/CenteredLayoutProps
7
10
  */
8
- export interface CenteredLayoutProps extends OptionalChildProps {
9
- /**
10
- * Drop the narrow max-width and let content fill the width.
11
- * @default false
12
- */
13
- fullWidth?: boolean;
11
+ export interface CenteredLayoutProps extends WidthVariants, PaddingVariants, IndentVariants, OptionalChildProps {
14
12
  }
15
13
  /**
16
14
  * Layout that centres its content with no header/footer and a narrow max-width.
@@ -19,4 +17,4 @@ export interface CenteredLayoutProps extends OptionalChildProps {
19
17
  * @kind component
20
18
  * @see https://shelving.cc/ui/CenteredLayout
21
19
  */
22
- export declare function CenteredLayout({ children, fullWidth }: CenteredLayoutProps): ReactElement;
20
+ export declare function CenteredLayout({ children, ...props }: CenteredLayoutProps): ReactElement;
@@ -1,7 +1,10 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { RouteCache } from "../router/RouteCache.js";
3
- import { getModuleClass } from "../util/css.js";
4
- import CENTERED_LAYOUT_CSS from "./CenteredLayout.module.css";
3
+ import { getBlockClass } from "../style/Block.js";
4
+ import { getClass, getModuleClass } from "../util/css.js";
5
+ import LAYOUT_CSS from "./CenteredLayout.module.css";
6
+ const LAYOUT_MAIN_CLASS = getModuleClass(LAYOUT_CSS, "main");
7
+ const LAYOUT_INNER_CLASS = getModuleClass(LAYOUT_CSS, "inner");
5
8
  /**
6
9
  * Layout that centres its content with no header/footer and a narrow max-width.
7
10
  * - Used for e.g. login/register/error/form pages where the content is the only focus.
@@ -9,8 +12,9 @@ import CENTERED_LAYOUT_CSS from "./CenteredLayout.module.css";
9
12
  * @kind component
10
13
  * @see https://shelving.cc/ui/CenteredLayout
11
14
  */
12
- export function CenteredLayout({ children, fullWidth = false }) {
15
+ export function CenteredLayout({ children, ...props }) {
13
16
  // Wrap the scrolling `<main>` in `<RouteCache>` so recently-visited pages stay mounted but hidden,
14
17
  // keeping their scroll position and state intact across back/forward navigation.
15
- return (_jsx(RouteCache, { children: _jsx("main", { className: getModuleClass(CENTERED_LAYOUT_CSS, "main"), children: _jsx("div", { className: getModuleClass(CENTERED_LAYOUT_CSS, "mainInner"), style: fullWidth ? { maxWidth: "none" } : undefined, children: children }) }) }));
18
+ return (_jsx(RouteCache, { children: _jsx("main", { className: LAYOUT_MAIN_CLASS, children: _jsx("div", { className: getClass(LAYOUT_INNER_CLASS, //
19
+ getBlockClass(props)), children: children }) }) }));
16
20
  }
@@ -27,6 +27,13 @@ Layouts compose naturally as `<Router>` route values — wrap a group of routes
27
27
 
28
28
  ## Styling
29
29
 
30
- This layout exposes no own `--centered-layout-*` hooks. The inner column is capped at the global `--width-narrow` token (dropped when `fullWidth` is set), and the outer element owns its scroll, padding, and safe-area behaviour directly — reading the layout hooks `--layout-space`, `--layout-padding`, `--layout-body-bg`, and `--layout-inset-top` / `-bottom` / `-left` / `-right`.
30
+ | Variable | Styles | Default |
31
+ |---|---|---|
32
+ | `--centered-layout-width` | Max width of the centred column | `var(--width-narrow)` |
33
+ | `--centered-layout-space` | Top/bottom padding of the scroll area | `var(--space-normal)` |
34
+ | `--centered-layout-padding` | Left/right padding of the scroll area | `var(--space-normal)` |
35
+ | `--centered-layout-background` | Page background while the layout is mounted | Unset — the `body` default from `Typography.module.css` shows |
31
36
 
32
- **Global tokens it reads** `--width-narrow`.
37
+ The max-width cap is dropped entirely when `fullWidth` is set. The outer element owns its scroll, padding, and safe-area behaviour directly — it also reads the `--layout-inset-top` / `-bottom` / `-left` / `-right` hooks owned by `Layout.ts` (`useSafeKeyboardArea()` writes `--layout-inset-bottom`).
38
+
39
+ **Global tokens it reads** — `--width-narrow` and `--space-normal`.
@@ -1,24 +1,7 @@
1
1
  @import "../style/layers.css";
2
2
  @import "../style/Space.module.css";
3
- @import "../style/Tint.module.css";
4
3
  @import "../style/Width.module.css";
5
4
 
6
- /* Low-priority padding defaults live in `@layer defaults` so consumers can override them */
7
- /* with normal-specificity selectors without needing `:where()` tricks. */
8
- @layer defaults {
9
- .main {
10
- /* Top/bottom use `layout-space` and left/right use `layout-padding`, plus safe-area additions. */
11
- padding: var(--layout-space, var(--space-normal)) var(--layout-padding, var(--space-normal));
12
- padding-top: calc(var(--layout-space, var(--space-normal)) + max(var(--layout-inset-top, 0px), env(safe-area-inset-top, 0px)));
13
- padding-bottom: calc(var(--layout-space, var(--space-normal)) + max(var(--layout-inset-bottom, 0px), env(safe-area-inset-bottom, 0px)));
14
- padding-left: calc(var(--layout-padding, var(--space-normal)) + max(var(--layout-inset-left, 0px), env(safe-area-inset-left, 0px)));
15
- padding-right: calc(var(--layout-padding, var(--space-normal)) + max(var(--layout-inset-right, 0px), env(safe-area-inset-right, 0px)));
16
-
17
- /* Grow to fill the body. */
18
- flex: 1;
19
- }
20
- }
21
-
22
5
  @layer components {
23
6
  /*
24
7
  * Lock the document and let `.main` own the page scroll.
@@ -51,11 +34,22 @@
51
34
  position: fixed;
52
35
  inset: 0;
53
36
 
54
- /* Layout-specific bg hook. */
55
- background: var(--layout-body-bg, var(--tint-100));
37
+ /* Deliberately no fallback: when the hook is unset this declaration fails and the body default from Typography.module.css shows. */
38
+ background: var(--centered-layout-background);
56
39
  }
57
40
 
58
41
  .main {
42
+ /* Box */
43
+ border-top: env(safe-area-inset-top, 0px) solid transparent;
44
+ border-bottom: env(safe-area-inset-bottom, 0px) solid transparent;
45
+ border-left: env(safe-area-inset-left, 0px) solid transparent;
46
+ border-right: env(safe-area-inset-right, 0px) solid transparent;
47
+ padding-block: var(--centered-layout-padding, var(--space-normal));
48
+ padding-inline: var(--centered-layout-indent, var(--space-normal));
49
+
50
+ /* Grow to fill the body. */
51
+ flex: 1;
52
+
59
53
  /* Content — a flex column whose inner column centres itself with `margin: auto` (both axes). */
60
54
  display: flex;
61
55
  flex-direction: column;
@@ -70,9 +64,9 @@
70
64
  scroll-behavior: smooth;
71
65
  }
72
66
 
73
- .mainInner {
74
- width: 100%;
75
- max-width: var(--width-narrow);
67
+ .inner {
68
+ width: var(--centered-layout-width, var(--width-narrow));
69
+ max-width: 100%;
76
70
  margin: auto;
77
71
  }
78
72
  }
@@ -1,21 +1,22 @@
1
1
  import type { ReactElement } from "react";
2
2
  import { RouteCache } from "../router/RouteCache.js";
3
- import { getModuleClass } from "../util/css.js";
3
+ import { getBlockClass } from "../style/Block.js";
4
+ import type { IndentVariants } from "../style/Indent.js";
5
+ import type { PaddingVariants } from "../style/Padding.js";
6
+ import type { WidthVariants } from "../style/Width.js";
7
+ import { getClass, getModuleClass } from "../util/css.js";
4
8
  import type { OptionalChildProps } from "../util/props.js";
5
- import CENTERED_LAYOUT_CSS from "./CenteredLayout.module.css";
9
+ import LAYOUT_CSS from "./CenteredLayout.module.css";
10
+
11
+ const LAYOUT_MAIN_CLASS = getModuleClass(LAYOUT_CSS, "main");
12
+ const LAYOUT_INNER_CLASS = getModuleClass(LAYOUT_CSS, "inner");
6
13
 
7
14
  /**
8
15
  * Props for `<CenteredLayout>` — optional `children` and a `fullWidth` flag to drop the max-width.
9
16
  *
10
17
  * @see https://shelving.cc/ui/CenteredLayoutProps
11
18
  */
12
- export interface CenteredLayoutProps extends OptionalChildProps {
13
- /**
14
- * Drop the narrow max-width and let content fill the width.
15
- * @default false
16
- */
17
- fullWidth?: boolean;
18
- }
19
+ export interface CenteredLayoutProps extends WidthVariants, PaddingVariants, IndentVariants, OptionalChildProps {}
19
20
 
20
21
  /**
21
22
  * Layout that centres its content with no header/footer and a narrow max-width.
@@ -24,13 +25,18 @@ export interface CenteredLayoutProps extends OptionalChildProps {
24
25
  * @kind component
25
26
  * @see https://shelving.cc/ui/CenteredLayout
26
27
  */
27
- export function CenteredLayout({ children, fullWidth = false }: CenteredLayoutProps): ReactElement {
28
+ export function CenteredLayout({ children, ...props }: CenteredLayoutProps): ReactElement {
28
29
  // Wrap the scrolling `<main>` in `<RouteCache>` so recently-visited pages stay mounted but hidden,
29
30
  // keeping their scroll position and state intact across back/forward navigation.
30
31
  return (
31
32
  <RouteCache>
32
- <main className={getModuleClass(CENTERED_LAYOUT_CSS, "main")}>
33
- <div className={getModuleClass(CENTERED_LAYOUT_CSS, "mainInner")} style={fullWidth ? { maxWidth: "none" } : undefined}>
33
+ <main className={LAYOUT_MAIN_CLASS}>
34
+ <div
35
+ className={getClass(
36
+ LAYOUT_INNER_CLASS, //
37
+ getBlockClass(props),
38
+ )}
39
+ >
34
40
  {children}
35
41
  </div>
36
42
  </main>
@@ -5,7 +5,13 @@ import { Button } from "../button/Button.js";
5
5
  import { requireMetaURL } from "../misc/MetaContext.js";
6
6
  import { RouteCache } from "../router/RouteCache.js";
7
7
  import { getClass, getModuleClass } from "../util/css.js";
8
- import SIDEBAR_LAYOUT_CSS from "./SidebarLayout.module.css";
8
+ import LAYOUT_CSS from "./SidebarLayout.module.css";
9
+ const LAYOUT_SIDEBAR_CLASS = getModuleClass(LAYOUT_CSS, "sidebar");
10
+ const LAYOUT_CONTENT_CLASS = getModuleClass(LAYOUT_CSS, "content");
11
+ const LAYOUT_TOGGLE_CLASS = getModuleClass(LAYOUT_CSS, "toggle");
12
+ const LAYOUT_OVERLAY_CLASS = getModuleClass(LAYOUT_CSS, "overlay");
13
+ const LAYOUT_MAIN_CLASS = getModuleClass(LAYOUT_CSS, "main");
14
+ const LAYOUT_OPEN_CLASS = getModuleClass(LAYOUT_CSS, "open");
9
15
  /**
10
16
  * Layout with a fixed-width side column (typically navigation) next to a scrollable main content column.
11
17
  * - The sidebar is rendered as `<nav>` — it almost always contains the page's primary navigation.
@@ -25,12 +31,13 @@ export function SidebarLayout({ sidebar, children, right = false }) {
25
31
  if (path)
26
32
  setOpen(false);
27
33
  }, [path]);
28
- const sidebarEl = (_jsx("nav", { className: getClass(getModuleClass(SIDEBAR_LAYOUT_CSS, "sidebar"), open && getModuleClass(SIDEBAR_LAYOUT_CSS, "open")), children: sidebar }, "sidebar"));
34
+ const sidebarEl = (_jsx("nav", { className: getClass(LAYOUT_SIDEBAR_CLASS, //
35
+ open && LAYOUT_OPEN_CLASS), children: sidebar }, "sidebar"));
29
36
  // Wrap the scrolling content column in `<RouteCache>` so recently-visited pages stay mounted but hidden
30
37
  // — keeping the scroll position of this `.content` container (and all page state) intact across
31
38
  // back/forward navigation. The sidebar and drawer state stay outside the cache, so they are neither
32
39
  // duplicated nor remounted as the URL changes.
33
- const contentEl = (_jsx(RouteCache, { children: _jsxs("div", { className: getModuleClass(SIDEBAR_LAYOUT_CSS, "content"), children: [_jsx("div", { className: getModuleClass(SIDEBAR_LAYOUT_CSS, "toggle"), children: _jsx(Button, { title: open ? "Close menu" : "Show menu", onClick: () => setOpen(o => !o), children: open ? _jsx(XMarkIcon, {}) : _jsx(Bars3Icon, {}) }) }), _jsx("div", { className: getModuleClass(SIDEBAR_LAYOUT_CSS, "contentInner"), children: children })] }) }, "content"));
34
- const overlayEl = open && (_jsx("button", { type: "button", className: getModuleClass(SIDEBAR_LAYOUT_CSS, "overlay"), "aria-label": "Close menu", onClick: () => setOpen(false) }, "overlay"));
35
- return (_jsx("main", { className: getModuleClass(SIDEBAR_LAYOUT_CSS, "main"), children: right ? [contentEl, sidebarEl, overlayEl] : [sidebarEl, contentEl, overlayEl] }));
40
+ const contentEl = (_jsx(RouteCache, { children: _jsxs("div", { className: LAYOUT_CONTENT_CLASS, children: [_jsx("div", { className: LAYOUT_TOGGLE_CLASS, children: _jsx(Button, { title: open ? "Close menu" : "Show menu", onClick: () => setOpen(o => !o), children: open ? _jsx(XMarkIcon, {}) : _jsx(Bars3Icon, {}) }) }), children] }) }, "content"));
41
+ const overlayEl = open && (_jsx("button", { type: "button", className: LAYOUT_OVERLAY_CLASS, "aria-label": "Close menu", onClick: () => setOpen(false) }, "overlay"));
42
+ return _jsx("main", { className: LAYOUT_MAIN_CLASS, children: right ? [contentEl, sidebarEl, overlayEl] : [sidebarEl, contentEl, overlayEl] });
36
43
  }
@@ -48,11 +48,11 @@ useEffect(useSafeKeyboardArea, []);
48
48
  | Variable | Styles | Default |
49
49
  |---|---|---|
50
50
  | `--sidebar-layout-width` | Width of the side column (and drawer) | `17.5rem` |
51
- | `--sidebar-layout-background` | Page background while the layout is mounted | `var(--tint-100)` |
52
- | `--sidebar-layout-sidebar-background` | Sidebar column fill | `var(--tint-90)` |
53
- | `--sidebar-layout-content-background` | Main content column fill | `var(--tint-100)` |
51
+ | `--sidebar-layout-background` | Page background while the layout is mounted | Unset — the `body` default from `Typography.module.css` shows |
52
+ | `--sidebar-layout-sidebar-background` | Sidebar column fill | Unset — the page background shows |
53
+ | `--sidebar-layout-content-background` | Main content column fill | Unset — the page background shows |
54
54
  | `--sidebar-layout-border` | Divider between sidebar and content | `var(--stroke-normal) solid var(--tint-80)` |
55
55
 
56
56
  The sidebar and content columns own their own scroll behaviour directly (this layout no longer composes a shared `.layout` class). `useSafeKeyboardArea()` still writes `--layout-inset-bottom` for layouts that pad to the safe area.
57
57
 
58
- **Global tokens it reads** — the tint ladder `--tint-80` / `--tint-90` / `--tint-100`, plus `--space-normal`, `--stroke-normal`, `--duration-normal`, and `--color-shadow`.
58
+ **Global tokens it reads** — `--tint-80`, plus `--space-normal`, `--stroke-normal`, `--duration-normal`, and `--color-shadow`.
@@ -50,7 +50,8 @@
50
50
  inset: 0;
51
51
 
52
52
  /* Style */
53
- background: var(--sidebar-layout-background, var(--tint-100));
53
+ /* Deliberately no fallback: when the hook is unset this declaration fails and the body default from Typography.module.css shows. */
54
+ background: var(--sidebar-layout-background);
54
55
  }
55
56
 
56
57
  .main {
@@ -74,7 +75,8 @@
74
75
  padding: var(--space-normal);
75
76
 
76
77
  /* Style */
77
- background: var(--sidebar-layout-sidebar-background, var(--tint-90));
78
+ /* Deliberately no fallback: when the hook is unset this declaration fails and the page background shows. */
79
+ background: var(--sidebar-layout-sidebar-background);
78
80
  }
79
81
 
80
82
  .content {
@@ -91,13 +93,8 @@
91
93
  scroll-behavior: smooth;
92
94
 
93
95
  /* Style */
94
- background: var(--sidebar-layout-content-background, var(--tint-100));
95
- }
96
-
97
- .contentInner {
98
- width: 100%;
99
- padding: 0;
100
- margin: 0 auto;
96
+ /* Deliberately no fallback: when the hook is unset this declaration fails and the page background shows. */
97
+ background: var(--sidebar-layout-content-background);
101
98
  }
102
99
 
103
100
  /* Wrapper for the menu toggle button — hidden on wide viewports, shown on narrow ones (see media query). */
@@ -5,7 +5,14 @@ import { requireMetaURL } from "../misc/MetaContext.js";
5
5
  import { RouteCache } from "../router/RouteCache.js";
6
6
  import { getClass, getModuleClass } from "../util/css.js";
7
7
  import type { OptionalChildProps } from "../util/props.js";
8
- import SIDEBAR_LAYOUT_CSS from "./SidebarLayout.module.css";
8
+ import LAYOUT_CSS from "./SidebarLayout.module.css";
9
+
10
+ const LAYOUT_SIDEBAR_CLASS = getModuleClass(LAYOUT_CSS, "sidebar");
11
+ const LAYOUT_CONTENT_CLASS = getModuleClass(LAYOUT_CSS, "content");
12
+ const LAYOUT_TOGGLE_CLASS = getModuleClass(LAYOUT_CSS, "toggle");
13
+ const LAYOUT_OVERLAY_CLASS = getModuleClass(LAYOUT_CSS, "overlay");
14
+ const LAYOUT_MAIN_CLASS = getModuleClass(LAYOUT_CSS, "main");
15
+ const LAYOUT_OPEN_CLASS = getModuleClass(LAYOUT_CSS, "open");
9
16
 
10
17
  /**
11
18
  * Props for `<SidebarLayout>` — the `sidebar` column content, main `children`, and a `right` placement flag.
@@ -42,39 +49,35 @@ export function SidebarLayout({ sidebar, children, right = false }: SidebarLayou
42
49
  const sidebarEl = (
43
50
  <nav
44
51
  key="sidebar"
45
- className={getClass(getModuleClass(SIDEBAR_LAYOUT_CSS, "sidebar"), open && getModuleClass(SIDEBAR_LAYOUT_CSS, "open"))}
52
+ className={getClass(
53
+ LAYOUT_SIDEBAR_CLASS, //
54
+ open && LAYOUT_OPEN_CLASS,
55
+ )}
46
56
  >
47
57
  {sidebar}
48
58
  </nav>
49
59
  );
60
+
50
61
  // Wrap the scrolling content column in `<RouteCache>` so recently-visited pages stay mounted but hidden
51
62
  // — keeping the scroll position of this `.content` container (and all page state) intact across
52
63
  // back/forward navigation. The sidebar and drawer state stay outside the cache, so they are neither
53
64
  // duplicated nor remounted as the URL changes.
54
65
  const contentEl = (
55
66
  <RouteCache key="content">
56
- <div className={getModuleClass(SIDEBAR_LAYOUT_CSS, "content")}>
57
- <div className={getModuleClass(SIDEBAR_LAYOUT_CSS, "toggle")}>
67
+ <div className={LAYOUT_CONTENT_CLASS}>
68
+ <div className={LAYOUT_TOGGLE_CLASS}>
58
69
  <Button title={open ? "Close menu" : "Show menu"} onClick={() => setOpen(o => !o)}>
59
70
  {open ? <XMarkIcon /> : <Bars3Icon />}
60
71
  </Button>
61
72
  </div>
62
- <div className={getModuleClass(SIDEBAR_LAYOUT_CSS, "contentInner")}>{children}</div>
73
+ {children}
63
74
  </div>
64
75
  </RouteCache>
65
76
  );
77
+
66
78
  const overlayEl = open && (
67
- <button
68
- key="overlay"
69
- type="button"
70
- className={getModuleClass(SIDEBAR_LAYOUT_CSS, "overlay")}
71
- aria-label="Close menu"
72
- onClick={() => setOpen(false)}
73
- />
74
- );
75
- return (
76
- <main className={getModuleClass(SIDEBAR_LAYOUT_CSS, "main")}>
77
- {right ? [contentEl, sidebarEl, overlayEl] : [sidebarEl, contentEl, overlayEl]}
78
- </main>
79
+ <button key="overlay" type="button" className={LAYOUT_OVERLAY_CLASS} aria-label="Close menu" onClick={() => setOpen(false)} />
79
80
  );
81
+
82
+ return <main className={LAYOUT_MAIN_CLASS}>{right ? [contentEl, sidebarEl, overlayEl] : [sidebarEl, contentEl, overlayEl]}</main>;
80
83
  }
@@ -1,9 +1,8 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import type { ReactNode } from "react";
3
3
  import { renderToStaticMarkup } from "react-dom/server";
4
- import { RequiredError } from "../../error/RequiredError.js";
5
- import { createMeta } from "../util/meta.js";
6
- import { MetaContext, requireMetaURL } from "./MetaContext.js";
4
+ import { RequiredError } from "shelving/error";
5
+ import { createMeta, MetaContext, requireMetaURL } from "shelving/ui";
7
6
 
8
7
  /** Render `requireMetaURL().path` from inside a component so its `use(MetaContext)` call is valid. */
9
8
  function Probe(): ReactNode {
package/ui/misc/Tag.md CHANGED
@@ -20,11 +20,10 @@ import { Tag } from "shelving/ui";
20
20
 
21
21
  ## Styling
22
22
 
23
- `Tag` paints from the [tint ladder](/ui/TINT_CLASS); override these hooks at `:root` (or any ancestor scope) to retheme. Move `--tag-tint` to recolour the whole tag at once.
23
+ `Tag` paints from the [tint ladder](/ui/TINT_CLASS); override these hooks at `:root` (or any ancestor scope) to retheme. Apply `color=` / `status=` (on the tag or an ancestor scope) to recolour the whole tag at once.
24
24
 
25
25
  | Variable | Styles | Default |
26
26
  |---|---|---|
27
- | `--tag-tint` | Tint anchor for the tag scope | `inherit` (flows from `color=` / `status=` / parent) |
28
27
  | `--tag-background` | Surface fill | `var(--tint-50)` |
29
28
  | `--tag-hover-background` | Surface fill when an interactive tag is hovered | `var(--tint-55)` |
30
29
  | `--tag-color` | Text colour | `var(--tint-100)` |
@@ -8,9 +8,6 @@
8
8
 
9
9
  @layer components {
10
10
  .tag {
11
- /* Theme */
12
- --tint-50: var(--tag-tint, inherit);
13
-
14
11
  /* Box */
15
12
  display: inline-block;
16
13
  vertical-align: text-bottom;
@@ -5,9 +5,6 @@
5
5
 
6
6
  @layer components {
7
7
  .message {
8
- /* Theme */
9
- --tint-50: var(--notice-tint, inherit);
10
-
11
8
  /* Box */
12
9
  margin-block: var(--message-space, var(--space-paragraph));
13
10
 
@@ -28,11 +28,10 @@ import { LOADING_NOTICE } from "shelving/ui";
28
28
 
29
29
  ## Styling
30
30
 
31
- `Notice` paints from the [tint ladder](/ui/TINT_CLASS); override these hooks at `:root` (or any ancestor scope) to retheme. Move `--notice-tint` to recolour everything at once.
31
+ `Notice` paints from the [tint ladder](/ui/TINT_CLASS); override these hooks at `:root` (or any ancestor scope) to retheme. Its `status` (or a `color=` / `status=` on an ancestor scope) sets the tint that the surface, border, and text all derive from; reach for a per-property hook for a single change.
32
32
 
33
33
  | Variable | Styles | Default |
34
34
  |---|---|---|
35
- | `--notice-tint` | Tint anchor for the notice scope | `inherit` (flows from `color=` / `status=` / parent) |
36
35
  | `--notice-background` | Surface fill | `var(--tint-90)` |
37
36
  | `--notice-color` | Text colour | `var(--tint-50)` |
38
37
  | `--notice-border` | Border shorthand | `var(--notice-stroke) solid var(--tint-80)` |
@@ -6,9 +6,6 @@
6
6
 
7
7
  @layer components {
8
8
  .notice {
9
- /* Theme */
10
- --tint-50: var(--notice-tint, inherit);
11
-
12
9
  /* Box */
13
10
  margin-block: var(--notice-space, var(--space-paragraph));
14
11
  border-radius: var(--notice-radius, var(--radius-xsmall));
@@ -1,9 +1,6 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import { renderToStaticMarkup } from "react-dom/server";
3
- import { MetaContext } from "../misc/MetaContext.js";
4
- import { createMeta } from "../util/meta.js";
5
- import { Router } from "./Router.js";
6
- import type { RouteProps } from "./Routes.js";
3
+ import { createMeta, MetaContext, type RouteProps, Router } from "shelving/ui";
7
4
 
8
5
  const ROUTES = {
9
6
  "/": () => <main>Home</main>,
@@ -42,7 +42,7 @@ Pairings follow contrast: long text reads at `00`-on-`90` or `00`-on-`100`; shor
42
42
  A theme is a CSS file of custom-property overrides at `:root`, imported after the base styles. Work from broadest to narrowest:
43
43
 
44
44
  1. **Move a palette colour.** Overriding `--color-gray` moves the default anchor, retinting every neutral ladder in the app — the broadest possible change. Overriding `--color-red`, `--color-primary`, etc. re-aims every variant and status that maps to it.
45
- 2. **Retint one component family.** Set its tint hook: `--card-tint: var(--color-purple)` makes all cards (and their nested content) purple-tinted, with text, border, surface, and hover shades all derived for free.
45
+ 2. **Retint a region.** Apply `color=` / `status=` to a container — `<Card color="purple">`, or a `<Panel color="purple">` wrapping a whole section — and everything inside re-tints, with text, border, surface, and hover shades all derived for free. Components don't expose a per-component `--x-tint` hook; the variant moves the anchor, not a bespoke variable.
46
46
  3. **Override one property.** Per-property hooks are the scalpel: `--button-radius: 999px`, `--card-border: none`, `--tag-case: none`.
47
47
 
48
48
  **Don't override individual ladder steps (`--tint-90`, etc.) at `:root`.** The ladder is *recomputed* from the anchor inside every `TINT_CLASS` scope — which includes every component that accepts `color=` or `status=` — so a step override at `:root` only reaches untinted regions and produces inconsistent surfaces. Move the anchor (option 1 or 2) instead, and the steps follow.
@@ -3,10 +3,11 @@
3
3
  * - `narrow` / `normal` / `wide` — fixed widths from the `--width-*` tokens (capped at 100%).
4
4
  * - `full` — take the full available width.
5
5
  * - `fit` — shrink to fit the content's intrinsic width (`fit-content`).
6
+ * - `1x`–`32x` — fixed multiples of `--space-normal` (1x–8x at 1x intervals, then 10x/12x, then 16x–32x at 4x intervals).
6
7
  *
7
8
  * @see https://shelving.cc/ui/WidthVariant
8
9
  */
9
- export type WidthVariant = "xxnarrow" | "xnarrow" | "narrow" | "normal" | "wide" | "xwide" | "xxwide" | "full" | "fit";
10
+ export type WidthVariant = "narrow" | "normal" | "wide" | "full" | "fit" | "1x" | "2x" | "3x" | "4x" | "5x" | "6x" | "7x" | "8x" | "10x" | "12x" | "16x" | "20x" | "24x" | "28x" | "32x";
10
11
  /**
11
12
  * Variant props that set (or unconstrain) a component's width, e.g. `width="narrow"` or `width="12x"`.
12
13
  *
@@ -4,14 +4,9 @@
4
4
  @layer defaults {
5
5
  :root {
6
6
  /* Semantic widths */
7
- --width-xxnarrow: 16rem;
8
- --width-xnarrow: 24rem;
9
7
  --width-narrow: 36rem;
10
8
  --width-normal: 48rem;
11
9
  --width-wide: 80rem;
12
- --width-xwide: 96rem;
13
- --width-xxwide: 112rem;
14
- --width-xxxwide: 128rem;
15
10
  }
16
11
 
17
12
  /* Frame shared by every width value (weak default — a component's own margin can still beat the centering). */
@@ -28,12 +23,6 @@
28
23
  }
29
24
 
30
25
  /* Semantic widths. */
31
- .width-xxnarrow {
32
- --width: var(--width-xxnarrow);
33
- }
34
- .width-xnarrow {
35
- --width: var(--width-xnarrow);
36
- }
37
26
  .width-narrow {
38
27
  --width: var(--width-narrow);
39
28
  }
@@ -43,12 +32,6 @@
43
32
  .width-wide {
44
33
  --width: var(--width-wide);
45
34
  }
46
- .width-xwide {
47
- --width: var(--width-xwide);
48
- }
49
- .width-xxwide {
50
- --width: var(--width-xxwide);
51
- }
52
35
  .width-full {
53
36
  --width: 100%;
54
37
  }
@@ -56,6 +39,53 @@
56
39
  --width: fit-content;
57
40
  }
58
41
 
42
+ /* Numeric widths — multiples of `--space-normal` (no `--width-*` tokens; calculated directly). */
43
+ .width-1x {
44
+ --width: calc(1 * var(--space-normal));
45
+ }
46
+ .width-2x {
47
+ --width: calc(2 * var(--space-normal));
48
+ }
49
+ .width-3x {
50
+ --width: calc(3 * var(--space-normal));
51
+ }
52
+ .width-4x {
53
+ --width: calc(4 * var(--space-normal));
54
+ }
55
+ .width-5x {
56
+ --width: calc(5 * var(--space-normal));
57
+ }
58
+ .width-6x {
59
+ --width: calc(6 * var(--space-normal));
60
+ }
61
+ .width-7x {
62
+ --width: calc(7 * var(--space-normal));
63
+ }
64
+ .width-8x {
65
+ --width: calc(8 * var(--space-normal));
66
+ }
67
+ .width-10x {
68
+ --width: calc(10 * var(--space-normal));
69
+ }
70
+ .width-12x {
71
+ --width: calc(12 * var(--space-normal));
72
+ }
73
+ .width-16x {
74
+ --width: calc(16 * var(--space-normal));
75
+ }
76
+ .width-20x {
77
+ --width: calc(20 * var(--space-normal));
78
+ }
79
+ .width-24x {
80
+ --width: calc(24 * var(--space-normal));
81
+ }
82
+ .width-28x {
83
+ --width: calc(28 * var(--space-normal));
84
+ }
85
+ .width-32x {
86
+ --width: calc(32 * var(--space-normal));
87
+ }
88
+
59
89
  /* Grow modifier — turn the chosen width into a floor and expand to fill (flex items + table columns). */
60
90
  .grow {
61
91
  flex-grow: 1;
@@ -6,10 +6,31 @@ import WIDTH_CSS from "./Width.module.css";
6
6
  * - `narrow` / `normal` / `wide` — fixed widths from the `--width-*` tokens (capped at 100%).
7
7
  * - `full` — take the full available width.
8
8
  * - `fit` — shrink to fit the content's intrinsic width (`fit-content`).
9
+ * - `1x`–`32x` — fixed multiples of `--space-normal` (1x–8x at 1x intervals, then 10x/12x, then 16x–32x at 4x intervals).
9
10
  *
10
11
  * @see https://shelving.cc/ui/WidthVariant
11
12
  */
12
- export type WidthVariant = "xxnarrow" | "xnarrow" | "narrow" | "normal" | "wide" | "xwide" | "xxwide" | "full" | "fit";
13
+ export type WidthVariant =
14
+ | "narrow"
15
+ | "normal"
16
+ | "wide"
17
+ | "full"
18
+ | "fit"
19
+ | "1x"
20
+ | "2x"
21
+ | "3x"
22
+ | "4x"
23
+ | "5x"
24
+ | "6x"
25
+ | "7x"
26
+ | "8x"
27
+ | "10x"
28
+ | "12x"
29
+ | "16x"
30
+ | "20x"
31
+ | "24x"
32
+ | "28x"
33
+ | "32x";
13
34
 
14
35
  /**
15
36
  * Variant props that set (or unconstrain) a component's width, e.g. `width="narrow"` or `width="12x"`.
@@ -1,14 +1,15 @@
1
1
  # getWidthClass
2
2
 
3
- The `width` variant prop sets a component's inline-size — `<Card width="narrow">`, `<Block width="wide">`, `<Cell width="xxnarrow">`. It's an **override** for one-off layout; for an app-wide change, retune the width variables below in a theme file. `<Section>` already defaults to the `--width-normal` width, so most pages never set `width` at all.
3
+ The `width` variant prop sets a component's inline-size — `<Card width="narrow">`, `<Block width="wide">`, `<Cell width="narrow">`. It's an **override** for one-off layout; for an app-wide change, retune the width variables below in a theme file. `<Section>` already defaults to the `--width-normal` width, so most pages never set `width` at all.
4
4
 
5
5
  `getWidthClass({ width, grow })` maps the props to a width class. Every value is capped at 100% of the container.
6
6
 
7
7
  **Values:**
8
8
 
9
- - `xxnarrow` / `xnarrow` / `narrow` / `normal` / `wide` / `xwide` / `xxwide` — fixed widths from the variables below.
9
+ - `narrow` / `normal` / `wide` — fixed widths from the variables below.
10
10
  - `full` — the full available width.
11
11
  - `fit` — shrink to the content's intrinsic width (`fit-content`).
12
+ - `1x` / `2x` / `3x` / `4x` / `5x` / `6x` / `7x` / `8x` / `10x` / `12x` / `16x` / `20x` / `24x` / `28x` / `32x` — fixed multiples of `--space-normal` (no per-step variables; calculated directly).
12
13
 
13
14
  **The `grow` flag** turns the chosen `width` into a floor rather than an exact size: it applies the value as `min-inline-size` and adds `flex-grow: 1`, so the element expands to fill the available space when it's a flex item. Set it on a `<Cell>` (`width="12x" grow`) to give that column a 192px minimum that absorbs the remaining width and keeps the table from collapsing the column on a narrow viewport — cells honour `min-width`, so the table scrolls instead.
14
15
 
@@ -18,11 +19,6 @@ The following `:root` variables are defined by this module and can be overridden
18
19
 
19
20
  | Variable | Default | Used for |
20
21
  |---|---|---
21
- | `--width-xxnarrow` | `16rem` |
22
- | `--width-xnarrow` | `24rem` |
23
- | `--width-narrow` | `36rem` |
22
+ | `--width-narrow` | `36rem` | Focused single-purpose column, e.g. forms and login pages. |
24
23
  | `--width-normal` | `48rem` | Standard content column. |
25
- | `--width-wide` | `80rem` |
26
- | `--width-xwide` | `96rem` |
27
- | `--width-xxwide` | `112rem` |
28
- | `--width-xxxwide` | `128rem` |
24
+ | `--width-wide` | `80rem` | Full dashboard-style content. |
package/ui/table/Table.md CHANGED
@@ -81,7 +81,7 @@ The following table does the same but with a minimum width on the description co
81
81
  <tr>
82
82
  <Cell nowrap>{name}</Cell>
83
83
  <Cell nowrap>{location}</Cell>
84
- <Cell width="xnarrow" grow>{description}</Cell>
84
+ <Cell width="24x" grow>{description}</Cell>
85
85
  </tr>
86
86
  </Table>
87
87
  ```
@@ -1,11 +1,8 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import type { ReactNode } from "react";
3
3
  import { renderToStaticMarkup } from "react-dom/server";
4
- import type { DocumentationElement, TreeElement } from "../../util/tree.js";
5
- import { MetaContext } from "../misc/MetaContext.js";
6
- import { createMeta } from "../util/meta.js";
7
- import { TreeButton } from "./TreeButton.js";
8
- import { TreeProvider } from "./TreeContext.js";
4
+ import { createMeta, MetaContext, TreeButton, TreeProvider } from "shelving/ui";
5
+ import type { DocumentationElement, TreeElement } from "shelving/util/tree";
9
6
 
10
7
  /** Tree with a class member so both flat (`Store.get`) and canonical-path references resolve. */
11
8
  const get: DocumentationElement = {