@hackcove/ui-react 0.9.1 → 0.10.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.
Files changed (60) hide show
  1. package/MIGRATING.md +21 -0
  2. package/README.md +31 -11
  3. package/dist/Avatar.d.ts +12 -5
  4. package/dist/Avatar.js +17 -12
  5. package/dist/AvatarStack.js +2 -1
  6. package/dist/Button.d.ts +15 -1
  7. package/dist/Button.js +17 -6
  8. package/dist/Card.d.ts +8 -1
  9. package/dist/Card.js +15 -6
  10. package/dist/CardParts.d.ts +2 -1
  11. package/dist/CardParts.js +6 -2
  12. package/dist/Checkbox.d.ts +1 -1
  13. package/dist/Checkbox.js +1 -13
  14. package/dist/ColourSchemeToggle.d.ts +4 -2
  15. package/dist/ColourSchemeToggle.js +23 -10
  16. package/dist/Dialog.d.ts +2 -1
  17. package/dist/Dialog.js +44 -3
  18. package/dist/Disclosure.d.ts +10 -1
  19. package/dist/Disclosure.js +13 -3
  20. package/dist/Field.d.ts +9 -1
  21. package/dist/Field.js +5 -3
  22. package/dist/IconButton.d.ts +2 -0
  23. package/dist/IconButton.js +2 -2
  24. package/dist/Image.d.ts +14 -4
  25. package/dist/Image.js +22 -6
  26. package/dist/InlineEdit.d.ts +22 -0
  27. package/dist/InlineEdit.js +60 -0
  28. package/dist/KeyValueList.d.ts +9 -2
  29. package/dist/KeyValueList.js +8 -3
  30. package/dist/Lightbox.d.ts +2 -0
  31. package/dist/Lightbox.js +2 -1
  32. package/dist/ListRow.d.ts +11 -0
  33. package/dist/ListRow.js +28 -6
  34. package/dist/MediaTile.d.ts +11 -3
  35. package/dist/MediaTile.js +10 -7
  36. package/dist/PageLayouts.d.ts +22 -21
  37. package/dist/PageLayouts.js +17 -21
  38. package/dist/SideColumn.d.ts +26 -0
  39. package/dist/SideColumn.js +22 -0
  40. package/dist/SidebarLayout.d.ts +24 -0
  41. package/dist/SidebarLayout.js +22 -0
  42. package/dist/Stack.d.ts +10 -2
  43. package/dist/Stack.js +5 -3
  44. package/dist/StatusPages.d.ts +13 -3
  45. package/dist/StatusPages.js +8 -5
  46. package/dist/Switch.d.ts +42 -0
  47. package/dist/Switch.js +56 -0
  48. package/dist/TableEditCell.d.ts +9 -7
  49. package/dist/TableEditCell.js +12 -53
  50. package/dist/foldedColumn.d.ts +18 -0
  51. package/dist/foldedColumn.js +45 -0
  52. package/dist/index.d.ts +5 -1
  53. package/dist/index.js +5 -1
  54. package/dist/labelWords.d.ts +12 -0
  55. package/dist/labelWords.js +14 -0
  56. package/dist/scrollStop.d.ts +27 -0
  57. package/dist/scrollStop.js +68 -0
  58. package/dist/stickyColumn.d.ts +17 -0
  59. package/dist/stickyColumn.js +17 -0
  60. package/package.json +2 -2
@@ -1,13 +1,15 @@
1
1
  import type { ComponentProps, ReactElement, ReactNode } from "react";
2
2
  import { TableCell } from "./Table.js";
