@hackcove/ui-react 0.9.1 → 0.9.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.
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # @hackcove/ui-react
2
2
 
3
3
  Hack Cove React components, written against Tailwind v4 utilities. They carry no theme: pair them
4
- with a `@hackcove/theme-*` package such as `@hackcove/theme-catppuccin-mocha` and put its scope
4
+ with a theme from `@hackcove/themes`, such as `@hackcove/themes/catppuccin-mocha.css`, and put its scope
5
5
  class on `<html>` (`.dark` for Mocha). With no theme they render in Tailwind's own default colours,
6
6
  a plain light look; it works, but it is not a look Hack Cove designs or checks.
7
7
 
@@ -253,6 +253,9 @@ except the last four.
253
253
  `children` is the list, replaced by `empty` when `isEmpty`.
254
254
  - **`DetailLayout`**: use this when a page is one thing with facts beside it. `children` take two
255
255
  thirds from `lg` and `aside` one third; below `lg` the aside stacks under the main column.
256
+ `stickyAside` (off by default, since an aside of ordinary cards may be taller than the screen)
257
+ keeps the aside in view from `lg` as a long page scrolls: it sticks under the shell's header and
258
+ scrolls itself when it is taller than the room left.
256
259
  - **`FormLayout`**: use this for a create or edit page. A `max-w-3xl` `<form>` that starts at the
257
260
  Container's left edge (it is not re-centred, so the title does not jump), with an `actions` row at
258
261
  the end. Other props (`onSubmit`, `action`) go on the `<form>`.
@@ -271,8 +274,10 @@ except the last four.
271
274
  plain `<AppShell>`, no `bleed`: it sits in the Container like every other layout, so the rail
272
275
  starts at the header logo's x and the canvas ends at the header's right edge. The rail has no fill
273
276
  and no border (pill `Tabs` with `orientation="vertical"` is its content); from `md` it is `md:w-56` beside
274
- the canvas, and below `md` it stacks above it. A long rail sets `railLabel`: below `md` the rail
275
- then folds, closed, behind a disclosure of that name, so a phone shows the page first.
277
+ the canvas, and below `md` it stacks above it. From `md` the rail also stays in view under the
278
+ shell's header as a long page scrolls (always, with or without `railLabel`); a rail taller than
279
+ the screen scrolls itself, so its last link is reachable. A long rail sets `railLabel`: below `md`
280
+ the rail then folds, closed, behind a disclosure of that name, so a phone shows the page first.
276
281
  - **`FullBleedLayout`**: use this for a viewer that needs the whole width. Render it in
277
282
  `<AppShell bleed>`; `children` fill the width, and an optional `panel` takes `lg:w-72` on the
278
283
  right and stacks below under `lg`. It adds no padding, so a viewer fills the width; set `padded`
@@ -12,10 +12,22 @@ export declare function ListLayout({ filters, empty, isEmpty, children, breadcru
12
12
  isEmpty?: boolean;
13
13
  children?: ReactNode;
14
14
  } & Passthrough): ReactElement;
