@parallelworks/ui 0.59.0 → 0.61.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -1
- package/dist/access/AccessDrawer.js +1 -1
- package/dist/access/PrincipalPicker.js +1 -1
- package/dist/ai/ui/ConversationSidebar.js +1 -1
- package/dist/components/Breadcrumbs.d.ts +33 -0
- package/dist/components/Breadcrumbs.js +203 -0
- package/dist/components/ColorInput.d.ts +3 -1
- package/dist/components/ColorInput.js +2 -1
- package/dist/components/Input.d.ts +3 -1
- package/dist/components/Input.js +2 -1
- package/dist/components/PageHeader.d.ts +2 -2
- package/dist/components/PageHeader.js +6 -145
- package/dist/components/Provider.d.ts +3 -1
- package/dist/components/Provider.js +1 -0
- package/dist/components/ThemeEditor.js +10 -0
- package/dist/components/pageHeaderStore.d.ts +1 -6
- package/dist/file-explorer/FileExplorer.js +13 -33
- package/dist/file-explorer/helpers.js +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +3 -2
- package/dist/list/ListTable.js +1 -1
- package/dist/list/UserHoverCard.js +1 -1
- package/dist/styles.css +1 -1
- package/dist/theme/derive.js +12 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -74,6 +74,7 @@ Each need has one component. Reach for it before styling markup by hand.
|
|
|
74
74
|
| Free text with suggestions, or tags | `Autocomplete`; `Combobox` with `multiple creatable` |
|
|
75
75
|
| A range | `Slider` |
|
|
76
76
|
| A page's breadcrumbs, title and commands | `PageHeader`, shown by the shell's `PageHeaderBar` (see [Page header](#page-header)) |
|
|
77
|
+
| A trail anywhere else, such as a folder's path in a panel | `Breadcrumbs`, which folds as the header's trail does |
|
|
77
78
|
| An action in the page header | `PageHeaderAction`; `PageHeaderAddButton` for "+ Add" or "+ Create"; `PageHeaderMenu` for the "…" menu |
|
|
78
79
|
| A titled part of a page or form | `Section` |
|
|
79
80
|
| More detail under a line | `Disclosure` |
|
|
@@ -173,7 +174,12 @@ A page declares its header in one place, and the shell shows it:
|
|
|
173
174
|
trail is a list in a `nav` landmark named by the strings' `breadcrumb`. A
|
|
174
175
|
trail too wide for its bar folds its ancestors into a "…" menu of links
|
|
175
176
|
(named by `showFullPath`), keeping the parent when it fits beside the page's
|
|
176
|
-
name; the name truncates last, with the full name on hover.
|
|
177
|
+
name; the name truncates last, with the full name on hover. The bar draws it
|
|
178
|
+
with `Breadcrumbs`, which renders the same trail anywhere else: in a flex
|
|
179
|
+
row it takes the room the row's other items leave it, and elsewhere its
|
|
180
|
+
container's width. `current="location"` marks a place within the page, such
|
|
181
|
+
as the open folder, and `heading` makes the last crumb the page's `h1`, as
|
|
182
|
+
the bar does.
|
|
177
183
|
- **`titleActions`** sit beside the title, for actions on the page's subject.
|
|
178
184
|
**`actions`** sit at the end of the bar, for the page's commands: a detail
|
|
179
185
|
page's Start, Stop or Terminal go here, not in a row of buttons below it.
|
|
@@ -479,6 +485,7 @@ where it opens elsewhere, and renders it through `AppLink`:
|
|
|
479
485
|
| `Button`, `IconButton`, `Card` | `href` |
|
|
480
486
|
| Menu rows, `RowAction`s (a list row's menu and pinned buttons), `ToolbarAction`s | `href` on the action, with `target: '_blank'` for a new tab |
|
|
481
487
|
| `PageHeaderAction`, `PageHeaderAddButton`, `PageHeader` breadcrumbs | `href`; `external` opens a new tab |
|
|
488
|
+
| `Breadcrumbs` | `href` on the crumb; `onSelect` only for a place with no URL, as the explorer's trail without `pathHref` |
|
|
482
489
|
| `Tabs` that are pages, `Pagination` | `href` on the item, `pageHref` |
|
|
483
490
|
| A list row | `ListRow` `href` with a `NameCell` of the same `href`: the name's link covers the row |
|
|
484
491
|
| A toast's action | `action: { label, href }` |
|
|
@@ -2,10 +2,10 @@ import { AddIcon, LockIcon, UsersIcon } from "../icons.js";
|
|
|
2
2
|
import { toast } from "../components/Toast.js";
|
|
3
3
|
import { EmptyState } from "../components/EmptyState.js";
|
|
4
4
|
import { useQueryMatch } from "../components/search.js";
|
|
5
|
+
import { LoadingRegion, Skeleton } from "../components/Skeleton.js";
|
|
5
6
|
import { Button } from "../components/Button.js";
|
|
6
7
|
import { Callout } from "../components/Callout.js";
|
|
7
8
|
import { Drawer } from "../components/Drawer.js";
|
|
8
|
-
import { LoadingRegion, Skeleton } from "../components/Skeleton.js";
|
|
9
9
|
import { SearchInput } from "../components/SearchInput.js";
|
|
10
10
|
import { SegmentedControl } from "../components/SegmentedControl.js";
|
|
11
11
|
import { buildModel, invert, key, principalsWithAccess, warnAboutMisuse } from "./model.js";
|
|
@@ -3,9 +3,9 @@ import { OrganizationIcon, RefreshIcon, UsersIcon } from "../icons.js";
|
|
|
3
3
|
import { useQueryMatch } from "../components/search.js";
|
|
4
4
|
import { categoryClasses } from "../components/category.js";
|
|
5
5
|
import { Avatar } from "../components/Avatar.js";
|
|
6
|
+
import { Skeleton } from "../components/Skeleton.js";
|
|
6
7
|
import { Callout } from "../components/Callout.js";
|
|
7
8
|
import { Combobox } from "../components/Combobox.js";
|
|
8
|
-
import { Skeleton } from "../components/Skeleton.js";
|
|
9
9
|
import { useRelativeTime } from "../components/relativeTime.js";
|
|
10
10
|
import { StatusBadge } from "../components/StatusBadge.js";
|
|
11
11
|
import { covers, isOwner } from "./model.js";
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { cx } from "../../cx.js";
|
|
2
2
|
import { positionKeys } from "../../components/keys.js";
|
|
3
3
|
import { usePersistedState } from "../../components/Provider.js";
|
|
4
|
+
import { LoadingRegion, Skeleton } from "../../components/Skeleton.js";
|
|
4
5
|
import { useSplitter } from "../../components/splitter.js";
|
|
5
6
|
import { Drawer } from "../../components/Drawer.js";
|
|
6
|
-
import { LoadingRegion, Skeleton } from "../../components/Skeleton.js";
|
|
7
7
|
import { SidebarGroupHeading, SidebarPanel, SidebarToggle } from "./sidebar.js";
|
|
8
8
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
9
9
|
import { Fragment as Fragment$1, useEffect, useRef } from "react";
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** One step of the trail to the current page. */
|
|
2
|
+
export interface Breadcrumb {
|
|
3
|
+
/** Empty while the name is still loading: the trail shows a placeholder. */
|
|
4
|
+
label: string;
|
|
5
|
+
href?: string | undefined;
|
|
6
|
+
}
|
|
7
|
+
export interface BreadcrumbsProps {
|
|
8
|
+
/** The trail, ending with where the reader is. A crumb with an empty label,
|
|
9
|
+
* such as a name still loading, shows a placeholder. */
|
|
10
|
+
crumbs: readonly Breadcrumb[];
|
|
11
|
+
/** Goes to a crumb that has no `href`: a place with no URL, such as a folder
|
|
12
|
+
* in a view that keeps its path out of the URL. A crumb with a URL takes
|
|
13
|
+
* `href` instead, so it opens in a new tab. Without either, a crumb is
|
|
14
|
+
* text. */
|
|
15
|
+
onSelect?: ((crumb: Breadcrumb, index: number) => void) | undefined;
|
|
16
|
+
/** What the last crumb is: the page itself, or a place within the page, such
|
|
17
|
+
* as the open folder. */
|
|
18
|
+
current?: 'page' | 'location' | undefined;
|
|
19
|
+
/** Makes the last crumb the page's `h1`, as `PageHeader` does. A page has
|
|
20
|
+
* one. */
|
|
21
|
+
heading?: boolean | undefined;
|
|
22
|
+
/** Names the landmark; the strings' `breadcrumb` by default. */
|
|
23
|
+
label?: string | undefined;
|
|
24
|
+
className?: string | undefined;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The trail to where the reader is, as a list in a `nav` landmark. A trail too
|
|
28
|
+
* wide for its room folds its ancestors into a "…" menu, keeping the parent
|
|
29
|
+
* when it fits beside the last crumb, which truncates last, with its full name
|
|
30
|
+
* on hover. In a flex row its room is what the row's other items leave it;
|
|
31
|
+
* elsewhere it fills its container's width.
|
|
32
|
+
*/
|
|
33
|
+
export declare function Breadcrumbs({ crumbs, onSelect, current, heading, label, className, }: BreadcrumbsProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { cx } from "../cx.js";
|
|
2
|
+
import { buttonClasses } from "./buttonClasses.js";
|
|
3
|
+
import { ChevronRightIcon, MoreIcon } from "../icons.js";
|
|
4
|
+
import { keyedByContent } from "./keys.js";
|
|
5
|
+
import { Tooltip } from "./Tooltip.js";
|
|
6
|
+
import { useStrings } from "./Provider.js";
|
|
7
|
+
import { AppLink } from "./AppLink.js";
|
|
8
|
+
import { ActionMenu } from "./ActionMenu.js";
|
|
9
|
+
import { Skeleton } from "./Skeleton.js";
|
|
10
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
11
|
+
import { useLayoutEffect, useState } from "react";
|
|
12
|
+
//#region src/components/Breadcrumbs.tsx
|
|
13
|
+
/**
|
|
14
|
+
* The trail to where the reader is, as a list in a `nav` landmark. A trail too
|
|
15
|
+
* wide for its room folds its ancestors into a "…" menu, keeping the parent
|
|
16
|
+
* when it fits beside the last crumb, which truncates last, with its full name
|
|
17
|
+
* on hover. In a flex row its room is what the row's other items leave it;
|
|
18
|
+
* elsewhere it fills its container's width.
|
|
19
|
+
*/
|
|
20
|
+
function Breadcrumbs({ crumbs, onSelect, current = "page", heading = false, label, className }) {
|
|
21
|
+
const { common } = useStrings();
|
|
22
|
+
const [nav, setNav] = useState(null);
|
|
23
|
+
const [natural, setNatural] = useState(null);
|
|
24
|
+
const fit = useTrailFit(nav, natural);
|
|
25
|
+
const last = crumbs[crumbs.length - 1];
|
|
26
|
+
const ancestors = crumbs.slice(0, -1);
|
|
27
|
+
const folded = fit === "none" ? ancestors : fit === "parent" ? ancestors.slice(0, -1) : [];
|
|
28
|
+
const shown = ancestors.slice(folded.length);
|
|
29
|
+
const Last = heading ? "h1" : "span";
|
|
30
|
+
const menuItem = (crumb, index) => {
|
|
31
|
+
const name = crumb.label || common.loading;
|
|
32
|
+
if (crumb.href) return {
|
|
33
|
+
label: name,
|
|
34
|
+
href: crumb.href
|
|
35
|
+
};
|
|
36
|
+
if (onSelect) return {
|
|
37
|
+
label: name,
|
|
38
|
+
onClick: () => onSelect(crumb, index)
|
|
39
|
+
};
|
|
40
|
+
return {
|
|
41
|
+
label: name,
|
|
42
|
+
disabled: true,
|
|
43
|
+
onClick: () => {}
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
return /* @__PURE__ */ jsxs("nav", {
|
|
47
|
+
ref: setNav,
|
|
48
|
+
"aria-label": label ?? common.breadcrumb,
|
|
49
|
+
className: cx("relative min-w-0", className),
|
|
50
|
+
children: [/* @__PURE__ */ jsxs("ol", {
|
|
51
|
+
className: "m-0 flex min-w-0 list-none items-center gap-1.5 p-0 text-[13px] leading-none",
|
|
52
|
+
children: [
|
|
53
|
+
folded.length > 0 && /* @__PURE__ */ jsxs("li", {
|
|
54
|
+
className: "flex shrink-0 items-center gap-1.5",
|
|
55
|
+
children: [/* @__PURE__ */ jsx(ActionMenu, {
|
|
56
|
+
label: common.showFullPath,
|
|
57
|
+
align: "start",
|
|
58
|
+
items: folded.map(menuItem),
|
|
59
|
+
triggerClassName: buttonClasses({
|
|
60
|
+
variant: "ghost",
|
|
61
|
+
size: "xs",
|
|
62
|
+
iconOnly: true
|
|
63
|
+
}),
|
|
64
|
+
children: /* @__PURE__ */ jsx(MoreIcon, {})
|
|
65
|
+
}), /* @__PURE__ */ jsx(Separator, {})]
|
|
66
|
+
}),
|
|
67
|
+
keyedByContent(shown, (c) => c.href ?? c.label).map(({ key, item: crumb }, i) => {
|
|
68
|
+
const index = folded.length + i;
|
|
69
|
+
return /* @__PURE__ */ jsxs("li", {
|
|
70
|
+
className: cx("flex items-center gap-1.5", folded.length > 0 ? "min-w-0 max-w-36 shrink-0" : "min-w-0"),
|
|
71
|
+
children: [crumb.label === "" ? /* @__PURE__ */ jsx(CrumbPlaceholder, {}) : /* @__PURE__ */ jsx(Tooltip, {
|
|
72
|
+
content: crumb.label,
|
|
73
|
+
overflowOnly: true,
|
|
74
|
+
children: crumb.href ? /* @__PURE__ */ jsx(AppLink, {
|
|
75
|
+
href: crumb.href,
|
|
76
|
+
className: cx(crumbText, crumbLink, "max-w-48"),
|
|
77
|
+
children: crumb.label
|
|
78
|
+
}) : onSelect ? /* @__PURE__ */ jsx("button", {
|
|
79
|
+
type: "button",
|
|
80
|
+
onClick: () => onSelect(crumb, index),
|
|
81
|
+
className: cx(crumbText, crumbLink, "max-w-48 cursor-pointer text-start"),
|
|
82
|
+
children: crumb.label
|
|
83
|
+
}) : /* @__PURE__ */ jsx("span", {
|
|
84
|
+
className: cx(crumbText, "max-w-48"),
|
|
85
|
+
children: crumb.label
|
|
86
|
+
})
|
|
87
|
+
}), /* @__PURE__ */ jsx(Separator, {})]
|
|
88
|
+
}, key);
|
|
89
|
+
}),
|
|
90
|
+
last && /* @__PURE__ */ jsx("li", {
|
|
91
|
+
className: "flex min-w-0 items-center",
|
|
92
|
+
children: last.label === "" ? /* @__PURE__ */ jsx(Last, {
|
|
93
|
+
"aria-current": current,
|
|
94
|
+
className: "m-0",
|
|
95
|
+
children: /* @__PURE__ */ jsx(CrumbPlaceholder, {})
|
|
96
|
+
}) : /* @__PURE__ */ jsx(Tooltip, {
|
|
97
|
+
content: last.label,
|
|
98
|
+
overflowOnly: true,
|
|
99
|
+
children: /* @__PURE__ */ jsx(Last, {
|
|
100
|
+
"aria-current": current,
|
|
101
|
+
className: "m-0 block truncate text-[13px] font-medium leading-none text-(--theme-app)",
|
|
102
|
+
children: last.label
|
|
103
|
+
})
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
]
|
|
107
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
108
|
+
"aria-hidden": true,
|
|
109
|
+
className: "pointer-events-none invisible absolute inset-0 overflow-hidden",
|
|
110
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
111
|
+
ref: setNatural,
|
|
112
|
+
className: "flex w-max items-center gap-1.5 whitespace-nowrap text-[13px] leading-none",
|
|
113
|
+
children: crumbs.map((crumb, i) => /* @__PURE__ */ jsxs("span", {
|
|
114
|
+
className: "flex items-center gap-1.5",
|
|
115
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
116
|
+
className: cx(i < crumbs.length - 1 && "max-w-48 truncate", i === crumbs.length - 1 && "font-medium"),
|
|
117
|
+
children: crumb.label || " ".repeat(6)
|
|
118
|
+
}), i < crumbs.length - 1 && /* @__PURE__ */ jsx(Separator, {})]
|
|
119
|
+
}, i))
|
|
120
|
+
})
|
|
121
|
+
})]
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
var crumbText = "block truncate text-(--theme-muted-text-color)";
|
|
125
|
+
var crumbLink = "rounded-sm transition-colors hover:text-(--theme-app) focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-(--theme-link)";
|
|
126
|
+
function Separator() {
|
|
127
|
+
return /* @__PURE__ */ jsx(ChevronRightIcon, { className: "size-3 shrink-0 text-(--theme-muted-text-color) opacity-60 rtl:rotate-180" });
|
|
128
|
+
}
|
|
129
|
+
function CrumbPlaceholder() {
|
|
130
|
+
const { common } = useStrings();
|
|
131
|
+
return /* @__PURE__ */ jsxs("span", {
|
|
132
|
+
className: "flex items-center",
|
|
133
|
+
children: [/* @__PURE__ */ jsx(Skeleton, {
|
|
134
|
+
width: "6rem",
|
|
135
|
+
height: "0.8125rem"
|
|
136
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
137
|
+
className: "sr-only",
|
|
138
|
+
children: common.loading
|
|
139
|
+
})]
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
var FOLD_WIDTH = 48;
|
|
143
|
+
var PARENT_MAX_WIDTH = 144;
|
|
144
|
+
var LAST_MIN_WIDTH = 128;
|
|
145
|
+
var px = (value) => Number.parseFloat(value) || 0;
|
|
146
|
+
/** The width the trail may take. A flex row sizes the trail to its content, so
|
|
147
|
+
* its room is what the row leaves beside its other items; anywhere else the
|
|
148
|
+
* `nav` is a block that fills its container. */
|
|
149
|
+
function roomFor(nav, row) {
|
|
150
|
+
const style = getComputedStyle(row);
|
|
151
|
+
if (!style.display.endsWith("flex") || style.flexDirection.startsWith("column")) return nav.clientWidth;
|
|
152
|
+
const gap = px(style.columnGap);
|
|
153
|
+
let room = row.clientWidth - px(style.paddingLeft) - px(style.paddingRight);
|
|
154
|
+
for (const child of row.children) if (child !== nav) room -= child.getBoundingClientRect().width + gap;
|
|
155
|
+
return room;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Fits the trail to its room. It measures a hidden copy of the trail at its
|
|
159
|
+
* natural width, so the answer doesn't depend on how the trail is folded at the
|
|
160
|
+
* moment.
|
|
161
|
+
*/
|
|
162
|
+
function useTrailFit(nav, natural) {
|
|
163
|
+
const [fit, setFit] = useState("all");
|
|
164
|
+
useLayoutEffect(() => {
|
|
165
|
+
const row = nav?.parentElement;
|
|
166
|
+
if (!nav || !natural || !row) return;
|
|
167
|
+
const measure = () => {
|
|
168
|
+
const room = roomFor(nav, row);
|
|
169
|
+
if (natural.scrollWidth <= room) {
|
|
170
|
+
setFit("all");
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const steps = [...natural.children];
|
|
174
|
+
const width = (el) => el?.getBoundingClientRect().width ?? 0;
|
|
175
|
+
const parent = Math.min(width(steps[steps.length - 2]), PARENT_MAX_WIDTH);
|
|
176
|
+
const last = Math.min(width(steps[steps.length - 1]), LAST_MIN_WIDTH);
|
|
177
|
+
setFit(FOLD_WIDTH + parent + last <= room ? "parent" : "none");
|
|
178
|
+
};
|
|
179
|
+
const resize = new ResizeObserver(measure);
|
|
180
|
+
const observe = () => {
|
|
181
|
+
for (const el of [
|
|
182
|
+
row,
|
|
183
|
+
nav,
|
|
184
|
+
natural,
|
|
185
|
+
...row.children
|
|
186
|
+
]) resize.observe(el);
|
|
187
|
+
};
|
|
188
|
+
const added = new MutationObserver(() => {
|
|
189
|
+
observe();
|
|
190
|
+
measure();
|
|
191
|
+
});
|
|
192
|
+
measure();
|
|
193
|
+
observe();
|
|
194
|
+
added.observe(row, { childList: true });
|
|
195
|
+
return () => {
|
|
196
|
+
resize.disconnect();
|
|
197
|
+
added.disconnect();
|
|
198
|
+
};
|
|
199
|
+
}, [nav, natural]);
|
|
200
|
+
return fit;
|
|
201
|
+
}
|
|
202
|
+
//#endregion
|
|
203
|
+
export { Breadcrumbs };
|
|
@@ -9,6 +9,8 @@ export interface ColorInputProps {
|
|
|
9
9
|
label?: ReactNode;
|
|
10
10
|
/** Beside the label and outside it, so it stays out of the field's name. */
|
|
11
11
|
hint?: ReactNode;
|
|
12
|
+
/** Buttons at the far end of the label's row that act on the color, as Field's `actions`. */
|
|
13
|
+
actions?: ReactNode;
|
|
12
14
|
/** Help under the field. */
|
|
13
15
|
description?: ReactNode;
|
|
14
16
|
error?: ReactNode;
|
|
@@ -33,4 +35,4 @@ export interface ColorInputProps {
|
|
|
33
35
|
* in. A half-typed color stays in the field, marked invalid, until it parses; leaving the field
|
|
34
36
|
* puts back the last color that did.
|
|
35
37
|
*/
|
|
36
|
-
export declare function ColorInput({ value, onValueChange, label, hint, description, error, size, id, name, disabled, required, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, 'aria-describedby': ariaDescribedBy, 'aria-invalid': ariaInvalid, 'aria-required': ariaRequired, className, }: ColorInputProps): string | number | bigint | boolean | import("react").JSX.Element | Iterable<ReactNode> | Promise<string | number | bigint | boolean | Iterable<ReactNode> | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | import("react").ReactPortal | null | undefined> | null | undefined;
|
|
38
|
+
export declare function ColorInput({ value, onValueChange, label, hint, actions, description, error, size, id, name, disabled, required, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, 'aria-describedby': ariaDescribedBy, 'aria-invalid': ariaInvalid, 'aria-required': ariaRequired, className, }: ColorInputProps): string | number | bigint | boolean | import("react").JSX.Element | Iterable<ReactNode> | Promise<string | number | bigint | boolean | Iterable<ReactNode> | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | import("react").ReactPortal | null | undefined> | null | undefined;
|
|
@@ -14,7 +14,7 @@ var swatchClassName = cx("shrink-0 cursor-pointer p-0.5", "[&::-webkit-color-swa
|
|
|
14
14
|
* in. A half-typed color stays in the field, marked invalid, until it parses; leaving the field
|
|
15
15
|
* puts back the last color that did.
|
|
16
16
|
*/
|
|
17
|
-
function ColorInput({ value, onValueChange, label, hint, description, error, size = "md", id, name, disabled, required, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, "aria-invalid": ariaInvalid, "aria-required": ariaRequired, className }) {
|
|
17
|
+
function ColorInput({ value, onValueChange, label, hint, actions, description, error, size = "md", id, name, disabled, required, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, "aria-invalid": ariaInvalid, "aria-required": ariaRequired, className }) {
|
|
18
18
|
const t = useStrings().colorInput;
|
|
19
19
|
const [draft, setDraft] = useState(null);
|
|
20
20
|
const errorId = useId();
|
|
@@ -25,6 +25,7 @@ function ColorInput({ value, onValueChange, label, hint, description, error, siz
|
|
|
25
25
|
return framed({
|
|
26
26
|
label,
|
|
27
27
|
hint,
|
|
28
|
+
actions,
|
|
28
29
|
description,
|
|
29
30
|
error: error ?? (unparsed ? t.invalid : void 0),
|
|
30
31
|
size
|
|
@@ -22,7 +22,9 @@ export interface TextareaProps extends TextareaHTMLAttributes<HTMLTextAreaElemen
|
|
|
22
22
|
ref?: Ref<HTMLTextAreaElement> | undefined;
|
|
23
23
|
}
|
|
24
24
|
/** The Field around a control when it has a label; the control alone when it doesn't. */
|
|
25
|
-
export declare function framed({ label, hint, help, description, error, size }: Omit<FramedProps, 'mono'
|
|
25
|
+
export declare function framed({ label, hint, help, actions, description, error, size, }: Omit<FramedProps, 'mono'> & {
|
|
26
|
+
actions?: ReactNode;
|
|
27
|
+
}, { id, required, disabled, }: {
|
|
26
28
|
id?: string | undefined;
|
|
27
29
|
required?: boolean | undefined;
|
|
28
30
|
disabled?: boolean | undefined;
|
package/dist/components/Input.js
CHANGED
|
@@ -6,12 +6,13 @@ import { useState } from "react";
|
|
|
6
6
|
import { Input } from "@base-ui/react/input";
|
|
7
7
|
//#region src/components/Input.tsx
|
|
8
8
|
/** The Field around a control when it has a label; the control alone when it doesn't. */
|
|
9
|
-
function framed({ label, hint, help, description, error, size }, { id, required, disabled }, control) {
|
|
9
|
+
function framed({ label, hint, help, actions, description, error, size }, { id, required, disabled }, control) {
|
|
10
10
|
if (label === void 0) return control(error ? { "aria-invalid": true } : {});
|
|
11
11
|
return /* @__PURE__ */ jsx(Field, {
|
|
12
12
|
label,
|
|
13
13
|
hint,
|
|
14
14
|
help,
|
|
15
|
+
actions,
|
|
15
16
|
description,
|
|
16
17
|
error,
|
|
17
18
|
required,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
|
-
import { type Breadcrumb } from './
|
|
3
|
-
export type { Breadcrumb } from './
|
|
2
|
+
import { type Breadcrumb } from './Breadcrumbs.js';
|
|
3
|
+
export type { Breadcrumb } from './Breadcrumbs.js';
|
|
4
4
|
/**
|
|
5
5
|
* Holds the header the current page declares, for the `PageHeaderBar` the
|
|
6
6
|
* shell renders. Wrap the shell, above both the bar and the routes.
|
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
import { cx } from "../cx.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { keyedByContent } from "./keys.js";
|
|
5
|
-
import { Tooltip } from "./Tooltip.js";
|
|
6
|
-
import { useSlots, useStrings } from "./Provider.js";
|
|
7
|
-
import { AppLink } from "./AppLink.js";
|
|
8
|
-
import { ActionMenu } from "./ActionMenu.js";
|
|
9
|
-
import { Skeleton } from "./Skeleton.js";
|
|
2
|
+
import { useSlots } from "./Provider.js";
|
|
3
|
+
import { Breadcrumbs } from "./Breadcrumbs.js";
|
|
10
4
|
import { PageHeaderStore } from "./pageHeaderStore.js";
|
|
11
5
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
12
6
|
import { createContext, use, useCallback, useLayoutEffect, useState, useSyncExternalStore } from "react";
|
|
@@ -146,7 +140,10 @@ function HeaderLayout({ breadcrumbs, titleActions, actions, toolbar, compact = f
|
|
|
146
140
|
className: "flex min-w-0 flex-1 items-center gap-2",
|
|
147
141
|
children: [
|
|
148
142
|
navToggle?.(),
|
|
149
|
-
hasTrail && /* @__PURE__ */ jsx(
|
|
143
|
+
hasTrail && /* @__PURE__ */ jsx(Breadcrumbs, {
|
|
144
|
+
crumbs: breadcrumbs,
|
|
145
|
+
heading: true
|
|
146
|
+
}),
|
|
150
147
|
present(titleActions) && /* @__PURE__ */ jsx("div", {
|
|
151
148
|
className: "flex shrink-0 items-center gap-0.5",
|
|
152
149
|
children: titleActions
|
|
@@ -163,141 +160,5 @@ function HeaderLayout({ breadcrumbs, titleActions, actions, toolbar, compact = f
|
|
|
163
160
|
})]
|
|
164
161
|
});
|
|
165
162
|
}
|
|
166
|
-
var crumbText = "block truncate text-(--theme-muted-text-color)";
|
|
167
|
-
var crumbLink = "rounded-sm transition-colors hover:text-(--theme-app) focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-(--theme-link)";
|
|
168
|
-
function Separator() {
|
|
169
|
-
return /* @__PURE__ */ jsx(ChevronRightIcon, { className: "size-3 shrink-0 text-(--theme-muted-text-color) opacity-60" });
|
|
170
|
-
}
|
|
171
|
-
var FOLD_WIDTH = 48;
|
|
172
|
-
var PARENT_MAX_WIDTH = 144;
|
|
173
|
-
var TITLE_MIN_WIDTH = 128;
|
|
174
|
-
/**
|
|
175
|
-
* Fits the trail to the room its row leaves it. It measures a hidden copy of
|
|
176
|
-
* the trail at its natural width against the row's width less its other items,
|
|
177
|
-
* so the answer doesn't depend on how the trail is folded at the moment.
|
|
178
|
-
*/
|
|
179
|
-
function useTrailFit(nav, natural) {
|
|
180
|
-
const [fit, setFit] = useState("all");
|
|
181
|
-
useLayoutEffect(() => {
|
|
182
|
-
const row = nav?.parentElement;
|
|
183
|
-
if (!nav || !natural || !row) return;
|
|
184
|
-
const measure = () => {
|
|
185
|
-
const gap = Number.parseFloat(getComputedStyle(row).columnGap) || 0;
|
|
186
|
-
let room = row.clientWidth;
|
|
187
|
-
for (const child of row.children) if (child !== nav) room -= child.getBoundingClientRect().width + gap;
|
|
188
|
-
if (natural.scrollWidth <= room) {
|
|
189
|
-
setFit("all");
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
const steps = [...natural.children];
|
|
193
|
-
const width = (el) => el?.getBoundingClientRect().width ?? 0;
|
|
194
|
-
const parent = Math.min(width(steps[steps.length - 2]), PARENT_MAX_WIDTH);
|
|
195
|
-
const title = Math.min(width(steps[steps.length - 1]), TITLE_MIN_WIDTH);
|
|
196
|
-
setFit(FOLD_WIDTH + parent + title <= room ? "parent" : "none");
|
|
197
|
-
};
|
|
198
|
-
measure();
|
|
199
|
-
const observer = new ResizeObserver(measure);
|
|
200
|
-
observer.observe(row);
|
|
201
|
-
observer.observe(natural);
|
|
202
|
-
return () => observer.disconnect();
|
|
203
|
-
}, [nav, natural]);
|
|
204
|
-
return fit;
|
|
205
|
-
}
|
|
206
|
-
function BreadcrumbTrail({ crumbs }) {
|
|
207
|
-
const { common } = useStrings();
|
|
208
|
-
const [nav, setNav] = useState(null);
|
|
209
|
-
const [natural, setNatural] = useState(null);
|
|
210
|
-
const fit = useTrailFit(nav, natural);
|
|
211
|
-
const current = crumbs[crumbs.length - 1];
|
|
212
|
-
const ancestors = crumbs.slice(0, -1);
|
|
213
|
-
const folded = fit === "none" ? ancestors : fit === "parent" ? ancestors.slice(0, -1) : [];
|
|
214
|
-
const shown = ancestors.slice(folded.length);
|
|
215
|
-
return /* @__PURE__ */ jsxs("nav", {
|
|
216
|
-
ref: setNav,
|
|
217
|
-
"aria-label": common.breadcrumb,
|
|
218
|
-
className: "relative min-w-0",
|
|
219
|
-
children: [/* @__PURE__ */ jsxs("ol", {
|
|
220
|
-
className: "m-0 flex min-w-0 list-none items-center gap-1.5 p-0 text-[13px] leading-none",
|
|
221
|
-
children: [
|
|
222
|
-
folded.length > 0 && /* @__PURE__ */ jsxs("li", {
|
|
223
|
-
className: "flex shrink-0 items-center gap-1.5",
|
|
224
|
-
children: [/* @__PURE__ */ jsx(ActionMenu, {
|
|
225
|
-
label: common.showFullPath,
|
|
226
|
-
align: "start",
|
|
227
|
-
items: folded.map((crumb) => crumb.href ? {
|
|
228
|
-
label: crumb.label,
|
|
229
|
-
href: crumb.href
|
|
230
|
-
} : {
|
|
231
|
-
label: crumb.label,
|
|
232
|
-
disabled: true,
|
|
233
|
-
onClick: () => {}
|
|
234
|
-
}),
|
|
235
|
-
triggerClassName: buttonClasses({
|
|
236
|
-
variant: "ghost",
|
|
237
|
-
size: "xs",
|
|
238
|
-
iconOnly: true
|
|
239
|
-
}),
|
|
240
|
-
children: /* @__PURE__ */ jsx(MoreIcon, {})
|
|
241
|
-
}), /* @__PURE__ */ jsx(Separator, {})]
|
|
242
|
-
}),
|
|
243
|
-
keyedByContent(shown, (c) => c.href ?? c.label).map(({ key, item: crumb }) => /* @__PURE__ */ jsxs("li", {
|
|
244
|
-
className: cx("flex items-center gap-1.5", folded.length > 0 ? "min-w-0 max-w-36 shrink-0" : "min-w-0"),
|
|
245
|
-
children: [crumb.label === "" ? /* @__PURE__ */ jsx(CrumbPlaceholder, {}) : /* @__PURE__ */ jsx(Tooltip, {
|
|
246
|
-
content: crumb.label,
|
|
247
|
-
overflowOnly: true,
|
|
248
|
-
children: crumb.href ? /* @__PURE__ */ jsx(AppLink, {
|
|
249
|
-
href: crumb.href,
|
|
250
|
-
className: cx(crumbText, crumbLink, "max-w-48"),
|
|
251
|
-
children: crumb.label
|
|
252
|
-
}) : /* @__PURE__ */ jsx("span", {
|
|
253
|
-
className: cx(crumbText, "max-w-48"),
|
|
254
|
-
children: crumb.label
|
|
255
|
-
})
|
|
256
|
-
}), /* @__PURE__ */ jsx(Separator, {})]
|
|
257
|
-
}, key)),
|
|
258
|
-
current && /* @__PURE__ */ jsx("li", {
|
|
259
|
-
className: "flex min-w-0 items-center",
|
|
260
|
-
children: current.label === "" ? /* @__PURE__ */ jsx("h1", {
|
|
261
|
-
"aria-current": "page",
|
|
262
|
-
className: "m-0",
|
|
263
|
-
children: /* @__PURE__ */ jsx(CrumbPlaceholder, {})
|
|
264
|
-
}) : /* @__PURE__ */ jsx(Tooltip, {
|
|
265
|
-
content: current.label,
|
|
266
|
-
overflowOnly: true,
|
|
267
|
-
children: /* @__PURE__ */ jsx("h1", {
|
|
268
|
-
"aria-current": "page",
|
|
269
|
-
className: "m-0 truncate text-[13px] font-medium leading-none text-(--theme-app)",
|
|
270
|
-
children: current.label
|
|
271
|
-
})
|
|
272
|
-
})
|
|
273
|
-
})
|
|
274
|
-
]
|
|
275
|
-
}), /* @__PURE__ */ jsx("div", {
|
|
276
|
-
ref: setNatural,
|
|
277
|
-
"aria-hidden": true,
|
|
278
|
-
className: "invisible absolute top-0 left-0 flex items-center gap-1.5 whitespace-nowrap text-[13px] leading-none",
|
|
279
|
-
children: crumbs.map((crumb, i) => /* @__PURE__ */ jsxs("span", {
|
|
280
|
-
className: "flex items-center gap-1.5",
|
|
281
|
-
children: [/* @__PURE__ */ jsx("span", {
|
|
282
|
-
className: cx(i < crumbs.length - 1 && "max-w-48 truncate", i === crumbs.length - 1 && "font-medium"),
|
|
283
|
-
children: crumb.label || " ".repeat(6)
|
|
284
|
-
}), i < crumbs.length - 1 && /* @__PURE__ */ jsx(Separator, {})]
|
|
285
|
-
}, i))
|
|
286
|
-
})]
|
|
287
|
-
});
|
|
288
|
-
}
|
|
289
|
-
function CrumbPlaceholder() {
|
|
290
|
-
const { common } = useStrings();
|
|
291
|
-
return /* @__PURE__ */ jsxs("span", {
|
|
292
|
-
className: "flex items-center",
|
|
293
|
-
children: [/* @__PURE__ */ jsx(Skeleton, {
|
|
294
|
-
width: "6rem",
|
|
295
|
-
height: "0.8125rem"
|
|
296
|
-
}), /* @__PURE__ */ jsx("span", {
|
|
297
|
-
className: "sr-only",
|
|
298
|
-
children: common.loading
|
|
299
|
-
})]
|
|
300
|
-
});
|
|
301
|
-
}
|
|
302
163
|
//#endregion
|
|
303
164
|
export { PageHeader, PageHeaderBar, PageHeaderProvider };
|
|
@@ -153,6 +153,8 @@ export interface UIStrings {
|
|
|
153
153
|
/** The background a theme is generated from. */
|
|
154
154
|
base: string;
|
|
155
155
|
accent: string;
|
|
156
|
+
/** Exchanges the background and the accent. */
|
|
157
|
+
swap: string;
|
|
156
158
|
contrast: string;
|
|
157
159
|
/** Under the contrast slider. */
|
|
158
160
|
contrastHelp: string;
|
|
@@ -639,7 +641,7 @@ export interface UIData {
|
|
|
639
641
|
usePersistedState: PersistedStateHook;
|
|
640
642
|
}
|
|
641
643
|
import type { FieldComponent } from '../form/fieldRegistry.js';
|
|
642
|
-
import type { Breadcrumb } from './
|
|
644
|
+
import type { Breadcrumb } from './Breadcrumbs.js';
|
|
643
645
|
/** An internal navigation link, which hosts bind to their router's Link. Its
|
|
644
646
|
* `href` is a full in-app URL that may carry a search and a hash, such as
|
|
645
647
|
* `/runs/42?tab=logs#step-3`, and the host routes all of it client-side on a
|
|
@@ -153,6 +153,7 @@ var DEFAULTS = {
|
|
|
153
153
|
presets: "Presets",
|
|
154
154
|
base: "Background",
|
|
155
155
|
accent: "Accent",
|
|
156
|
+
swap: "Swap background and accent",
|
|
156
157
|
contrast: "Contrast",
|
|
157
158
|
contrastHelp: "Separates surfaces and strengthens text. Text always meets WCAG AA.",
|
|
158
159
|
customSidebar: "Custom sidebar",
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { cx } from "../cx.js";
|
|
2
|
+
import { SwapIcon } from "../icons.js";
|
|
2
3
|
import { useStrings } from "./Provider.js";
|
|
3
4
|
import { useCopy } from "./useCopy.js";
|
|
4
5
|
import { Button } from "./Button.js";
|
|
6
|
+
import { IconButton } from "./IconButton.js";
|
|
5
7
|
import { fieldBox } from "./fieldBox.js";
|
|
6
8
|
import { parseColor, toHex } from "../theme/color.js";
|
|
7
9
|
import { CODE_STYLES, DEFAULT_THEME, THEME_PRESETS, modeOf, parseTheme, schemeFor, serializeTheme } from "../theme/model.js";
|
|
@@ -110,6 +112,14 @@ function ThemeEditor({ value, onChange, mode, onModeChange, preference, onPrefer
|
|
|
110
112
|
}),
|
|
111
113
|
/* @__PURE__ */ jsx(ColorInput, {
|
|
112
114
|
label: t.accent,
|
|
115
|
+
actions: /* @__PURE__ */ jsx(IconButton, {
|
|
116
|
+
icon: /* @__PURE__ */ jsx(SwapIcon, { className: "rotate-90" }),
|
|
117
|
+
label: t.swap,
|
|
118
|
+
onClick: () => setSeed({
|
|
119
|
+
base: seed.accent,
|
|
120
|
+
accent: seed.base
|
|
121
|
+
})
|
|
122
|
+
}),
|
|
113
123
|
value: seed.accent,
|
|
114
124
|
onValueChange: (accent) => setSeed({ accent })
|
|
115
125
|
}),
|
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
export interface Breadcrumb {
|
|
3
|
-
/** Empty while the name is still loading: the bar shows a placeholder. */
|
|
4
|
-
label: string;
|
|
5
|
-
href?: string | undefined;
|
|
6
|
-
}
|
|
1
|
+
import type { Breadcrumb } from './Breadcrumbs.js';
|
|
7
2
|
export type PageHeaderRegion = 'titleActions' | 'actions' | 'toolbar';
|
|
8
3
|
/** The nodes a declaration portals its regions into. They are created with the
|
|
9
4
|
* declaration, before any bar exists, so its content renders on the first pass
|