3
3
  /**
4
- * A `TableCell` whose value opens an editor in place. It shows `children` as a button with a dotted
5
- * underline; a click, Enter or Space swaps it for `renderEditor(close)` and moves the focus into the
6
- * editor. The cell keeps its own open state. The editor calls `close` when it has saved, and Esc
7
- * closes it: both put the focus back on the value. A press outside it or Tab out of it closes it too and leaves the
8
- * focus where it went, so the editor saves its value on its own change or submit, never on a close.
9
- * Put the control in the cell itself, not in a popover of its own, which counts as outside. Takes
10
- * `TableCell`'s `align`, `tone`, `numeric`, `mono`, `indent` and `secondary`.
4
+ * A `TableCell` whose value opens an editor in place: a cell that holds one `InlineEdit` and
5
+ * nothing else. It shows `children` as a button with a dotted underline; a click, Enter or Space
6
+ * swaps it for `renderEditor(close)` and moves the focus into the editor. The editor calls `close`
7
+ * when it has saved, and Esc closes it: both put the focus back on the value. A press outside it or
8
+ * Tab out of it closes it too and leaves the focus where it went, so the editor saves its value on
9
+ * its own change or submit, never on a close. Put the control in the cell itself, not in a popover
10
+ * of its own, which counts as outside. Takes `TableCell`'s `align`, `tone`, `numeric`, `mono`,
11
+ * `indent` and `secondary`. For an edit trigger in a cell that holds other content too, put an
12
+ * `InlineEdit` in a `TableCell`.
11
13
  */