15
- /** A detail page: header, then `children` (two thirds from `lg`) beside `aside` (one third), which stacks below on a smaller screen. */
16
- export declare function DetailLayout({ aside, children, breadcrumbs, title, subtitle, actions, tabs, ...rest }: PageHeaderProps & {
15
+ /**
16
+ * A detail page: header, then `children` (two thirds from `lg`) beside `aside` (one third), which
17
+ * stacks below on a smaller screen. `stickyAside` keeps the aside in view from `lg` as a long page
18
+ * scrolls: it sticks under AppShell's header and, if it is taller than the room left, scrolls
19
+ * itself so its end stays reachable.
20
+ */
21
+ export declare function DetailLayout({ aside, stickyAside, children, breadcrumbs, title, subtitle, actions, tabs, ...rest }: PageHeaderProps & {
17
22
  /** The side column: one third from `lg`, below `children` on a smaller screen. */
18
23
  aside: ReactNode;
24
+ /**
25
+ * From `lg`, keeps the aside in view under the header as the page scrolls; an aside taller than
26
+ * the screen scrolls inside itself. Off by default, since an aside of ordinary cards may be
27
+ * taller than the screen. Below `lg` the aside stacks as ever.
28
+ * @default false
29
+ */
30
+ stickyAside?: boolean | undefined;
19
31
  children: ReactNode;
20
32
  } & Passthrough): ReactElement;
21
33
  /** A form page: header (no header `actions`), then a `max-w-3xl` column that starts at the Container's left edge (it is not re-centred), with an `actions` row at the end. Other props go on the `<form>`. */
@@ -97,23 +109,6 @@ type CardGridProps = ({
97
109
  * screen reader: the grid is a `<ul>` and each child is wrapped in an `<li>`, still stretched.
98
110
  */
99
111
  export declare function CardGrid({ columns, minWidth, align, as: Tag, children, ...rest }: CardGridProps): ReactElement;
100
- /**
101
- * A `rail` of links beside the `children` canvas, in the Container the shell already gives every
102
- * page (a plain `<AppShell>`, no `bleed`): the rail starts at the header logo's x and the canvas
103
- * ends at the header's right edge. The rail has no background and no border, so it is a list of
104
- * links on the page; a `Tabs variant="pill" orientation="vertical"` is its content. From `md` it is `w-56` with
105
- * the canvas filling the rest; below `md` it stacks above the canvas. A long rail sets `railLabel`:
106
- * below `md` it then folds behind a disclosure of that name, closed, so the page comes first on a
107
- * phone, and from `md` it is the same rail. Upgrading from 0.2: the rail no longer has a fill, a
108
- * border or a fixed height, and it no longer sits beside the canvas on a phone.
109
- */
110
- export declare function SidebarLayout({ rail, railLabel, children, ...rest }: {
111
- /** The links beside the canvas, usually a `Tabs variant="pill" orientation="vertical"`. */
112
- rail: ReactNode;
113
- /** Folds the rail behind a disclosure with this name below `md` (a long rail). Left out, the rail is always open. */
114
- railLabel?: string | undefined;
115
- children: ReactNode;
116
- } & Passthrough): ReactElement;
117
112
  /**
118
113
  * Edge-to-edge `children` with an optional right `panel` (`lg:w-72`) that stacks below on a smaller
119
114
  * screen. Render it in `<AppShell bleed>`. It deliberately breaks the shared left edge: a viewer
@@ -1,9 +1,10 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Children, isValidElement, } from "react";
3
3
  import { Card } from "./Card.js";
4
- import { cx, focusRing } from "./cx.js";
4
+ import { cx } from "./cx.js";
5
5
  import { PageHeader, PageHeaderContent } from "./PageHeader.js";
6
6
  import { STACK_WIDTH, Stack } from "./Stack.js";
7
+ import { STICKY_ASIDE_LG, STICKY_SCROLL_LG } from "./stickyColumn.js";
7
8
  import { StickyBar } from "./StickyBar.js";
8
9
  // Compositions of the layout foundation. None sets a page width or outer padding of its own; the
9
10
  // ones that narrow their content do it inside the Container they are rendered in.
@@ -11,9 +12,14 @@ import { StickyBar } from "./StickyBar.js";
11
12
  export function ListLayout({ filters, empty, isEmpty = false, children, breadcrumbs, title, subtitle, actions, tabs, ...rest }) {
12
13
  return (_jsxs(_Fragment, { children: [_jsx(PageHeader, { breadcrumbs: breadcrumbs, title: title, subtitle: subtitle, actions: actions, tabs: tabs }), _jsxs(Stack, { ...rest, children: [filters, isEmpty ? empty : children] })] }));
13
14
  }
14
- /** A detail page: header, then `children` (two thirds from `lg`) beside `aside` (one third), which stacks below on a smaller screen. */
15
- export function DetailLayout({ aside, children, breadcrumbs, title, subtitle, actions, tabs, ...rest }) {
16
- return (_jsxs(_Fragment, { children: [_jsx(PageHeader, { breadcrumbs: breadcrumbs, title: title, subtitle: subtitle, actions: actions, tabs: tabs }), _jsxs("div", { ...rest, className: "grid gap-6 lg:grid-cols-3", children: [_jsx("div", { className: "min-w-0 lg:col-span-2", children: _jsx(Stack, { children: children }) }), _jsx("aside", { className: "min-w-0", children: _jsx(Stack, { children: aside }) })] })] }));
15
+ /**
16
+ * A detail page: header, then `children` (two thirds from `lg`) beside `aside` (one third), which
17
+ * stacks below on a smaller screen. `stickyAside` keeps the aside in view from `lg` as a long page
18
+ * scrolls: it sticks under AppShell's header and, if it is taller than the room left, scrolls
19
+ * itself so its end stays reachable.
20
+ */
21
+ export function DetailLayout({ aside, stickyAside = false, children, breadcrumbs, title, subtitle, actions, tabs, ...rest }) {
22
+ return (_jsxs(_Fragment, { children: [_jsx(PageHeader, { breadcrumbs: breadcrumbs, title: title, subtitle: subtitle, actions: actions, tabs: tabs }), _jsxs("div", { ...rest, className: "grid gap-6 lg:grid-cols-3", children: [_jsx("div", { className: "min-w-0 lg:col-span-2", children: _jsx(Stack, { children: children }) }), _jsx("aside", { className: cx("min-w-0", stickyAside && STICKY_ASIDE_LG), children: _jsx("div", { className: stickyAside ? STICKY_SCROLL_LG : undefined, children: _jsx(Stack, { children: aside }) }) })] })] }));
17
23
  }
18
24
  /** A form page: header (no header `actions`), then a `max-w-3xl` column that starts at the Container's left edge (it is not re-centred), with an `actions` row at the end. Other props go on the `<form>`. */
19
25
  export function FormLayout({ actions, children, breadcrumbs, title, subtitle, tabs, ...form }) {
@@ -66,19 +72,6 @@ export function CardGrid({ columns = 3, minWidth = "md", align = "stretch", as:
66
72
  ? Children.toArray(children).map((child, index) => (_jsx("li", { className: "grid", children: child }, isValidElement(child) ? child.key : index)))
67
73
  : children }));
68
74
  }
69
- /**
70
- * A `rail` of links beside the `children` canvas, in the Container the shell already gives every
71
- * page (a plain `<AppShell>`, no `bleed`): the rail starts at the header logo's x and the canvas
72
- * ends at the header's right edge. The rail has no background and no border, so it is a list of
73
- * links on the page; a `Tabs variant="pill" orientation="vertical"` is its content. From `md` it is `w-56` with
74
- * the canvas filling the rest; below `md` it stacks above the canvas. A long rail sets `railLabel`:
75
- * below `md` it then folds behind a disclosure of that name, closed, so the page comes first on a
76
- * phone, and from `md` it is the same rail. Upgrading from 0.2: the rail no longer has a fill, a
77
- * border or a fixed height, and it no longer sits beside the canvas on a phone.
78
- */
79
- export function SidebarLayout({ rail, railLabel, children, ...rest }) {
80
- return (_jsxs("div", { ...rest, className: "flex flex-col gap-8 md:flex-row md:gap-10", children: [_jsx("aside", { className: "w-full shrink-0 md:w-56", children: railLabel === undefined ? (rail) : (_jsxs("details", { className: "group md:[&::details-content]:[content-visibility:visible]", children: [_jsxs("summary", { className: cx("flex cursor-pointer list-none items-center justify-between 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 md:hidden [&::-webkit-details-marker]:hidden", focusRing), children: [railLabel, _jsx("svg", { viewBox: "0 0 20 20", width: "16", height: "16", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", className: "shrink-0 text-slate-500 group-open:rotate-180", children: _jsx("path", { d: "M5 8l5 5 5-5" }) })] }), _jsx("div", { className: "mt-2 md:mt-0", children: rail })] })) }), _jsx("div", { className: "min-w-0 flex-1", children: children })] }));
81
- }
82
75
  /**
83
76
  * Edge-to-edge `children` with an optional right `panel` (`lg:w-72`) that stacks below on a smaller
84
77
  * screen. Render it in `<AppShell bleed>`. It deliberately breaks the shared left edge: a viewer
@@ -0,0 +1,21 @@
1
+ import type { ReactElement, ReactNode } from "react";
2
+ import type { Passthrough } from "./props.js";
3
+ /**
4
+ * A `rail` of links beside the `children` canvas, in the Container the shell already gives every
5
+ * page (a plain `<AppShell>`, no `bleed`): the rail starts at the header logo's x and the canvas
6
+ * ends at the header's right edge. The rail has no background and no border, so it is a list of
7
+ * links on the page; a `Tabs variant="pill" orientation="vertical"` is its content. From `md` it is `w-56` with
8
+ * the canvas filling the rest, and it stays in view under the shell's header as a long page scrolls;
9
+ * a rail taller than the screen scrolls itself, so its last link is reachable. Below `md` it stacks
10
+ * above the canvas and nothing sticks. A long rail sets `railLabel`:
11
+ * below `md` it then folds behind a disclosure of that name, closed, so the page comes first on a
12
+ * phone, and from `md` it is the same rail. Upgrading from 0.2: the rail no longer has a fill, a
13
+ * border or a fixed height, and it no longer sits beside the canvas on a phone.
14
+ */
15
+ export declare function SidebarLayout({ rail, railLabel, children, ...rest }: {
16
+ /** The links beside the canvas, usually a `Tabs variant="pill" orientation="vertical"`. */
17
+ rail: ReactNode;
18
+ /** Folds the rail behind a disclosure with this name below `md` (a long rail). Left out, the rail is always open. */
19
+ railLabel?: string | undefined;
20
+ children: ReactNode;
21
+ } & Passthrough): ReactElement;
@@ -0,0 +1,18 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cx, focusRing } from "./cx.js";
3
+ import { STICKY_ASIDE_MD, STICKY_SCROLL_MD } from "./stickyColumn.js";
4
+ /**
5
+ * A `rail` of links beside the `children` canvas, in the Container the shell already gives every
6
+ * page (a plain `<AppShell>`, no `bleed`): the rail starts at the header logo's x and the canvas
7
+ * ends at the header's right edge. The rail has no background and no border, so it is a list of
8
+ * links on the page; a `Tabs variant="pill" orientation="vertical"` is its content. From `md` it is `w-56` with
9
+ * the canvas filling the rest, and it stays in view under the shell's header as a long page scrolls;
10
+ * a rail taller than the screen scrolls itself, so its last link is reachable. Below `md` it stacks
11
+ * above the canvas and nothing sticks. A long rail sets `railLabel`:
12
+ * below `md` it then folds behind a disclosure of that name, closed, so the page comes first on a
13
+ * phone, and from `md` it is the same rail. Upgrading from 0.2: the rail no longer has a fill, a
14
+ * border or a fixed height, and it no longer sits beside the canvas on a phone.
15
+ */
16
+ export function SidebarLayout({ rail, railLabel, children, ...rest }) {
17
+ return (_jsxs("div", { ...rest, className: "flex flex-col gap-8 md:flex-row md:gap-10", children: [_jsx("aside", { className: cx("w-full shrink-0 md:w-56", STICKY_ASIDE_MD), children: _jsx("div", { className: STICKY_SCROLL_MD, children: railLabel === undefined ? (rail) : (_jsxs("details", { className: "group md:[&::details-content]:[content-visibility:visible]", children: [_jsxs("summary", { className: cx("flex cursor-pointer list-none items-center justify-between 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 md:hidden [&::-webkit-details-marker]:hidden", focusRing), children: [railLabel, _jsx("svg", { viewBox: "0 0 20 20", width: "16", height: "16", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", className: "shrink-0 text-slate-500 group-open:rotate-180", children: _jsx("path", { d: "M5 8l5 5 5-5" }) })] }), _jsx("div", { className: "mt-2 md:mt-0", children: rail })] })) }) }), _jsx("div", { className: "min-w-0 flex-1", children: children })] }));
18
+ }
package/dist/index.d.ts CHANGED
@@ -10,7 +10,8 @@ export { Markdown } from "./Markdown.js";
10
10
  export { AppShell, AppShellBrand, type AppShellBreakpoint, AppShellHideBelow, appShellRootClassFor, } from "./AppShell.js";
11
11
  export { Container } from "./Container.js";
12
12
  export { PageHeader, type PageHeaderProps } from "./PageHeader.js";
13
- export { CardGrid, CenteredLayout, DetailLayout, FormLayout, FullBleedLayout, ListLayout, ProseLayout, SidebarLayout, } from "./PageLayouts.js";
13
+ export { CardGrid, CenteredLayout, DetailLayout, FormLayout, FullBleedLayout, ListLayout, ProseLayout, } from "./PageLayouts.js";
14
+ export { SidebarLayout } from "./SidebarLayout.js";
14
15
  export { StickyBar } from "./StickyBar.js";
15
16
  export { ErrorPage, Forbidden, Maintenance, NotFound, Offline } from "./StatusPages.js";
16
17
  export { Cluster, ClusterGrow, type ClusterAlign, type ClusterGap, type ClusterJustify, } from "./Cluster.js";
package/dist/index.js CHANGED
@@ -10,7 +10,8 @@ export { Markdown } from "./Markdown.js";
10
10
  export { AppShell, AppShellBrand, AppShellHideBelow, appShellRootClassFor, } from "./AppShell.js";
11
11
  export { Container } from "./Container.js";
12
12
  export { PageHeader } from "./PageHeader.js";
13
- export { CardGrid, CenteredLayout, DetailLayout, FormLayout, FullBleedLayout, ListLayout, ProseLayout, SidebarLayout, } from "./PageLayouts.js";
13
+ export { CardGrid, CenteredLayout, DetailLayout, FormLayout, FullBleedLayout, ListLayout, ProseLayout, } from "./PageLayouts.js";
14
+ export { SidebarLayout } from "./SidebarLayout.js";
14
15
  export { StickyBar } from "./StickyBar.js";
15
16
  export { ErrorPage, Forbidden, Maintenance, NotFound, Offline } from "./StatusPages.js";
16
17
  export { Cluster, ClusterGrow, } from "./Cluster.js";
@@ -0,0 +1,17 @@
1
+ /**
2
+ * A column that stays in view as the page scrolls (`SidebarLayout`'s rail from `md`, `DetailLayout`'s
3
+ * aside from `lg` when asked). The `<aside>` sticks directly under AppShell's header (`--app-header`,
4
+ * as `StickyBar` does; 0 outside a shell) and is as tall as its content. A sticky `top` also pushes a
5
+ * column that starts above it down, so it is exactly the header and no gap: a gap would move a rail
6
+ * rendered at the top of its box. Inside it the scroller is capped at the viewport less the header
7
+ * and 2rem (the room the Container's `py-8` leaves at the page's end, so the column is never pushed
8
+ * up under the header there), so a column taller than that scrolls itself and its end stays
9
+ * reachable. `overflow` clips, which would cut a keyboard focus ring (an outline 4px out) at the
10
+ * scroller's edges, so it is padded 4px (`p-1`) and pulled back out by the same (`-m-1`): the ring
11
+ * has room and the column stays on the page's edges. Each variant is written out whole, so Tailwind
12
+ * sees every class.
13
+ */
14
+ export declare const STICKY_ASIDE_MD = "md:sticky md:top-[var(--app-header,0px)] md:self-start";
15
+ export declare const STICKY_SCROLL_MD = "md:-m-1 md:max-h-[calc(100dvh-var(--app-header,0px)-2rem)] md:overflow-y-auto md:p-1";
16
+ export declare const STICKY_ASIDE_LG = "lg:sticky lg:top-[var(--app-header,0px)] lg:self-start";
17
+ export declare const STICKY_SCROLL_LG = "lg:-m-1 lg:max-h-[calc(100dvh-var(--app-header,0px)-2rem)] lg:overflow-y-auto lg:p-1";
@@ -0,0 +1,17 @@
1
+ /**
2
+ * A column that stays in view as the page scrolls (`SidebarLayout`'s rail from `md`, `DetailLayout`'s
3
+ * aside from `lg` when asked). The `<aside>` sticks directly under AppShell's header (`--app-header`,
4
+ * as `StickyBar` does; 0 outside a shell) and is as tall as its content. A sticky `top` also pushes a
5
+ * column that starts above it down, so it is exactly the header and no gap: a gap would move a rail
6
+ * rendered at the top of its box. Inside it the scroller is capped at the viewport less the header
7
+ * and 2rem (the room the Container's `py-8` leaves at the page's end, so the column is never pushed
8
+ * up under the header there), so a column taller than that scrolls itself and its end stays
9
+ * reachable. `overflow` clips, which would cut a keyboard focus ring (an outline 4px out) at the
10
+ * scroller's edges, so it is padded 4px (`p-1`) and pulled back out by the same (`-m-1`): the ring
11
+ * has room and the column stays on the page's edges. Each variant is written out whole, so Tailwind
12
+ * sees every class.
13
+ */
14
+ export const STICKY_ASIDE_MD = "md:sticky md:top-[var(--app-header,0px)] md:self-start";
15
+ export const STICKY_SCROLL_MD = "md:-m-1 md:max-h-[calc(100dvh-var(--app-header,0px)-2rem)] md:overflow-y-auto md:p-1";
16
+ export const STICKY_ASIDE_LG = "lg:sticky lg:top-[var(--app-header,0px)] lg:self-start";
17
+ export const STICKY_SCROLL_LG = "lg:-m-1 lg:max-h-[calc(100dvh-var(--app-header,0px)-2rem)] lg:overflow-y-auto lg:p-1";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@hackcove/ui-react",
3
- "version": "0.9.1",
4
- "description": "Hack Cove React components: primitives, typography roles and a safe Markdown renderer, written against Tailwind v4 utilities. Pair with a @hackcove/theme-* package.",
3
+ "version": "0.9.2",
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",
7
7
  "design-system",