dustyui 1.0.2 → 2.0.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/LICENSE +21 -0
- package/README.md +56 -1
- package/dist/Avatar/Avatar.d.ts +9 -0
- package/dist/Avatar/Avatar.js +17 -0
- package/dist/Avatar/Avatar.module.css +33 -0
- package/dist/Avatar/getInitials.d.ts +1 -0
- package/dist/Avatar/getInitials.js +13 -0
- package/dist/Badge/Badge.d.ts +3 -13
- package/dist/Badge/Badge.js +5 -31
- package/dist/Badge/Badge.module.css +12 -11
- package/dist/Button/Button.d.ts +6 -11
- package/dist/Button/Button.js +21 -40
- package/dist/Button/Button.module.css +93 -22
- package/dist/IconButton/IconButton.d.ts +7 -0
- package/dist/IconButton/IconButton.js +8 -0
- package/dist/IconButton/IconButton.module.css +17 -0
- package/dist/ObjectInspector/InspectorDetails.d.ts +6 -0
- package/dist/ObjectInspector/InspectorDetails.js +32 -0
- package/dist/ObjectInspector/InspectorDetails.module.css +47 -0
- package/dist/ObjectInspector/InspectorSearch.d.ts +9 -0
- package/dist/ObjectInspector/InspectorSearch.js +12 -0
- package/dist/ObjectInspector/InspectorSearch.module.css +39 -0
- package/dist/ObjectInspector/ObjectInspector.d.ts +8 -0
- package/dist/ObjectInspector/ObjectInspector.js +83 -0
- package/dist/ObjectInspector/ObjectInspector.module.css +46 -0
- package/dist/ObjectInspector/describe/describe.d.ts +31 -0
- package/dist/ObjectInspector/describe/describe.js +123 -0
- package/dist/ObjectInspector/describe/getChildren.d.ts +21 -0
- package/dist/ObjectInspector/describe/getChildren.js +77 -0
- package/dist/ObjectInspector/describe/getNode.d.ts +2 -0
- package/dist/ObjectInspector/describe/getNode.js +9 -0
- package/dist/ObjectInspector/describe/getPath.d.ts +26 -0
- package/dist/ObjectInspector/describe/getPath.js +58 -0
- package/dist/ObjectInspector/describe/getPreview.d.ts +10 -0
- package/dist/ObjectInspector/describe/getPreview.js +189 -0
- package/dist/ObjectInspector/describe/getValueKind.d.ts +2 -0
- package/dist/ObjectInspector/describe/getValueKind.js +64 -0
- package/dist/ObjectInspector/describe/safeRead.d.ts +12 -0
- package/dist/ObjectInspector/describe/safeRead.js +88 -0
- package/dist/ObjectInspector/getCopyText.d.ts +2 -0
- package/dist/ObjectInspector/getCopyText.js +103 -0
- package/dist/ObjectInspector/getLabelText.d.ts +2 -0
- package/dist/ObjectInspector/getLabelText.js +15 -0
- package/dist/ObjectInspector/getValueText.d.ts +2 -0
- package/dist/ObjectInspector/getValueText.js +5 -0
- package/dist/ObjectInspector/search/getHighlightParts.d.ts +5 -0
- package/dist/ObjectInspector/search/getHighlightParts.js +20 -0
- package/dist/ObjectInspector/search/getMatches.d.ts +6 -0
- package/dist/ObjectInspector/search/getMatches.js +46 -0
- package/dist/ObjectInspector/search/getSearchStatus.d.ts +2 -0
- package/dist/ObjectInspector/search/getSearchStatus.js +14 -0
- package/dist/Tree/Tree.d.ts +14 -0
- package/dist/Tree/Tree.js +166 -0
- package/dist/Tree/Tree.module.css +65 -0
- package/dist/Tree/getWindow.d.ts +22 -0
- package/dist/Tree/getWindow.js +43 -0
- package/dist/Tree/treeModel.d.ts +55 -0
- package/dist/Tree/treeModel.js +203 -0
- package/dist/Tree/useRowWindow.d.ts +3 -0
- package/dist/Tree/useRowWindow.js +78 -0
- package/dist/Tree/useTypeahead.d.ts +2 -0
- package/dist/Tree/useTypeahead.js +15 -0
- package/dist/announcer/announcer.d.ts +2 -0
- package/dist/announcer/announcer.js +41 -0
- package/dist/index.d.ts +10 -2
- package/dist/index.js +9 -6
- package/dist/styles/themes/dusty.css +22 -0
- package/dist/styles/themes/mono.css +21 -0
- package/dist/styles/tokens.css +19 -13
- package/dist/styles/visuallyHidden.module.css +8 -0
- package/package.json +54 -38
- package/dist/Badge/Badge.cjs +0 -67
- package/dist/Button/Button.cjs +0 -74
- package/dist/index.cjs +0 -32
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Wioletta Gluza
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1 +1,56 @@
|
|
|
1
|
-
# DustyUI
|
|
1
|
+
# DustyUI
|
|
2
|
+
|
|
3
|
+
A small React component library, styled with CSS Modules and themed through CSS custom properties. Accessible by default, with light and dark themes built in.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install dustyui
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Requires React 19.2 or later and a bundler that handles CSS Modules, such as Next.js or Vite.
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
import { Button } from "dustyui";
|
|
17
|
+
|
|
18
|
+
export function Example() {
|
|
19
|
+
return <Button>Save</Button>;
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The styles load with the components, so there is no stylesheet to import.
|
|
24
|
+
|
|
25
|
+
Components, props and themes are documented in [dustyui-docs](https://github.com/w-gluza/dustyui-docs).
|
|
26
|
+
|
|
27
|
+
## Accessibility
|
|
28
|
+
|
|
29
|
+
- A component that needs an accessible name takes it as a required prop, so it cannot be left out by accident.
|
|
30
|
+
- Decorative parts, such as icons, are hidden from screen readers.
|
|
31
|
+
- Unavailable and loading controls stay focusable, so keyboard users do not lose their place.
|
|
32
|
+
- Status changes are announced through one shared live region, which you can also use directly:
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
import { announce } from "dustyui";
|
|
36
|
+
|
|
37
|
+
announce("3 unread messages");
|
|
38
|
+
announce("Connection lost", "assertive");
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Scripts
|
|
42
|
+
|
|
43
|
+
| Script | What it does |
|
|
44
|
+
| ------------------- | ----------------------------------------------------------- |
|
|
45
|
+
| `npm run build` | Compiles to `dist` and copies the stylesheets |
|
|
46
|
+
| `npm run typecheck` | Type-checks the source and the tests with TypeScript 7 |
|
|
47
|
+
| `npm test` | Unit tests in Node and component tests in headless Chromium |
|
|
48
|
+
| `npm run lint` | ESLint, with accessibility rules as errors |
|
|
49
|
+
| `npm run lint:css` | Stylelint |
|
|
50
|
+
| `npm run format` | Prettier |
|
|
51
|
+
|
|
52
|
+
Run `npx playwright install chromium` once before the first `npm test`.
|
|
53
|
+
|
|
54
|
+
## Licence
|
|
55
|
+
|
|
56
|
+
MIT
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ComponentProps, ReactNode } from "react";
|
|
2
|
+
export interface AvatarProps extends Omit<ComponentProps<"span">, "children" | "dangerouslySetInnerHTML" | "role" | "aria-label" | "aria-labelledby" | "aria-hidden"> {
|
|
3
|
+
name: string;
|
|
4
|
+
image?: ReactNode;
|
|
5
|
+
fallback?: string;
|
|
6
|
+
size?: number;
|
|
7
|
+
decorative?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare function Avatar({ name, image, fallback, size, decorative, className, style, ...props }: AvatarProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import clsx from "clsx";
|
|
3
|
+
import { getInitials } from "./getInitials.js";
|
|
4
|
+
import styles from "./Avatar.module.css";
|
|
5
|
+
const DEFAULT_SIZE = 64;
|
|
6
|
+
const ROOT_FONT_SIZE = 16;
|
|
7
|
+
export function Avatar({ name, image, fallback, size = DEFAULT_SIZE, decorative = false, className, style, ...props }) {
|
|
8
|
+
const safeSize = Number.isFinite(size) && size > 0 ? size : DEFAULT_SIZE;
|
|
9
|
+
const sizedStyle = {
|
|
10
|
+
...style,
|
|
11
|
+
"--avatar-size": `${safeSize / ROOT_FONT_SIZE}rem`,
|
|
12
|
+
};
|
|
13
|
+
const accessibility = decorative
|
|
14
|
+
? { "aria-hidden": true }
|
|
15
|
+
: { role: "img", "aria-label": name };
|
|
16
|
+
return (_jsx("span", { ...props, ...accessibility, className: clsx(styles.avatar, className), style: sizedStyle, children: image ? (image) : (_jsx("span", { className: styles.fallback, children: fallback ?? getInitials(name) })) }));
|
|
17
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
.avatar {
|
|
2
|
+
display: inline-flex;
|
|
3
|
+
flex-shrink: 0;
|
|
4
|
+
inline-size: var(--avatar-size, 4rem);
|
|
5
|
+
block-size: var(--avatar-size, 4rem);
|
|
6
|
+
overflow: hidden;
|
|
7
|
+
border-radius: var(--dusty-radius-full);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.avatar > img {
|
|
11
|
+
inline-size: 100%;
|
|
12
|
+
block-size: 100%;
|
|
13
|
+
object-fit: cover;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.fallback {
|
|
17
|
+
display: flex;
|
|
18
|
+
align-items: center;
|
|
19
|
+
justify-content: center;
|
|
20
|
+
inline-size: 100%;
|
|
21
|
+
block-size: 100%;
|
|
22
|
+
background-color: var(--dusty-surface-sunken);
|
|
23
|
+
color: var(--dusty-text-muted);
|
|
24
|
+
font-weight: var(--dusty-weight-semibold);
|
|
25
|
+
font-size: calc(var(--avatar-size, 4rem) / 3);
|
|
26
|
+
text-transform: uppercase;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@media (forced-colors: active) {
|
|
30
|
+
.avatar {
|
|
31
|
+
border: 1px solid CanvasText;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function getInitials(name: string): string;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const graphemes = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
2
|
+
function firstGrapheme(word) {
|
|
3
|
+
for (const { segment } of graphemes.segment(word)) {
|
|
4
|
+
return segment;
|
|
5
|
+
}
|
|
6
|
+
return "";
|
|
7
|
+
}
|
|
8
|
+
export function getInitials(name) {
|
|
9
|
+
const words = name.trim().split(/\s+/u).filter(Boolean);
|
|
10
|
+
const first = words[0] ?? "";
|
|
11
|
+
const last = words.length > 1 ? (words.at(-1) ?? "") : "";
|
|
12
|
+
return firstGrapheme(first) + firstGrapheme(last);
|
|
13
|
+
}
|
package/dist/Badge/Badge.d.ts
CHANGED
|
@@ -1,17 +1,7 @@
|
|
|
1
|
-
import
|
|
2
|
-
interface BadgeProps {
|
|
3
|
-
/** Visible text inside the badge. */
|
|
1
|
+
import type { ComponentProps, ReactNode } from "react";
|
|
2
|
+
export interface BadgeProps extends Omit<ComponentProps<"span">, "children" | "dangerouslySetInnerHTML"> {
|
|
4
3
|
label: string;
|
|
5
|
-
/** Optional icon */
|
|
6
4
|
icon?: ReactNode;
|
|
7
|
-
/** Where to place the icon relative to the label. */
|
|
8
5
|
iconPosition?: "left" | "right";
|
|
9
|
-
/** If true, announces updates to assistive tech (use when value changes dynamically). */
|
|
10
|
-
announce?: boolean;
|
|
11
|
-
/** Test hook. */
|
|
12
|
-
dataTestId?: string;
|
|
13
|
-
/** Extra class on the root element. */
|
|
14
|
-
className?: string;
|
|
15
6
|
}
|
|
16
|
-
export declare
|
|
17
|
-
export {};
|
|
7
|
+
export declare function Badge({ label, icon, iconPosition, className, ...props }: BadgeProps): import("react").JSX.Element;
|
package/dist/Badge/Badge.js
CHANGED
|
@@ -1,33 +1,7 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { forwardRef } from "react";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
2
|
import clsx from "clsx";
|
|
4
3
|
import styles from "./Badge.module.css";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
label,
|
|
8
|
-
|
|
9
|
-
iconPosition = "left",
|
|
10
|
-
announce = false,
|
|
11
|
-
dataTestId,
|
|
12
|
-
className
|
|
13
|
-
}, ref) => {
|
|
14
|
-
return /* @__PURE__ */ React.createElement(
|
|
15
|
-
"span",
|
|
16
|
-
{
|
|
17
|
-
ref,
|
|
18
|
-
className: clsx(styles.badge, className),
|
|
19
|
-
"data-testid": dataTestId,
|
|
20
|
-
"data-icon-position": iconPosition,
|
|
21
|
-
role: announce ? "status" : void 0,
|
|
22
|
-
"aria-live": announce ? "polite" : void 0
|
|
23
|
-
},
|
|
24
|
-
icon && iconPosition === "left" && /* @__PURE__ */ React.createElement("span", { className: styles.icon, "aria-hidden": "true" }, icon),
|
|
25
|
-
/* @__PURE__ */ React.createElement("span", { className: styles.label }, label),
|
|
26
|
-
icon && iconPosition === "right" && /* @__PURE__ */ React.createElement("span", { className: styles.icon, "aria-hidden": "true" }, icon)
|
|
27
|
-
);
|
|
28
|
-
}
|
|
29
|
-
);
|
|
30
|
-
Badge.displayName = "Badge";
|
|
31
|
-
export {
|
|
32
|
-
Badge
|
|
33
|
-
};
|
|
4
|
+
export function Badge({ label, icon, iconPosition = "left", className, ...props }) {
|
|
5
|
+
const iconNode = icon != null ? (_jsx("span", { className: styles.icon, "aria-hidden": "true", children: icon })) : null;
|
|
6
|
+
return (_jsxs("span", { ...props, className: clsx(styles.badge, className), children: [iconPosition === "left" ? iconNode : null, _jsx("span", { className: styles.label, children: label }), iconPosition === "right" ? iconNode : null] }));
|
|
7
|
+
}
|
|
@@ -1,26 +1,27 @@
|
|
|
1
1
|
.badge {
|
|
2
2
|
display: inline-flex;
|
|
3
3
|
align-items: center;
|
|
4
|
+
gap: 0.25rem;
|
|
5
|
+
max-inline-size: 100%;
|
|
6
|
+
padding-block: 0.125rem;
|
|
7
|
+
padding-inline: 0.625rem;
|
|
8
|
+
border: 1px solid var(--dusty-border);
|
|
9
|
+
border-radius: var(--dusty-radius-full);
|
|
10
|
+
color: var(--dusty-text);
|
|
4
11
|
font-size: 0.75rem;
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
font-weight: 600;
|
|
8
|
-
border: 1px solid var(--color-gray-300);
|
|
9
|
-
width: max-content;
|
|
10
|
-
border-radius: 12px;
|
|
11
|
-
align-items: center;
|
|
12
|
-
gap: 4px;
|
|
12
|
+
font-weight: var(--dusty-weight-semibold);
|
|
13
|
+
line-height: 1.33;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
.icon {
|
|
16
|
-
width: 0.875rem;
|
|
17
|
-
height: 0.875rem;
|
|
18
17
|
display: inline-flex;
|
|
19
18
|
align-items: center;
|
|
20
19
|
justify-content: center;
|
|
20
|
+
inline-size: 0.875rem;
|
|
21
|
+
block-size: 0.875rem;
|
|
21
22
|
line-height: 0;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
.label {
|
|
25
|
-
|
|
26
|
+
overflow-wrap: anywhere;
|
|
26
27
|
}
|
package/dist/Button/Button.d.ts
CHANGED
|
@@ -1,14 +1,9 @@
|
|
|
1
|
-
import
|
|
2
|
-
export
|
|
3
|
-
|
|
1
|
+
import { type ComponentProps, type ReactNode } from "react";
|
|
2
|
+
export type ButtonVariant = "primary" | "secondary";
|
|
3
|
+
export interface ButtonProps extends Omit<ComponentProps<"button">, "children" | "dangerouslySetInnerHTML"> {
|
|
4
4
|
children: ReactNode;
|
|
5
|
-
|
|
6
|
-
variant?: "primary" | "secondary";
|
|
7
|
-
/** Show as busy (disables interaction + aria-busy) */
|
|
5
|
+
variant?: ButtonVariant;
|
|
8
6
|
loading?: boolean;
|
|
9
|
-
|
|
10
|
-
dataTestId?: string;
|
|
11
|
-
/** Additional CSS class names */
|
|
12
|
-
className?: string;
|
|
7
|
+
loadingLabel?: string;
|
|
13
8
|
}
|
|
14
|
-
export declare
|
|
9
|
+
export declare function Button({ variant, loading, loadingLabel, className, type, children, onClick, "aria-disabled": ariaDisabled, ...props }: ButtonProps): import("react").JSX.Element;
|
package/dist/Button/Button.js
CHANGED
|
@@ -1,42 +1,23 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
} from "react";
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, } from "react";
|
|
4
4
|
import clsx from "clsx";
|
|
5
|
+
import { announce } from "../announcer/announcer.js";
|
|
5
6
|
import styles from "./Button.module.css";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
className: clsx(
|
|
24
|
-
styles.button,
|
|
25
|
-
styles[variant],
|
|
26
|
-
isDisabled && styles.disabled,
|
|
27
|
-
className
|
|
28
|
-
),
|
|
29
|
-
"data-testid": dataTestId,
|
|
30
|
-
"data-variant": variant,
|
|
31
|
-
"aria-busy": loading || void 0,
|
|
32
|
-
disabled: isDisabled,
|
|
33
|
-
...props
|
|
34
|
-
},
|
|
35
|
-
children
|
|
36
|
-
);
|
|
37
|
-
}
|
|
38
|
-
);
|
|
39
|
-
Button.displayName = "Button";
|
|
40
|
-
export {
|
|
41
|
-
Button
|
|
42
|
-
};
|
|
7
|
+
export function Button({ variant = "primary", loading = false, loadingLabel = "Loading", className, type = "button", children, onClick, "aria-disabled": ariaDisabled, ...props }) {
|
|
8
|
+
const inactive = loading || ariaDisabled === true || ariaDisabled === "true";
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
if (loading) {
|
|
11
|
+
announce(loadingLabel);
|
|
12
|
+
}
|
|
13
|
+
}, [loading, loadingLabel]);
|
|
14
|
+
function handleClick(event) {
|
|
15
|
+
if (inactive) {
|
|
16
|
+
event.preventDefault();
|
|
17
|
+
event.stopPropagation();
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
onClick?.(event);
|
|
21
|
+
}
|
|
22
|
+
return (_jsxs("button", { ...props, type: type, className: clsx(styles.button, styles[variant], className), "data-variant": variant, "data-loading": loading || undefined, "aria-disabled": inactive ? true : ariaDisabled, onClick: handleClick, children: [_jsx("span", { className: styles.label, children: children }), loading ? _jsx("span", { className: styles.spinner, "aria-hidden": "true" }) : null] }));
|
|
23
|
+
}
|
|
@@ -1,37 +1,108 @@
|
|
|
1
1
|
.button {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
position: relative;
|
|
3
|
+
display: inline-grid;
|
|
4
|
+
place-items: center;
|
|
5
|
+
padding-block: var(--button-padding-block, 0.75rem);
|
|
6
|
+
padding-inline: var(--button-padding-inline, 1.5rem);
|
|
7
|
+
border: 1px solid var(--dusty-action);
|
|
8
|
+
border-radius: var(--dusty-radius-md);
|
|
9
|
+
font: inherit;
|
|
10
|
+
font-weight: var(--dusty-weight-semibold);
|
|
11
|
+
line-height: 1.25;
|
|
12
|
+
overflow-wrap: anywhere;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.button:focus-visible {
|
|
16
|
+
outline: var(--dusty-focus-ring-width) solid var(--dusty-focus-ring);
|
|
17
|
+
outline-offset: var(--dusty-focus-ring-offset);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.button:disabled,
|
|
21
|
+
.button[aria-disabled="true"]:not([data-loading]) {
|
|
22
|
+
opacity: 0.5;
|
|
23
|
+
cursor: not-allowed;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.button[data-loading] {
|
|
27
|
+
cursor: progress;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.label,
|
|
31
|
+
.spinner {
|
|
32
|
+
grid-area: 1 / 1;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.button[data-loading] .label {
|
|
36
|
+
opacity: 0;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.spinner {
|
|
40
|
+
box-sizing: border-box;
|
|
41
|
+
inline-size: 1em;
|
|
42
|
+
block-size: 1em;
|
|
43
|
+
border: 2px solid currentcolor;
|
|
44
|
+
border-inline-end-color: transparent;
|
|
45
|
+
border-radius: var(--dusty-radius-full);
|
|
46
|
+
animation: spin 0.8s linear infinite;
|
|
8
47
|
}
|
|
9
48
|
|
|
10
|
-
/* Primary: filled black */
|
|
11
49
|
.primary {
|
|
12
|
-
background-color: var(--
|
|
13
|
-
color: var(--
|
|
14
|
-
border: 1px solid var(--color-black);
|
|
50
|
+
background-color: var(--dusty-action);
|
|
51
|
+
color: var(--dusty-on-action);
|
|
15
52
|
}
|
|
16
53
|
|
|
17
|
-
.primary:
|
|
18
|
-
|
|
54
|
+
.primary:enabled:not([aria-disabled="true"]):active {
|
|
55
|
+
border-color: var(--dusty-action-active);
|
|
56
|
+
background-color: var(--dusty-action-active);
|
|
19
57
|
}
|
|
20
58
|
|
|
21
|
-
/* Secondary: outline black */
|
|
22
59
|
.secondary {
|
|
23
60
|
background-color: transparent;
|
|
24
|
-
color: var(--
|
|
25
|
-
border: 1px solid var(--color-black);
|
|
61
|
+
color: var(--dusty-action);
|
|
26
62
|
}
|
|
27
63
|
|
|
28
|
-
.secondary:
|
|
29
|
-
background-color: var(--
|
|
64
|
+
.secondary:enabled:not([aria-disabled="true"]):active {
|
|
65
|
+
background-color: var(--dusty-surface-selected);
|
|
30
66
|
}
|
|
31
67
|
|
|
32
|
-
|
|
33
|
-
.disabled {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
68
|
+
@media (hover: hover) {
|
|
69
|
+
.primary:enabled:not([aria-disabled="true"]):hover:not(:active) {
|
|
70
|
+
border-color: var(--dusty-action-hover);
|
|
71
|
+
background-color: var(--dusty-action-hover);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.secondary:enabled:not([aria-disabled="true"]):hover:not(:active) {
|
|
75
|
+
background-color: var(--dusty-surface-hover);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
@media (prefers-reduced-motion: reduce) {
|
|
80
|
+
.spinner {
|
|
81
|
+
animation: pulse 1.2s ease-in-out infinite alternate;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
@media (forced-colors: active) {
|
|
86
|
+
.button:disabled,
|
|
87
|
+
.button[aria-disabled="true"]:not([data-loading]) {
|
|
88
|
+
opacity: 1;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.spinner {
|
|
92
|
+
forced-color-adjust: none;
|
|
93
|
+
border-color: ButtonText;
|
|
94
|
+
border-inline-end-color: transparent;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
@keyframes spin {
|
|
99
|
+
to {
|
|
100
|
+
rotate: 1turn;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
@keyframes pulse {
|
|
105
|
+
to {
|
|
106
|
+
opacity: 0.3;
|
|
107
|
+
}
|
|
37
108
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { type ButtonProps } from "../Button/Button.js";
|
|
3
|
+
export interface IconButtonProps extends Omit<ButtonProps, "children" | "dangerouslySetInnerHTML" | "aria-label" | "aria-labelledby"> {
|
|
4
|
+
label: string;
|
|
5
|
+
icon: ReactNode;
|
|
6
|
+
}
|
|
7
|
+
export declare function IconButton({ label, icon, className, ...props }: IconButtonProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import clsx from "clsx";
|
|
3
|
+
import { Button } from "../Button/Button.js";
|
|
4
|
+
import visuallyHidden from "../styles/visuallyHidden.module.css";
|
|
5
|
+
import styles from "./IconButton.module.css";
|
|
6
|
+
export function IconButton({ label, icon, className, ...props }) {
|
|
7
|
+
return (_jsxs(Button, { ...props, className: clsx(styles.iconButton, className), children: [_jsx("span", { className: styles.icon, "aria-hidden": "true", children: icon }), _jsx("span", { className: visuallyHidden.visuallyHidden, children: label })] }));
|
|
8
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
.iconButton {
|
|
2
|
+
--button-padding-inline: 0.75rem;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
.icon {
|
|
6
|
+
display: flex;
|
|
7
|
+
align-items: center;
|
|
8
|
+
justify-content: center;
|
|
9
|
+
inline-size: 1.25em;
|
|
10
|
+
block-size: 1.25em;
|
|
11
|
+
line-height: 1;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.icon > svg {
|
|
15
|
+
inline-size: 100%;
|
|
16
|
+
block-size: 100%;
|
|
17
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { announce } from "../announcer/announcer.js";
|
|
4
|
+
import { Button } from "../Button/Button.js";
|
|
5
|
+
import { getCopyText } from "./getCopyText.js";
|
|
6
|
+
import { getValueText } from "./getValueText.js";
|
|
7
|
+
import styles from "./InspectorDetails.module.css";
|
|
8
|
+
const SHOWN_VALUE_LENGTH = 2000;
|
|
9
|
+
function getTypeText(node) {
|
|
10
|
+
return node.typeName ? `${node.kind} (${node.typeName})` : node.kind;
|
|
11
|
+
}
|
|
12
|
+
function getShownValue(node) {
|
|
13
|
+
const text = getValueText(node);
|
|
14
|
+
return text.length > SHOWN_VALUE_LENGTH
|
|
15
|
+
? `${text.slice(0, SHOWN_VALUE_LENGTH)}…`
|
|
16
|
+
: text;
|
|
17
|
+
}
|
|
18
|
+
export function InspectorDetails({ node }) {
|
|
19
|
+
const [feedback, setFeedback] = useState({ id: "", text: "" });
|
|
20
|
+
async function copy(text, name) {
|
|
21
|
+
let result = `${name} copied`;
|
|
22
|
+
try {
|
|
23
|
+
await navigator.clipboard.writeText(text);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
result = "Copying failed";
|
|
27
|
+
}
|
|
28
|
+
setFeedback({ id: node.id, text: result });
|
|
29
|
+
announce(result);
|
|
30
|
+
}
|
|
31
|
+
return (_jsxs("section", { "aria-label": "Selected value", className: styles.details, children: [_jsxs("dl", { className: styles.facts, children: [_jsx("dt", { children: "Path" }), _jsx("dd", { children: node.path }), _jsx("dt", { children: "Type" }), _jsx("dd", { children: getTypeText(node) }), node.childCount === 0 ? (_jsxs(_Fragment, { children: [_jsx("dt", { children: "Value" }), _jsx("dd", { className: styles.value, children: getShownValue(node) })] })) : null] }), _jsxs("div", { className: styles.actions, children: [_jsx(Button, { variant: "secondary", className: styles.copy, onClick: () => void copy(node.path, "Path"), children: "Copy path" }), _jsx(Button, { variant: "secondary", className: styles.copy, onClick: () => void copy(getCopyText(node), "Value"), children: "Copy value" }), _jsx("span", { className: styles.feedback, children: feedback.id === node.id ? feedback.text : "" })] })] }));
|
|
32
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
.details {
|
|
2
|
+
display: grid;
|
|
3
|
+
gap: 0.5rem;
|
|
4
|
+
padding-block-start: 0.5rem;
|
|
5
|
+
border-block-start: 1px solid var(--dusty-border);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.facts {
|
|
9
|
+
display: grid;
|
|
10
|
+
grid-template-columns: max-content minmax(0, 1fr);
|
|
11
|
+
gap: 0.25rem 0.75rem;
|
|
12
|
+
margin: 0;
|
|
13
|
+
font-family: var(--dusty-font-mono);
|
|
14
|
+
font-size: 0.8125rem;
|
|
15
|
+
line-height: 1.6;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.facts dt {
|
|
19
|
+
color: var(--dusty-text-muted);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.facts dd {
|
|
23
|
+
margin: 0;
|
|
24
|
+
overflow-wrap: anywhere;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.facts .value {
|
|
28
|
+
max-block-size: 8rem;
|
|
29
|
+
overflow-y: auto;
|
|
30
|
+
white-space: pre-wrap;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.actions {
|
|
34
|
+
display: flex;
|
|
35
|
+
flex-wrap: wrap;
|
|
36
|
+
gap: 0.5rem;
|
|
37
|
+
align-items: center;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.copy {
|
|
41
|
+
--button-padding-block: 0.375rem;
|
|
42
|
+
--button-padding-inline: 0.75rem;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.feedback {
|
|
46
|
+
color: var(--dusty-text-muted);
|
|
47
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
interface InspectorSearchProps {
|
|
2
|
+
query: string;
|
|
3
|
+
status: string;
|
|
4
|
+
hasMatches: boolean;
|
|
5
|
+
onQueryChange: (query: string) => void;
|
|
6
|
+
onMove: (step: 1 | -1) => void;
|
|
7
|
+
}
|
|
8
|
+
export declare function InspectorSearch({ query, status, hasMatches, onQueryChange, onMove, }: InspectorSearchProps): import("react").JSX.Element;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { IconButton } from "../IconButton/IconButton.js";
|
|
3
|
+
import styles from "./InspectorSearch.module.css";
|
|
4
|
+
export function InspectorSearch({ query, status, hasMatches, onQueryChange, onMove, }) {
|
|
5
|
+
function handleKeyDown(event) {
|
|
6
|
+
if (event.key === "Enter") {
|
|
7
|
+
event.preventDefault();
|
|
8
|
+
onMove(event.shiftKey ? -1 : 1);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
return (_jsxs("search", { className: styles.search, children: [_jsxs("label", { className: styles.field, children: [_jsx("span", { children: "Search" }), _jsx("input", { type: "search", className: styles.input, value: query, autoComplete: "off", spellCheck: false, onChange: (event) => onQueryChange(event.target.value), onKeyDown: handleKeyDown })] }), _jsx("span", { className: styles.status, children: status }), _jsx(IconButton, { variant: "secondary", className: styles.move, label: "Previous match", icon: "\u2191", "aria-disabled": !hasMatches, onClick: () => onMove(-1) }), _jsx(IconButton, { variant: "secondary", className: styles.move, label: "Next match", icon: "\u2193", "aria-disabled": !hasMatches, onClick: () => onMove(1) })] }));
|
|
12
|
+
}
|