@hackcove/ui-react 0.10.8 → 0.10.9
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/dist/AppShell.d.ts +14 -2
- package/dist/AppShell.js +18 -10
- package/dist/AppShellMenu.d.ts +8 -1
- package/dist/AppShellMenu.js +16 -5
- package/package.json +1 -1
package/dist/AppShell.d.ts
CHANGED
|
@@ -78,21 +78,33 @@ export declare function AppShellHideBelow({ breakpoint, children, ...rest }: Pas
|
|
|
78
78
|
* reachable and nothing scrolls sideways. The menu is a native `<details>`, so it works with the
|
|
79
79
|
* keyboard, with a screen reader and on a page that never hydrates. From `md` the header is the
|
|
80
80
|
* row, and the `subnav` row, as they are.
|
|
81
|
+
*
|
|
82
|
+
* `controls` stay in the row at every width, so keep them few (a theme toggle, the viewer's avatar);
|
|
83
|
+
* on a phone a crowded `controls` clips, and the Menu button is never the part cut off. Pickers and
|
|
84
|
+
* actions that do not fit a phone row go in `menuControls`: rendered once, they sit in the row after
|
|
85
|
+
* `controls` from `md`, and below `md` at the top of the Menu, above `nav` and `subnav`.
|
|
81
86
|
*/
|
|
82
|
-
export declare function AppShell({ brand, nav, controls, header, subnav, menuLabel, footer, bleed, hideChromeInPrint, children, ...rest }: {
|
|
87
|
+
export declare function AppShell({ brand, nav, controls, menuControls, header, subnav, menuLabel, footer, bleed, hideChromeInPrint, children, ...rest }: {
|
|
83
88
|
/** The brand: a logo and a name, in the header's brand type, with the gap between them. `AppShellBrand` makes both one link. */
|
|
84
89
|
brand?: ReactNode;
|
|
85
90
|
/** The main navigation, e.g. a `Tabs variant="pill"` of links (it is already a `<nav>`). Below `md` it is in the menu. */
|
|
86
91
|
nav?: ReactNode;
|
|
87
92
|
/** The end of the row: a picker, a theme toggle. Gap-2, vertically centred; no child shrinks, and a full-width control (a `Select` at its default) sizes to its content. */
|
|
88
93
|
controls?: ReactNode;
|
|
94
|
+
/**
|
|
95
|
+
* Pickers and actions that do not fit a phone row, such as a project `Select` and a Sign out
|
|
96
|
+
* `Button`. From `md` they sit in the row after `controls`; below `md` they are at the top of the
|
|
97
|
+
* Menu, above `nav` and `subnav`. Rendered once, so a field keeps its state and ids stay unique.
|
|
98
|
+
* `controls` stay in the row at every width, so keep those few.
|
|
99
|
+
*/
|
|
100
|
+
menuControls?: ReactNode;
|
|
89
101
|
/** The escape hatch: a free node in the header row, after `brand` and `nav`. */
|
|
90
102
|
header?: ReactNode;
|
|
91
103
|
/** A second fixed-height row under the header from `md`, e.g. a `Tabs variant="pill"` of links. Below `md` it is in the menu with `nav`. */
|
|
92
104
|
subnav?: ReactNode;
|
|
93
105
|
/**
|
|
94
106
|
* The phone menu button's name, for an app in another language. Only drawn below `md`, and only
|
|
95
|
-
* when `nav` or `
|
|
107
|
+
* when `nav`, `subnav` or `menuControls` is given.
|
|
96
108
|
* @default Menu
|
|
97
109
|
*/
|
|
98
110
|
menuLabel?: string;
|
package/dist/AppShell.js
CHANGED
|
@@ -62,17 +62,20 @@ function ScrollRow({ className, children }) {
|
|
|
62
62
|
return (_jsx(ContainerFrame, { elementRef: setRow, className: cx(ROW, FADE, className), children: children }));
|
|
63
63
|
}
|
|
64
64
|
/**
|
|
65
|
-
* The header row's slots in order: brand, nav (from `md`), the free `children`, then controls and
|
|
66
|
-
*
|
|
65
|
+
* The header row's slots in order: brand, nav (from `md`), the free `children`, then `controls` and
|
|
66
|
+
* the menu at the end. `controls` have a wrapper of their own that shrinks and clips, and the menu
|
|
67
|
+
* sits beside it, so a crowded `controls` never pushes the Menu off a phone screen. From `md` the
|
|
68
|
+
* menu is the inline row of `menuControls` after `controls`.
|
|
67
69
|
*/
|
|
68
|
-
function HeaderSlots({ brand, nav, controls, menu, children, }) {
|
|
69
|
-
return (_jsxs(_Fragment, { children: [brand && (_jsx("div", { className: "flex items-center gap-2 text-base font-bold tracking-tight text-slate-900", children: brand })), nav && _jsx("div", { className: "hidden items-center gap-4 *:shrink-0 md:flex", children: nav }), children, (controls || menu) && (_jsxs("div", { className:
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
function HeaderSlots({ brand, nav, controls, menu, menuInline, children, }) {
|
|
71
|
+
return (_jsxs(_Fragment, { children: [brand && (_jsx("div", { className: "flex items-center gap-2 text-base font-bold tracking-tight text-slate-900", children: brand })), nav && _jsx("div", { className: "hidden items-center gap-4 *:shrink-0 md:flex", children: nav }), children, (controls || menu) && (_jsxs("div", { className: cx(
|
|
72
|
+
// Below `md` this wrapper may shrink (the row's `*:shrink-0` is overridden), so a crowded
|
|
73
|
+
// `controls` clips inside its own box; from `md` it keeps its width and the row scrolls.
|
|
74
|
+
"ml-auto flex min-w-0 items-center gap-2 max-md:shrink!", !controls && !menuInline && "md:hidden"), children: [controls && (_jsx("div", { className: "flex min-w-0 items-center gap-2 *:shrink-0 max-md:-mx-1 max-md:overflow-x-clip max-md:px-1 [&>.w-full]:w-auto", children: controls })), menu] }))] }));
|
|
72
75
|
}
|
|
73
76
|
/** The sticky header: the one-row slots, and from `md` the `subnav` row under them. */
|
|
74
|
-
function AppShellHeader({ brand, nav, controls, subnav, menuLabel, hideChromeInPrint, children, }) {
|
|
75
|
-
return (_jsxs("header", { className: cx("sticky top-0 z-10 border-b border-slate-200 bg-white", hideChromeInPrint && "print:hidden"), children: [_jsx(ScrollRow, { className: "flex h-14", children: _jsx(HeaderSlots, { brand: brand, nav: nav, controls: controls, menu: (nav || subnav) && (_jsxs(AppShellMenu, { label: menuLabel, children: [nav, subnav] })), children: children }) }), subnav && _jsx(ScrollRow, { className: "hidden h-12 md:flex", children: subnav })] }));
|
|
77
|
+
function AppShellHeader({ brand, nav, controls, menuControls, subnav, menuLabel, hideChromeInPrint, children, }) {
|
|
78
|
+
return (_jsxs("header", { className: cx("sticky top-0 z-10 border-b border-slate-200 bg-white", hideChromeInPrint && "print:hidden"), children: [_jsx(ScrollRow, { className: "flex h-14", children: _jsx(HeaderSlots, { brand: brand, nav: nav, controls: controls, menuInline: !!menuControls, menu: (nav || subnav || menuControls) && (_jsxs(AppShellMenu, { label: menuLabel, controls: menuControls, children: [nav, subnav] })), children: children }) }), subnav && _jsx(ScrollRow, { className: "hidden h-12 md:flex", children: subnav })] }));
|
|
76
79
|
}
|
|
77
80
|
/**
|
|
78
81
|
* The page frame: it paints the page canvas and sets the type face, ink and font smoothing
|
|
@@ -93,10 +96,15 @@ function AppShellHeader({ brand, nav, controls, subnav, menuLabel, hideChromeInP
|
|
|
93
96
|
* reachable and nothing scrolls sideways. The menu is a native `<details>`, so it works with the
|
|
94
97
|
* keyboard, with a screen reader and on a page that never hydrates. From `md` the header is the
|
|
95
98
|
* row, and the `subnav` row, as they are.
|
|
99
|
+
*
|
|
100
|
+
* `controls` stay in the row at every width, so keep them few (a theme toggle, the viewer's avatar);
|
|
101
|
+
* on a phone a crowded `controls` clips, and the Menu button is never the part cut off. Pickers and
|
|
102
|
+
* actions that do not fit a phone row go in `menuControls`: rendered once, they sit in the row after
|
|
103
|
+
* `controls` from `md`, and below `md` at the top of the Menu, above `nav` and `subnav`.
|
|
96
104
|
*/
|
|
97
|
-
export function AppShell({ brand, nav, controls, header, subnav, menuLabel = "Menu", footer, bleed = false, hideChromeInPrint = false, children, ...rest }) {
|
|
105
|
+
export function AppShell({ brand, nav, controls, menuControls, header, subnav, menuLabel = "Menu", footer, bleed = false, hideChromeInPrint = false, children, ...rest }) {
|
|
98
106
|
return (_jsxs("div", { ...rest, className: cx("flex min-h-dvh flex-col bg-gray-50 font-sans text-slate-900 antialiased",
|
|
99
107
|
// The sticky header's height (its rows and the bottom border), for a bar that sticks under it.
|
|
100
108
|
// Below `md` it is one row whatever the slots are: the subnav is in the menu.
|
|
101
|
-
"[--app-header:calc(3.5rem+1px)]", subnav ? "md:[--app-header:calc(6.5rem+1px)]" : undefined), children: [_jsx(AppShellHeader, { brand: brand, nav: nav, controls: controls, subnav: subnav, menuLabel: menuLabel, hideChromeInPrint: hideChromeInPrint, children: header }), _jsx("main", { className: "flex flex-1 flex-col", children: bleed ? (children) : (_jsx(ContainerFrame, { className: cx("py-8", hideChromeInPrint && "print:p-0"), children: children })) }), footer && (_jsx("footer", { className: cx("border-t border-slate-200", hideChromeInPrint && "print:hidden"), children: _jsx(ContainerFrame, { className: "py-6 text-sm text-slate-500", children: footer }) }))] }));
|
|
109
|
+
"[--app-header:calc(3.5rem+1px)]", subnav ? "md:[--app-header:calc(6.5rem+1px)]" : undefined), children: [_jsx(AppShellHeader, { brand: brand, nav: nav, controls: controls, menuControls: menuControls, subnav: subnav, menuLabel: menuLabel, hideChromeInPrint: hideChromeInPrint, children: header }), _jsx("main", { className: "flex flex-1 flex-col", children: bleed ? (children) : (_jsx(ContainerFrame, { className: cx("py-8", hideChromeInPrint && "print:p-0"), children: children })) }), footer && (_jsx("footer", { className: cx("border-t border-slate-200", hideChromeInPrint && "print:hidden"), children: _jsx(ContainerFrame, { className: "py-6 text-sm text-slate-500", children: footer }) }))] }));
|
|
102
110
|
}
|
package/dist/AppShellMenu.d.ts
CHANGED
|
@@ -6,8 +6,15 @@ import { type ReactElement, type ReactNode } from "react";
|
|
|
6
6
|
* is out of flow (it hangs under the header, `top-full`), so the header keeps its one height open
|
|
7
7
|
* or closed. With script it also closes on Escape (focus returns to the button) and when a link in
|
|
8
8
|
* it is followed, for a router that changes the page without a reload.
|
|
9
|
+
*
|
|
10
|
+
* `controls` (AppShell's `menuControls`) sit at the top of the panel, above the links. From `md`
|
|
11
|
+
* the `<details>` drops its own box and shows only them, inline at the row's end, whether it is
|
|
12
|
+
* open or not: the summary and the link copies are hidden (the links render in their own rows from
|
|
13
|
+
* `md`) and the panel is a plain row. They are rendered once, so a field keeps its state.
|
|
9
14
|
*/
|
|
10
|
-
export declare function AppShellMenu({ label, children, }: {
|
|
15
|
+
export declare function AppShellMenu({ label, controls, children, }: {
|
|
11
16
|
label: string;
|
|
17
|
+
/** Rendered once: at the top of the panel below `md`, inline in the row from `md`. */
|
|
18
|
+
controls?: ReactNode;
|
|
12
19
|
children: ReactNode;
|
|
13
20
|
}): ReactElement;
|
package/dist/AppShellMenu.js
CHANGED
|
@@ -21,8 +21,13 @@ const ICON = {
|
|
|
21
21
|
* is out of flow (it hangs under the header, `top-full`), so the header keeps its one height open
|
|
22
22
|
* or closed. With script it also closes on Escape (focus returns to the button) and when a link in
|
|
23
23
|
* it is followed, for a router that changes the page without a reload.
|
|
24
|
+
*
|
|
25
|
+
* `controls` (AppShell's `menuControls`) sit at the top of the panel, above the links. From `md`
|
|
26
|
+
* the `<details>` drops its own box and shows only them, inline at the row's end, whether it is
|
|
27
|
+
* open or not: the summary and the link copies are hidden (the links render in their own rows from
|
|
28
|
+
* `md`) and the panel is a plain row. They are rendered once, so a field keeps its state.
|
|
24
29
|
*/
|
|
25
|
-
export function AppShellMenu({ label, children, }) {
|
|
30
|
+
export function AppShellMenu({ label, controls, children, }) {
|
|
26
31
|
const details = useRef(null);
|
|
27
32
|
const close = (focusButton) => {
|
|
28
33
|
const el = details.current;
|
|
@@ -32,11 +37,17 @@ export function AppShellMenu({ label, children, }) {
|
|
|
32
37
|
if (focusButton)
|
|
33
38
|
el.querySelector("summary")?.focus();
|
|
34
39
|
};
|
|
35
|
-
return (_jsxs("details", { ref: details, className: "group
|
|
36
|
-
|
|
40
|
+
return (_jsxs("details", { ref: details, className: cx("group shrink-0", controls
|
|
41
|
+
? "md:contents md:[&::details-content]:contents md:[&::details-content]:[content-visibility:visible]"
|
|
42
|
+
: "md:hidden"), onKeyDown: (event) => {
|
|
43
|
+
// A dialog opened from a control in the menu takes its own Escape; the menu stays open.
|
|
44
|
+
if (event.key === "Escape" &&
|
|
45
|
+
!(event.target instanceof Element && event.target.closest("dialog")))
|
|
37
46
|
close(true);
|
|
38
|
-
}, children: [_jsxs("summary", { className: cx("flex cursor-pointer list-none items-center gap-2 rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm font-medium text-slate-900 select-none hover:bg-slate-50 [&::-webkit-details-marker]:hidden", focusRing), children: [_jsx("svg", { ...ICON, className: cx(ICON.className, "group-open:hidden"), children: _jsx("path", { d: "M3 5.5h14M3 10h14M3 14.5h14" }) }), _jsx("svg", { ...ICON, className: cx(ICON.className, "hidden group-open:block"), children: _jsx("path", { d: "M5 5l10 10M15 5L5 15" }) }), label] }), _jsx("div", {
|
|
47
|
+
}, children: [_jsxs("summary", { className: cx("flex cursor-pointer list-none md:hidden items-center gap-2 rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm font-medium text-slate-900 select-none hover:bg-slate-50 [&::-webkit-details-marker]:hidden", focusRing), children: [_jsx("svg", { ...ICON, className: cx(ICON.className, "group-open:hidden"), children: _jsx("path", { d: "M3 5.5h14M3 10h14M3 14.5h14" }) }), _jsx("svg", { ...ICON, className: cx(ICON.className, "hidden group-open:block"), children: _jsx("path", { d: "M5 5l10 10M15 5L5 15" }) }), label] }), _jsx("div", {
|
|
48
|
+
// From `md` the panel draws no box (`contents`), so its dropdown look applies below `md` only.
|
|
49
|
+
className: "absolute inset-x-0 top-full max-h-[calc(100dvh-var(--app-header))] overflow-y-auto overscroll-contain border-y border-slate-200 bg-white shadow-lg md:contents", onClick: (event) => {
|
|
39
50
|
if (event.target instanceof Element && event.target.closest("a[href]"))
|
|
40
51
|
close(false);
|
|
41
|
-
}, children:
|
|
52
|
+
}, children: _jsxs(ContainerFrame, { className: "flex flex-col gap-3 py-3 md:contents", children: [controls && (_jsx("div", { className: "flex flex-wrap items-center gap-2 md:flex-nowrap md:*:shrink-0 md:[&>.w-full]:w-auto", children: controls })), _jsx("div", { className: "flex flex-col gap-3 md:hidden", children: children })] }) })] }));
|
|
42
53
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hackcove/ui-react",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.9",
|
|
4
4
|
"description": "Hack Cove React components: primitives, typography roles and a safe Markdown renderer, written against Tailwind v4 utilities. Pair with a theme from @hackcove/themes.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"components",
|