12
14
  export declare function TableEditCell({ label, renderEditor, children, ...cell }: {
13
15
  /** What the button does, for a screen reader, heard after the value: "Edit category". It also names the editor. */
@@ -1,58 +1,17 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useCallback, useEffect, useRef, useState } from "react";
3
- import { cx, focusRing } from "./cx.js";
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { InlineEdit } from "./InlineEdit.js";
4
3
  import { TableCell } from "./Table.js";
5
- /** What takes the focus when the editor opens, unless the editor already took it (`autoFocus`). */
6
- const FOCUSABLE = "input, select, textarea, button, a[href]";
7
- /** Whether `target` is a node outside `box`. */
8
- function isOutside(box, target) {
9
- return target instanceof Node && box !== null && !box.contains(target);
10
- }
11
- /** Closes the editor when a press lands outside `box`, for as long as `open`. */
12
- function useDismissOnOutsidePress(box, open, dismiss) {
13
- useEffect(() => {
14
- if (!open)
15
- return undefined;
16
- const onPress = (event) => {
17
- if (isOutside(box.current, event.target))
18
- dismiss();
19
- };
20
- document.addEventListener("pointerdown", onPress);
21
- return () => document.removeEventListener("pointerdown", onPress);
22
- }, [box, open, dismiss]);
23
- }
24
4
  /**
25
- * A `TableCell` whose value opens an editor in place. It shows `children` as a button with a dotted
26
- * underline; a click, Enter or Space swaps it for `renderEditor(close)` and moves the focus into the
27
- * editor. The cell keeps its own open state. The editor calls `close` when it has saved, and Esc
28
- * closes it: both put the focus back on the value. A press outside it or Tab out of it closes it too and leaves the
29
- * focus where it went, so the editor saves its value on its own change or submit, never on a close.
30
- * Put the control in the cell itself, not in a popover of its own, which counts as outside. Takes
31
- * `TableCell`'s `align`, `tone`, `numeric`, `mono`, `indent` and `secondary`.
5
+ * A `TableCell` whose value opens an editor in place: a cell that holds one `InlineEdit` and
6
+ * nothing else. It shows `children` as a button with a dotted underline; a click, Enter or Space
7
+ * swaps it for `renderEditor(close)` and moves the focus into the editor. The editor calls `close`
8
+ * when it has saved, and Esc closes it: both put the focus back on the value. A press outside it or
9
+ * Tab out of it closes it too and leaves the focus where it went, so the editor saves its value on
10
+ * its own change or submit, never on a close. Put the control in the cell itself, not in a popover
11
+ * of its own, which counts as outside. Takes `TableCell`'s `align`, `tone`, `numeric`, `mono`,
12
+ * `indent` and `secondary`. For an edit trigger in a cell that holds other content too, put an
13
+ * `InlineEdit` in a `TableCell`.
32
14
  */
33
15
  export function TableEditCell({ label, renderEditor, children, ...cell }) {
34
- const [phase, setPhase] = useState("closed");
35
- const open = phase === "open";
36
- const box = useRef(null);
37
- const trigger = useRef(null);
38
- const dismiss = useCallback(() => setPhase("closed"), []);
39
- const close = () => setPhase("returning");
40
- useDismissOnOutsidePress(box, open, dismiss);
41
- useEffect(() => {
42
- if (phase === "returning")
43
- trigger.current?.focus();
44
- if (phase === "open" && !box.current?.contains(document.activeElement))
45
- box.current?.querySelector(FOCUSABLE)?.focus();
46
- }, [phase]);
47
- const onKeyDown = (event) => {
48
- if (event.key !== "Escape")
49
- return;
50
- event.preventDefault();
51
- event.stopPropagation();
52
- close();
53
- };
54
- return (_jsx(TableCell, { ...cell, children: open ? (_jsx("div", { ref: box, role: "group", "aria-label": label, className: "-mx-2 -my-1", onKeyDown: onKeyDown, onBlur: (event) => {
55
- if (isOutside(event.currentTarget, event.relatedTarget))
56
- dismiss();
57
- }, children: renderEditor(close) })) : (_jsxs("button", { ref: trigger, type: "button", onClick: () => setPhase("open"), className: cx("-mx-2 -my-1 block w-[calc(100%+1rem)] cursor-pointer rounded-md px-2 py-1 [text-align:inherit] underline decoration-dotted underline-offset-4 transition hover:bg-slate-100", focusRing), children: [children, _jsxs("span", { className: "sr-only", children: [" ", label] })] })) }));
16
+ return (_jsx(TableCell, { ...cell, children: _jsx(InlineEdit, { label: label, renderEditor: renderEditor, children: children }) }));
58
17
  }
@@ -0,0 +1,18 @@
1
+ import { type ReactElement, type ReactNode } from "react";
2
+ /** The breakpoint a folded column opens up at: below it the column sits behind a disclosure. */
3
+ export type FoldFrom = "md" | "lg";
4
+ /**
5
+ * A side column that folds behind a native `<details>` disclosure named `label`, closed, below
6
+ * `from`, and is the same column, always open, from `from` up. The column stays mounted while it is
7
+ * folded, so state inside it is kept. Shared by `SidebarLayout` (`md`) and `SideColumn` (`lg`).
8
+ *
9
+ * From `from` up the CSS shows the content of the closed `<details>` (the first frame, and a page
10
+ * that never hydrates). But a closed `<details>` is hidden to the accessibility tree and to axe, so
11
+ * once mounted an effect also opens it for real from `from` up and closes it again below. There is
12
+ * no `open` prop, so React never fights the browser's toggle and the server markup stays closed.
13
+ */
14
+ export declare function FoldedColumn({ from, label, children, }: {
15
+ from: FoldFrom;
16
+ label: string;
17
+ children: ReactNode;
18
+ }): ReactElement;
@@ -0,0 +1,45 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef } from "react";
3
+ import { cx, focusRing } from "./cx.js";
4
+ // Each variant is written out whole, so Tailwind sees every class.
5
+ const FOLD = {
6
+ md: {
7
+ query: "(width >= 48rem)",
8
+ content: "md:[&::details-content]:[content-visibility:visible]",
9
+ summary: "md:hidden",
10
+ body: "md:mt-0",
11
+ },
12
+ lg: {
13
+ query: "(width >= 64rem)",
14
+ content: "lg:[&::details-content]:[content-visibility:visible]",
15
+ summary: "lg:hidden",
16
+ body: "lg:mt-0",
17
+ },
18
+ };
19
+ /**
20
+ * A side column that folds behind a native `<details>` disclosure named `label`, closed, below
21
+ * `from`, and is the same column, always open, from `from` up. The column stays mounted while it is
22
+ * folded, so state inside it is kept. Shared by `SidebarLayout` (`md`) and `SideColumn` (`lg`).
23
+ *
24
+ * From `from` up the CSS shows the content of the closed `<details>` (the first frame, and a page
25
+ * that never hydrates). But a closed `<details>` is hidden to the accessibility tree and to axe, so
26
+ * once mounted an effect also opens it for real from `from` up and closes it again below. There is
27
+ * no `open` prop, so React never fights the browser's toggle and the server markup stays closed.
28
+ */
29
+ export function FoldedColumn({ from, label, children, }) {
30
+ const fold = FOLD[from];
31
+ const details = useRef(null);
32
+ useEffect(() => {
33
+ const el = details.current;
34
+ if (!el || typeof window.matchMedia !== "function")
35
+ return undefined;
36
+ const wide = window.matchMedia(fold.query);
37
+ const sync = () => {
38
+ el.open = wide.matches;
39
+ };
40
+ sync();
41
+ wide.addEventListener("change", sync);
42
+ return () => wide.removeEventListener("change", sync);
43
+ }, [fold.query]);
44
+ return (_jsxs("details", { ref: details, className: cx("group", fold.content), 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 [&::-webkit-details-marker]:hidden", fold.summary, focusRing), children: [label, _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: cx("mt-2", fold.body), children: children })] }));
45
+ }
package/dist/index.d.ts CHANGED
@@ -10,7 +10,9 @@ 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";
15
+ export { SideColumn, type SideColumnWidth } from "./SideColumn.js";
14
16
  export { StickyBar } from "./StickyBar.js";
15
17
  export { ErrorPage, Forbidden, Maintenance, NotFound, Offline } from "./StatusPages.js";
16
18
  export { Cluster, ClusterGrow, type ClusterAlign, type ClusterGap, type ClusterJustify, } from "./Cluster.js";
@@ -26,6 +28,7 @@ export { Callout, type CalloutTone } from "./Callout.js";
26
28
  export { ListRow, type ListRowProps } from "./ListRow.js";
27
29
  export { OutlineRegion, RegionFrame, type RegionRect } from "./OutlineRegion.js";
28
30
  export { type CellTone, type RowTone, Table, TableBody, TableCell, TableFoot, TableGroupRow, TableHead, TableHeaderCell, TableRow, } from "./Table.js";
31
+ export { InlineEdit } from "./InlineEdit.js";
29
32
  export { TableEditCell } from "./TableEditCell.js";
30
33
  export { EmptyState } from "./EmptyState.js";
31
34
  export { Step, Steps } from "./Steps.js";
@@ -49,6 +52,7 @@ export { Tabs, type Tab, type TabsProps } from "./Tabs.js";
49
52
  export { SegmentedControl, type Segment } from "./SegmentedControl.js";
50
53
  export { FilterGroup } from "./FilterGroup.js";
51
54
  export { Checkbox } from "./Checkbox.js";
55
+ export { Switch } from "./Switch.js";
52
56
  export { Radio, RadioGroup } from "./RadioGroup.js";
53
57
  export { FileDrop } from "./FileDrop.js";
54
58
  export { CopyButton } from "./CopyButton.js";
package/dist/index.js CHANGED
@@ -10,7 +10,9 @@ 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";
15
+ export { SideColumn } from "./SideColumn.js";
14
16
  export { StickyBar } from "./StickyBar.js";
15
17
  export { ErrorPage, Forbidden, Maintenance, NotFound, Offline } from "./StatusPages.js";
16
18
  export { Cluster, ClusterGrow, } from "./Cluster.js";
@@ -26,6 +28,7 @@ export { Callout } from "./Callout.js";
26
28
  export { ListRow } from "./ListRow.js";
27
29
  export { OutlineRegion, RegionFrame } from "./OutlineRegion.js";
28
30
  export { Table, TableBody, TableCell, TableFoot, TableGroupRow, TableHead, TableHeaderCell, TableRow, } from "./Table.js";
31
+ export { InlineEdit } from "./InlineEdit.js";
29
32
  export { TableEditCell } from "./TableEditCell.js";
30
33
  export { EmptyState } from "./EmptyState.js";
31
34
  export { Step, Steps } from "./Steps.js";
@@ -47,6 +50,7 @@ export { Tabs } from "./Tabs.js";
47
50
  export { SegmentedControl } from "./SegmentedControl.js";
48
51
  export { FilterGroup } from "./FilterGroup.js";
49
52
  export { Checkbox } from "./Checkbox.js";
53
+ export { Switch } from "./Switch.js";
50
54
  export { Radio, RadioGroup } from "./RadioGroup.js";
51
55
  export { FileDrop } from "./FileDrop.js";
52
56
  export { CopyButton } from "./CopyButton.js";
@@ -0,0 +1,12 @@
1
+ import { type ReactNode } from "react";
2
+ /** The label, with its muted description under it when there is one. */
3
+ export declare function Words({ label, description, id, }: {
4
+ label: ReactNode;
5
+ description: ReactNode;
6
+ id: string | undefined;
7
+ }): ReactNode;
8
+ /** The description's id (none without a description) and the control's whole `aria-describedby`. */
9
+ export declare function useDescription(description: ReactNode, describedBy: string | undefined): {
10
+ descriptionId: string | undefined;
11
+ describedIds: string;
12
+ };
@@ -0,0 +1,14 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useId } from "react";
3
+ /** The label, with its muted description under it when there is one. */
4
+ export function Words({ label, description, id, }) {
5
+ if (id === undefined)
6
+ return label;
7
+ return (_jsxs("span", { className: "flex flex-col gap-0.5", children: [_jsx("span", { children: label }), _jsx("span", { id: id, className: "text-xs text-slate-500", children: description })] }));
8
+ }
9
+ /** The description's id (none without a description) and the control's whole `aria-describedby`. */
10
+ export function useDescription(description, describedBy) {
11
+ const generatedId = useId();
12
+ const descriptionId = description != null && description !== false ? generatedId : undefined;
13
+ return { descriptionId, describedIds: [describedBy, descriptionId].filter(Boolean).join(" ") };
14
+ }
@@ -0,0 +1,27 @@
1
+ import { type HTMLAttributes } from "react";
2
+ /** Anything the keyboard can reach on its own. */
3
+ export declare const TABBABLE = ":is(a[href], button, input:not([type=\"hidden\"]), select, textarea, summary, iframe, audio[controls], video[controls], [contenteditable]:not([contenteditable=\"false\"]), [tabindex]):not([disabled], [tabindex=\"-1\"])";
4
+ /**
5
+ * The keyboard side of a box that scrolls (a `Dialog`'s body, a sticky column): a box that
6
+ * overflows and holds nothing the keyboard can reach has no way to be scrolled from the keyboard,
7
+ * so it takes a tab stop (axe's `scrollable-region-focusable`). A box that holds a link, a field or
8
+ * a button needs none: the keyboard scrolls it by moving focus inside. `attach` is the box's `ref`;
9
+ * `element` is the box once it is mounted; `stop` is whether it takes the tab stop. Attach it to
10
+ * nothing and `stop` stays false.
11
+ */
12
+ export declare function useScrollStop(): {
13
+ element: HTMLElement | null;
14
+ attach: (el: HTMLDivElement | null) => void;
15
+ stop: boolean;
16
+ };
17
+ /**
18
+ * The props a scroller takes while it holds the tab stop: focusable, ringed and, when the column
19
+ * has a name, a group of that name so a screen reader says what the stop is. Nothing otherwise.
20
+ */
21
+ export declare function scrollStopProps(stop: boolean, name?: {
22
+ label?: string | undefined;
23
+ labelledBy?: string | undefined;
24
+ }): {
25
+ props: HTMLAttributes<HTMLDivElement>;
26
+ ring: string;
27
+ };
@@ -0,0 +1,68 @@
1
+ import { useEffect, useState } from "react";
2
+ import { focusRing } from "./cx.js";
3
+ /** Anything the keyboard can reach on its own. */
4
+ export const TABBABLE = ':is(a[href], button, input:not([type="hidden"]), select, textarea, summary, iframe, audio[controls], video[controls], [contenteditable]:not([contenteditable="false"]), [tabindex]):not([disabled], [tabindex="-1"])';
5
+ /** Whether something in `scroller` is on screen and reachable by the keyboard. */
6
+ function holdsTabbable(scroller) {
7
+ for (const el of scroller.querySelectorAll(TABBABLE)) {
8
+ // A `display: none` summary (a folded column's, from its breakpoint up) is not a tab stop.
9
+ // jsdom (a consumer's own tests) has no `checkVisibility`; there everything counts.
10
+ if (typeof el.checkVisibility !== "function" || el.checkVisibility())
11
+ return true;
12
+ }
13
+ return false;
14
+ }
15
+ /**
16
+ * The keyboard side of a box that scrolls (a `Dialog`'s body, a sticky column): a box that
17
+ * overflows and holds nothing the keyboard can reach has no way to be scrolled from the keyboard,
18
+ * so it takes a tab stop (axe's `scrollable-region-focusable`). A box that holds a link, a field or
19
+ * a button needs none: the keyboard scrolls it by moving focus inside. `attach` is the box's `ref`;
20
+ * `element` is the box once it is mounted; `stop` is whether it takes the tab stop. Attach it to
21
+ * nothing and `stop` stays false.
22
+ */
23
+ export function useScrollStop() {
24
+ const [element, setElement] = useState(null);
25
+ const [stop, setStop] = useState(false);
26
+ useEffect(() => {
27
+ // jsdom (a consumer's own tests) has no ResizeObserver.
28
+ if (!element || typeof ResizeObserver === "undefined")
29
+ return undefined;
30
+ const measure = () => setStop(element.scrollHeight > element.clientHeight && !holdsTabbable(element));
31
+ // Each observer reports once when it starts, and again when the box is resized, its content
32
+ // changes or something in it becomes (un)reachable by the keyboard.
33
+ const resize = new ResizeObserver(measure);
34
+ resize.observe(element);
35
+ for (const child of element.children)
36
+ resize.observe(child);
37
+ const change = new MutationObserver(measure);
38
+ change.observe(element, {
39
+ childList: true,
40
+ subtree: true,
41
+ characterData: true,
42
+ attributeFilter: ["disabled", "tabindex", "href", "type", "contenteditable", "open"],
43
+ });
44
+ return () => {
45
+ resize.disconnect();
46
+ change.disconnect();
47
+ };
48
+ }, [element]);
49
+ return { element, attach: setElement, stop: stop && element !== null };
50
+ }
51
+ /**
52
+ * The props a scroller takes while it holds the tab stop: focusable, ringed and, when the column
53
+ * has a name, a group of that name so a screen reader says what the stop is. Nothing otherwise.
54
+ */
55
+ export function scrollStopProps(stop, name = {}) {
56
+ if (!stop)
57
+ return { props: {}, ring: "" };
58
+ const named = name.label !== undefined || name.labelledBy !== undefined;
59
+ return {
60
+ props: {
61
+ tabIndex: 0,
62
+ ...(named ? { role: "group" } : {}),
63
+ ...(name.label !== undefined ? { "aria-label": name.label } : {}),
64
+ ...(name.labelledBy !== undefined ? { "aria-labelledby": name.labelledBy } : {}),
65
+ },
66
+ ring: focusRing,
67
+ };
68
+ }
@@ -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.10.0",
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",