Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/devtools/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ The widget renders nothing in production builds (`NODE_ENV === "production"`) un

`@openuidev/react-lang` ships with this package and auto-mounts the widget in development — no manual `<OpenUIDevtools />` needed. Mounting it manually still works (e.g. to customize props): only one instance ever renders, and a manually mounted instance takes precedence over the auto-mounted one.

In development, `createLibrary()` registers the live library with the widget. The **OpenUI Paste** banner at the bottom of the drawer widens the drawer into an editor against that library (host CSS included), with Render / Validation / Tree / JSON / Stream panels and simulated stream playback. A stream event's **Debug** button opens its response the same way. Eject moves the view into a separate window. The first visit opens a short step-by-step guide (also on **Help**); dismissing it is remembered.
**OpenUI Inspect** (the event drawer) and **OpenUI Debug** are independent tools on independent trays. Debug docks beside Inspect rather than growing out of it, and each closes on its own: dismissing Debug leaves the event list where it was, and vice versa.

In development, `createLibrary()` registers the live library with the widget. The **OpenUI Debug** banner at the bottom of the Inspect tray opens an editor against that library (host CSS included), with Render / Validation / Tree / JSON / Stream panels and simulated stream playback. A stream event's **Debug** button opens its response the same way. Eject moves the view into a separate window. The first visit opens a short step-by-step guide (also on **Help**); dismissing it is remembered.

Display filters ("auto-open on error", "errors only") and the theme live behind the gear in the drawer header. The theme is Light or Dark, chosen manually and remembered across reloads: nothing is auto-detected from the host page or the OS, and it styles the devtools chrome only — never your app. The floating Shiro toggle stays dark so the branded mark stays readable.

Expand Down
63 changes: 63 additions & 0 deletions packages/devtools/src/IconButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { useState, type ButtonHTMLAttributes, type CSSProperties } from "react";

/**
* The widget's square icon buttons — header actions, close crosses, the
* settings trigger. They sit on their tray with no chrome until pointed at,
* then take a fill; keeping that in one component means every one of them
* behaves the same without threading hover state through each call site.
*/
export function IconButton({
active = false,
outlined = false,
style,
...props
}: ButtonHTMLAttributes<HTMLButtonElement> & {
/** Held open, e.g. the settings trigger while its menu shows. */
active?: boolean;
/** Carries its own border, for buttons that sit on a busy surface. */
outlined?: boolean;
}) {
const [hovered, setHovered] = useState(false);
return (
<button
{...props}
style={{
...styles.button,
...(outlined ? styles.outlined : null),
...(hovered || active ? styles.filled : null),
...style,
}}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
/>
);
}

const styles = {
button: {
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
boxSizing: "border-box",
width: 26,
height: 26,
borderWidth: 1,
borderStyle: "solid",
borderColor: "transparent",
borderRadius: 8,
background: "transparent",
color: "var(--oui-dt-fg-muted)",
cursor: "pointer",
padding: 0,
transition: "background 150ms ease, color 150ms ease",
},
outlined: {
borderColor: "var(--oui-dt-control-border)",
background: "var(--oui-dt-control-bg)",
color: "var(--oui-dt-fg)",
},
filled: {
background: "var(--oui-dt-bg-subtle)",
color: "var(--oui-dt-fg)",
},
} satisfies Record<string, CSSProperties>;
30 changes: 18 additions & 12 deletions packages/devtools/src/LevelIcon.tsx
Original file line number Diff line number Diff line change
@@ -1,41 +1,43 @@
import { type ObservabilityEvent } from "@openuidev/observability";
import { Info, TriangleAlert, X } from "lucide-react";
import type { CSSProperties } from "react";
import { X } from "lucide-react";
import type { CSSProperties, ReactNode } from "react";

// The chip is already the container, so info is a bare letter rather than an
// icon that draws its own circle around one.
const BY_LEVEL = {
info: {
Icon: Info,
glyph: "i",
style: {
background: "var(--oui-dt-bg-subtle)",
background: "var(--oui-dt-level-neutral)",
color: "var(--oui-dt-fg-muted)",
},
},
warning: {
Icon: TriangleAlert,
glyph: <X size={11} strokeWidth={2.75} />,
style: {
background: "var(--oui-dt-warning-bg)",
background: "var(--oui-dt-level-warning)",
color: "var(--oui-dt-warning)",
},
},
error: {
Icon: X,
glyph: <X size={11} strokeWidth={2.75} />,
style: {
background: "var(--oui-dt-danger-bg)",
background: "var(--oui-dt-level-danger)",
color: "var(--oui-dt-danger)",
},
},
} satisfies Record<ObservabilityEvent["level"], { Icon: typeof Info; style: CSSProperties }>;
} satisfies Record<ObservabilityEvent["level"], { glyph: ReactNode; style: CSSProperties }>;

/**
* Severity as a colored glyph instead of a word, so the row header has room for
* the fields that actually differ between events. The level stays the accessible
* name, which is also what tests and screen readers read.
*/
export function LevelIcon({ level }: { level: ObservabilityEvent["level"] }) {
const { Icon, style } = BY_LEVEL[level];
const { glyph, style } = BY_LEVEL[level];
return (
<span style={{ ...styles.chip, ...style }} role="img" aria-label={level} title={level}>
<Icon size={11} strokeWidth={2.75} />
{glyph}
</span>
);
}
Expand All @@ -46,8 +48,12 @@ const styles = {
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
boxSizing: "border-box",
width: 18,
height: 18,
borderRadius: 999,
borderRadius: 6,
fontSize: 11,
fontWeight: 700,
lineHeight: 1,
},
} satisfies Record<string, CSSProperties>;
Loading