@parallelworks/ui 0.92.0 → 0.94.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 +82 -1
- package/dist/app-frame/app-frame.css +281 -0
- package/dist/app-frame/index.d.ts +73 -0
- package/dist/app-frame/index.js +369 -0
- package/dist/components/PageHeader.js +70 -34
- package/dist/components/Provider.d.ts +2 -0
- package/dist/components/Provider.js +2 -1
- package/dist/components/headerActionsFit.d.ts +15 -0
- package/dist/components/headerActionsFit.js +36 -3
- package/dist/list/ListPage.js +13 -9
- package/dist/list/listSearch.d.ts +8 -1
- package/dist/list/listSearch.js +151 -55
- package/package.json +8 -3
|
@@ -35,7 +35,14 @@ export declare const ListFooterCountContext: import("react").Context<boolean>;
|
|
|
35
35
|
export declare const ListToolbarInlineContext: import("react").Context<boolean>;
|
|
36
36
|
/** The count's words, "7 tokens" or "2 of 7 tokens", as a status so a screen reader hears the
|
|
37
37
|
* count change as a filter narrows it. */
|
|
38
|
-
export declare function ListCountText(
|
|
38
|
+
export declare function ListCountText(props: ListCountProps): import("react").JSX.Element;
|
|
39
|
+
/** A list's count as a status that can say nothing yet: kept mounted while its text is empty, so
|
|
40
|
+
* a screen reader hears the count when it arrives, as it would not from a status that mounts
|
|
41
|
+
* with its text already in. */
|
|
42
|
+
export declare function ListCountStatus({ count, className, }: {
|
|
43
|
+
count: ListCountProps | undefined;
|
|
44
|
+
className?: string | undefined;
|
|
45
|
+
}): import("react").JSX.Element;
|
|
39
46
|
/** A list's count, placed by hand.
|
|
40
47
|
* @deprecated Give `ListPage` a `count` instead (with its `total` and `matches`): it shows the
|
|
41
48
|
* count under the last row, beside a `pager`, and hides it while the list shows its empty or
|
package/dist/list/listSearch.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
|
+
import { cx } from "../cx.js";
|
|
1
2
|
import { buttonClasses } from "../components/buttonClasses.js";
|
|
2
3
|
import { CloseIcon, RemoveIcon, SearchIcon } from "../icons.js";
|
|
3
4
|
import { ariaKeyShortcuts } from "../components/Kbd.js";
|
|
4
5
|
import { Tooltip } from "../components/Tooltip.js";
|
|
5
6
|
import { useLocale, usePersistedState, useStrings } from "../components/Provider.js";
|
|
6
7
|
import { isTextEntryContext } from "../components/shortcutKey.js";
|
|
8
|
+
import { HeaderActionsFitContext } from "../components/headerActionsFit.js";
|
|
7
9
|
import { formatInteger } from "../format.js";
|
|
8
|
-
import { createContext, useCallback, useContext, useEffect, useRef, useState } from "react";
|
|
9
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
10
|
+
import { createContext, use, useCallback, useContext, useEffect, useId, useLayoutEffect, useRef, useState, useSyncExternalStore } from "react";
|
|
11
|
+
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
12
|
+
import { createPortal } from "react-dom";
|
|
10
13
|
//#region src/list/listSearch.tsx
|
|
11
14
|
var NO_TERMS = [];
|
|
12
15
|
/** Pass `enabled: false` to suspend the global ⌘F/`/` hijack (e.g. when search isn't shown and browser Find should stay active). */
|
|
@@ -69,67 +72,149 @@ function useListSearch(enabled = true, storageKey) {
|
|
|
69
72
|
function ListSearchControl({ search, placeholder, label: labelProp }) {
|
|
70
73
|
const { common, list } = useStrings();
|
|
71
74
|
const label = labelProp ?? common.search;
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
75
|
+
const fit = use(HeaderActionsFitContext);
|
|
76
|
+
useSyncExternalStore(fit?.subscribe ?? subscribeNever, fit?.getVersion ?? zero, zero);
|
|
77
|
+
const covers = fit?.searchCovers() ?? false;
|
|
78
|
+
const coverNode = fit?.coverNode() ?? null;
|
|
79
|
+
const covering = covers && search.searchOpen && coverNode !== null;
|
|
80
|
+
const field = covering || !covers && (search.searchOpen || search.terms.length > 0);
|
|
81
|
+
const active = search.terms.length > 0 || search.filter.trim() !== "";
|
|
82
|
+
const id = useId();
|
|
83
|
+
useLayoutEffect(() => {
|
|
84
|
+
fit?.setCovering(id, covering);
|
|
85
|
+
}, [
|
|
86
|
+
fit,
|
|
87
|
+
id,
|
|
88
|
+
covering
|
|
89
|
+
]);
|
|
90
|
+
useLayoutEffect(() => fit ? () => fit.setCovering(id, false) : void 0, [fit, id]);
|
|
91
|
+
const button = useRef(null);
|
|
92
|
+
const returnFocus = useRef(false);
|
|
93
|
+
useLayoutEffect(() => {
|
|
94
|
+
if (!field && returnFocus.current) {
|
|
95
|
+
returnFocus.current = false;
|
|
96
|
+
queueMicrotask(() => button.current?.focus());
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
const { inputRef, searchOpen } = search;
|
|
100
|
+
const wasCovering = useRef(covering);
|
|
101
|
+
useLayoutEffect(() => {
|
|
102
|
+
if (wasCovering.current === covering) return;
|
|
103
|
+
wasCovering.current = covering;
|
|
104
|
+
if (!searchOpen) return;
|
|
105
|
+
queueMicrotask(() => {
|
|
106
|
+
const focused = document.activeElement;
|
|
107
|
+
if (!focused || focused === document.body || coverNode?.parentElement?.contains(focused)) inputRef.current?.focus();
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
if (field) {
|
|
111
|
+
const close = () => {
|
|
112
|
+
returnFocus.current = true;
|
|
113
|
+
search.clearSearch();
|
|
114
|
+
};
|
|
115
|
+
const closeOnEscape = (e) => {
|
|
116
|
+
if (e.key === "Escape" && search.searchOpen) {
|
|
117
|
+
e.preventDefault();
|
|
118
|
+
returnFocus.current = covering || search.terms.length === 0;
|
|
119
|
+
search.closeSearch();
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
return false;
|
|
123
|
+
};
|
|
124
|
+
const box = /* @__PURE__ */ jsxs("div", {
|
|
125
|
+
className: cx("flex flex-wrap items-center gap-1.5 rounded-md border border-(--theme-border) bg-(--theme-muted-panel-bg) px-2 py-0.5", covering ? "pointer-events-auto min-h-8 w-full" : "min-h-7 w-72 max-w-[55vw]"),
|
|
126
|
+
children: [
|
|
127
|
+
/* @__PURE__ */ jsx(SearchIcon, { className: "w-3.5 h-3.5 shrink-0 text-(--theme-muted-text-color)" }),
|
|
128
|
+
search.terms.map((term) => /* @__PURE__ */ jsxs("span", {
|
|
129
|
+
className: "flex max-w-40 items-center gap-1 rounded border border-(--theme-border) bg-(--theme-app-bg) px-1.5 py-px text-[12px] text-(--theme-app)",
|
|
130
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
131
|
+
className: "truncate",
|
|
132
|
+
children: term
|
|
133
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
134
|
+
type: "button",
|
|
135
|
+
onClick: () => search.removeTerm(term),
|
|
136
|
+
"aria-label": list.removeSearchTerm(term),
|
|
137
|
+
className: "shrink-0 rounded p-0.5 text-(--theme-muted-text-color) hover:text-(--theme-app) transition-colors",
|
|
138
|
+
children: /* @__PURE__ */ jsx(RemoveIcon, { className: "w-2.5 h-2.5" })
|
|
139
|
+
})]
|
|
140
|
+
}, term)),
|
|
141
|
+
/* @__PURE__ */ jsx("input", {
|
|
142
|
+
ref: search.inputRef,
|
|
143
|
+
value: search.filter,
|
|
144
|
+
onChange: (e) => search.setFilter(e.target.value),
|
|
145
|
+
onFocus: search.openSearch,
|
|
146
|
+
"aria-label": label,
|
|
147
|
+
onKeyDown: (e) => {
|
|
148
|
+
if (closeOnEscape(e)) return;
|
|
149
|
+
if (e.key === "Enter") {
|
|
150
|
+
e.preventDefault();
|
|
151
|
+
search.addTerm(search.filter);
|
|
152
|
+
} else if (e.key === "Backspace" && !search.filter) {
|
|
153
|
+
const lastTerm = search.terms.at(-1);
|
|
154
|
+
if (lastTerm) search.removeTerm(lastTerm);
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
placeholder,
|
|
158
|
+
className: "min-w-24 flex-1 bg-transparent text-[13px] text-(--theme-app) placeholder:text-(--theme-placeholder) focus:outline-none"
|
|
159
|
+
}),
|
|
160
|
+
/* @__PURE__ */ jsx("button", {
|
|
82
161
|
type: "button",
|
|
83
|
-
onClick:
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
} else if (e.key === "Enter") {
|
|
100
|
-
e.preventDefault();
|
|
101
|
-
search.addTerm(search.filter);
|
|
102
|
-
} else if (e.key === "Backspace" && !search.filter) {
|
|
103
|
-
const lastTerm = search.terms.at(-1);
|
|
104
|
-
if (lastTerm) search.removeTerm(lastTerm);
|
|
105
|
-
}
|
|
106
|
-
},
|
|
107
|
-
placeholder,
|
|
108
|
-
className: "min-w-24 flex-1 bg-transparent text-[13px] text-(--theme-app) placeholder:text-(--theme-placeholder) focus:outline-none"
|
|
109
|
-
}),
|
|
110
|
-
/* @__PURE__ */ jsx("button", {
|
|
111
|
-
type: "button",
|
|
112
|
-
onClick: search.clearSearch,
|
|
113
|
-
"aria-label": common.clearSearch,
|
|
114
|
-
className: "shrink-0 rounded p-0.5 text-(--theme-muted-text-color) hover:text-(--theme-app) transition-colors",
|
|
115
|
-
children: /* @__PURE__ */ jsx(CloseIcon, { className: "w-3 h-3" })
|
|
162
|
+
onClick: close,
|
|
163
|
+
onKeyDown: closeOnEscape,
|
|
164
|
+
"aria-label": common.clearSearch,
|
|
165
|
+
className: cx("shrink-0 rounded text-(--theme-muted-text-color) hover:text-(--theme-app) transition-colors", covering ? "-me-1 inline-flex size-6 items-center justify-center" : "p-0.5"),
|
|
166
|
+
children: /* @__PURE__ */ jsx(CloseIcon, { className: "w-3 h-3" })
|
|
167
|
+
})
|
|
168
|
+
]
|
|
169
|
+
});
|
|
170
|
+
if (covering && coverNode) return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
|
|
171
|
+
"data-header-search": "",
|
|
172
|
+
className: "contents",
|
|
173
|
+
children: /* @__PURE__ */ jsx(SearchButton, {
|
|
174
|
+
label,
|
|
175
|
+
name: active ? list.searchActive(label) : label,
|
|
176
|
+
active,
|
|
177
|
+
onOpen: search.openSearch
|
|
116
178
|
})
|
|
117
|
-
]
|
|
179
|
+
}), createPortal(box, coverNode)] });
|
|
180
|
+
return fit ? /* @__PURE__ */ jsx("span", {
|
|
181
|
+
"data-header-search": "",
|
|
182
|
+
className: "contents",
|
|
183
|
+
children: box
|
|
184
|
+
}) : box;
|
|
185
|
+
}
|
|
186
|
+
const control = /* @__PURE__ */ jsx(SearchButton, {
|
|
187
|
+
ref: button,
|
|
188
|
+
label,
|
|
189
|
+
name: active ? list.searchActive(label) : label,
|
|
190
|
+
active,
|
|
191
|
+
onOpen: search.openSearch
|
|
118
192
|
});
|
|
193
|
+
return fit ? /* @__PURE__ */ jsx("span", {
|
|
194
|
+
"data-header-search": "",
|
|
195
|
+
className: "contents",
|
|
196
|
+
children: control
|
|
197
|
+
}) : control;
|
|
198
|
+
}
|
|
199
|
+
var subscribeNever = () => () => {};
|
|
200
|
+
var zero = () => 0;
|
|
201
|
+
/** The search closed: a button, with a dot while a query it holds narrows the list. */
|
|
202
|
+
function SearchButton({ ref, label, name = label, active, onOpen }) {
|
|
119
203
|
return /* @__PURE__ */ jsx(Tooltip, {
|
|
120
204
|
content: label,
|
|
121
205
|
shortcut: "/",
|
|
122
|
-
children: /* @__PURE__ */
|
|
206
|
+
children: /* @__PURE__ */ jsxs("button", {
|
|
207
|
+
ref,
|
|
123
208
|
type: "button",
|
|
124
|
-
onClick:
|
|
125
|
-
"aria-label":
|
|
209
|
+
onClick: onOpen,
|
|
210
|
+
"aria-label": name,
|
|
126
211
|
"aria-keyshortcuts": `/ ${ariaKeyShortcuts("Mod+F")}`,
|
|
127
212
|
className: buttonClasses({
|
|
128
213
|
variant: "outline",
|
|
129
214
|
size: "sm",
|
|
130
215
|
iconOnly: true
|
|
131
216
|
}),
|
|
132
|
-
children: /* @__PURE__ */ jsx(SearchIcon, {})
|
|
217
|
+
children: [/* @__PURE__ */ jsx(SearchIcon, {}), active && /* @__PURE__ */ jsx("span", { className: "absolute right-0 top-0 h-1.5 w-1.5 rounded-full bg-(--theme-link)" })]
|
|
133
218
|
})
|
|
134
219
|
});
|
|
135
220
|
}
|
|
@@ -141,14 +226,25 @@ var ListFooterCountContext = createContext(false);
|
|
|
141
226
|
var ListToolbarInlineContext = createContext(false);
|
|
142
227
|
/** The count's words, "7 tokens" or "2 of 7 tokens", as a status so a screen reader hears the
|
|
143
228
|
* count change as a filter narrows it. */
|
|
144
|
-
function ListCountText(
|
|
229
|
+
function ListCountText(props) {
|
|
230
|
+
return /* @__PURE__ */ jsx(ListCountStatus, { count: props });
|
|
231
|
+
}
|
|
232
|
+
/** A list's count as a status that can say nothing yet: kept mounted while its text is empty, so
|
|
233
|
+
* a screen reader hears the count when it arrives, as it would not from a status that mounts
|
|
234
|
+
* with its text already in. */
|
|
235
|
+
function ListCountStatus({ count, className = "text-[13px] text-(--theme-muted-text-color)" }) {
|
|
145
236
|
const { list: t } = useStrings();
|
|
146
237
|
const locale = useLocale();
|
|
147
|
-
const
|
|
238
|
+
const n = (value) => formatInteger(value, locale);
|
|
239
|
+
let words = "";
|
|
240
|
+
if (count) {
|
|
241
|
+
const { total, matches, filterActive, noun, nounPlural } = count;
|
|
242
|
+
words = filterActive ? t.matchCount(n(matches), n(total), nounPlural) : t.count(n(total), total === 1 ? noun : nounPlural);
|
|
243
|
+
}
|
|
148
244
|
return /* @__PURE__ */ jsx("span", {
|
|
149
245
|
role: "status",
|
|
150
|
-
className
|
|
151
|
-
children:
|
|
246
|
+
className,
|
|
247
|
+
children: words
|
|
152
248
|
});
|
|
153
249
|
}
|
|
154
250
|
/** A list's count, placed by hand.
|
|
@@ -163,4 +259,4 @@ function ListCount(props) {
|
|
|
163
259
|
});
|
|
164
260
|
}
|
|
165
261
|
//#endregion
|
|
166
|
-
export { ListCount, ListCountText, ListFooterCountContext, ListSearchControl, ListToolbarInlineContext, useListSearch };
|
|
262
|
+
export { ListCount, ListCountStatus, ListCountText, ListFooterCountContext, ListSearchControl, ListToolbarInlineContext, useListSearch };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@parallelworks/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.94.0",
|
|
4
4
|
"description": "React UI components: a theme contract, icons, brand logos, primitives, lists, forms, a job graph, a code editor, a log viewer, a file explorer and an AI chat, each on its own subpath",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -76,7 +76,12 @@
|
|
|
76
76
|
"types": "./dist/ai/adapter/mock/index.d.ts",
|
|
77
77
|
"import": "./dist/ai/adapter/mock/index.js"
|
|
78
78
|
},
|
|
79
|
-
"./ai.css": "./dist/ai/ai.css"
|
|
79
|
+
"./ai.css": "./dist/ai/ai.css",
|
|
80
|
+
"./app-frame": {
|
|
81
|
+
"types": "./dist/app-frame/index.d.ts",
|
|
82
|
+
"import": "./dist/app-frame/index.js"
|
|
83
|
+
},
|
|
84
|
+
"./app-frame.css": "./dist/app-frame/app-frame.css"
|
|
80
85
|
},
|
|
81
86
|
"publishConfig": {
|
|
82
87
|
"registry": "https://registry.npmjs.org"
|
|
@@ -148,7 +153,7 @@
|
|
|
148
153
|
"scripts": {
|
|
149
154
|
"build": "vite build && pnpm build:types && pnpm build:css",
|
|
150
155
|
"build:types": "tsc -p tsconfig.build.json && node scripts/declaration-extensions.mjs",
|
|
151
|
-
"build:css": "mkdir -p dist/styles dist/logviewer dist/ai && cp src/styles/theme.css src/styles/base.css src/styles/fonts.css src/styles/*.woff2 dist/styles/ && tailwindcss -i ./src/styles.css -o ./dist/styles.css --minify && cp src/logviewer/logviewer.css dist/logviewer/ && cp src/ai/ai.css dist/ai/",
|
|
156
|
+
"build:css": "mkdir -p dist/styles dist/logviewer dist/ai && cp src/styles/theme.css src/styles/base.css src/styles/fonts.css src/styles/*.woff2 dist/styles/ && tailwindcss -i ./src/styles.css -o ./dist/styles.css --minify && cp src/logviewer/logviewer.css dist/logviewer/ && cp src/ai/ai.css dist/ai/ && mkdir -p dist/app-frame && cp src/app-frame/app-frame.css dist/app-frame/",
|
|
152
157
|
"clean": "rm -rf dist",
|
|
153
158
|
"test": "vitest run",
|
|
154
159
|
"lint:package": "publint --strict",
|