diff --git a/.changeset/dev-toolbar-error-stack-parser-es.md b/.changeset/dev-toolbar-error-stack-parser-es.md new file mode 100644 index 000000000..54c45ac7c --- /dev/null +++ b/.changeset/dev-toolbar-error-stack-parser-es.md @@ -0,0 +1,5 @@ +--- +"@solidjs/start": patch +--- + +Parse stack traces in the dev toolbar's error overlay with `error-stack-parser-es/lite` instead of `error-stack-parser`. The lite entry point is a much smaller, ESM-only parser that returns plain frame objects rather than class instances, and it no longer pulls in the `stackframe` package. Errors that carry no stack are now handled as an empty frame list instead of throwing. diff --git a/.changeset/dev-toolbar-redesign.md b/.changeset/dev-toolbar-redesign.md new file mode 100644 index 000000000..dd1a77cb8 --- /dev/null +++ b/.changeset/dev-toolbar-redesign.md @@ -0,0 +1,7 @@ +--- +"@solidjs/start": patch +--- + +Redesign the dev toolbar. The toolbar and its panels share a common set of design tokens, giving them a consistent dark palette, translucent surfaces, and elevation. The server function inspector is now a master-detail split with a persistent call list beside the request/response pane, and the error overlay places the stack frame list beside a code preview that fills the panel. Headers, form data, and URL search params render as aligned key-value tables, the hex viewer gains an offset gutter with the ASCII column aligned per row, and blobs are shown as file cards with their type and size. The seroval body inspector is now an expandable tree with collapsed previews, syntax-colored values, cycle detection, and live promise and stream state, replacing the previous column-based drill-down. + +Along with the redesign, the toolbar only starts a drag from the toolbar itself rather than from its panels, unhandled promise rejections are captured by the error overlay, the code preview shows more surrounding lines, and a stack frame whose source cannot be loaded now stays listed and reports that its source is unavailable instead of silently rendering nothing. diff --git a/.changeset/dev-toolbar-trace-mapping.md b/.changeset/dev-toolbar-trace-mapping.md new file mode 100644 index 000000000..b033fe74c --- /dev/null +++ b/.changeset/dev-toolbar-trace-mapping.md @@ -0,0 +1,5 @@ +--- +"@solidjs/start": patch +--- + +Use `@jridgewell/trace-mapping` instead of `source-map-js` to resolve original sources in the dev toolbar's error overlay. It decodes mappings lazily, so only the positions actually inspected are resolved, it is significantly smaller in the browser bundle, and it understands indexed source maps. diff --git a/packages/start/package.json b/packages/start/package.json index ce6186c95..7fc1fc45a 100644 --- a/packages/start/package.json +++ b/packages/start/package.json @@ -58,12 +58,13 @@ "@babel/core": "^7.29.7", "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7", + "@jridgewell/trace-mapping": "^0.3.31", "@solidjs/meta": "^0.29.4", "@types/babel__traverse": "^7.28.0", "@types/micromatch": "^4.0.10", "cookie-es": "^3.1.1", "defu": "^6.1.7", - "error-stack-parser": "^2.1.4", + "error-stack-parser-es": "^2.0.1", "fast-glob": "^3.3.3", "h3": "^2.0.1-rc.26", "html-to-image": "^1.11.13", @@ -76,9 +77,8 @@ "seroval-plugins": "^1.6.0", "shiki": "^4.3.1", "solid-js": "^1.9.15", - "source-map-js": "^1.2.1", "srvx": "^0.12.4", - "terracotta": "^1.1.1", + "terracotta": "^1.2.4", "vite-plugin-solid": "^2.11.13" }, "devDependencies": { diff --git a/packages/start/src/config/dev-toolbar-deps.spec.ts b/packages/start/src/config/dev-toolbar-deps.spec.ts deleted file mode 100644 index b6cf84dc8..000000000 --- a/packages/start/src/config/dev-toolbar-deps.spec.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import type { Plugin } from "vite"; -import { afterEach, expect, it } from "vitest"; - -import { solidStart, type SolidStartOptions } from "./index.ts"; - -const cwd = process.cwd(); -const roots: string[] = []; - -afterEach(() => { - process.chdir(cwd); - for (const root of roots.splice(0)) rmSync(root, { recursive: true }); -}); - -async function resolveDevConfig(options?: SolidStartOptions) { - const root = realpathSync.native(mkdtempSync(join(tmpdir(), "solid-start-dev-toolbar-deps-"))); - roots.push(root); - mkdirSync(join(root, "src")); - writeFileSync(join(root, "package.json"), "{}"); - writeFileSync(join(root, "src/app.tsx"), "export default function App() {}"); - process.chdir(root); - - const plugin = solidStart(options).find( - (candidate): candidate is Plugin => - typeof candidate === "object" && - candidate !== null && - "name" in candidate && - candidate.name === "solid-start:config", - ); - const config = plugin?.config; - const handler = typeof config === "function" ? config : config?.handler; - return (await handler?.call({} as never, {}, { command: "serve", mode: "development" })) as - | { environments?: { client?: { optimizeDeps?: { include?: string[] } } } } - | undefined; -} - -it("pre-bundles the dev toolbar's CommonJS dependencies", async () => { - const config = await resolveDevConfig(); - - expect(config?.environments?.client?.optimizeDeps?.include).toEqual([ - "@solidjs/start > source-map-js", - "@solidjs/start > error-stack-parser", - ]); -}); - -it("leaves them alone when the dev toolbar is disabled", async () => { - const config = await resolveDevConfig({ devOverlay: false }); - - expect(config?.environments?.client?.optimizeDeps).toBeUndefined(); -}); diff --git a/packages/start/src/config/index.ts b/packages/start/src/config/index.ts index 1793e1c56..9ad41a3f7 100644 --- a/packages/start/src/config/index.ts +++ b/packages/start/src/config/index.ts @@ -199,11 +199,6 @@ export interface SolidStartOptions { }; } -const DEV_TOOLBAR_COMMONJS_DEPENDENCIES = [ - "@solidjs/start > source-map-js", - "@solidjs/start > error-stack-parser", -]; - const absolute = (path: string, root: string) => path ? (isAbsolute(path) ? path : join(root, path)) : path; @@ -296,9 +291,6 @@ export function solidStart(options?: SolidStartOptions): Array { environments: { [VITE_ENVIRONMENTS.client]: { consumer: "client", - ...(start.devOverlay - ? { optimizeDeps: { include: DEV_TOOLBAR_COMMONJS_DEPENDENCIES } } - : {}), build: { write: true, manifest: true, diff --git a/packages/start/src/shared/dev-toolbar/error-viewer/CodeView.tsx b/packages/start/src/shared/dev-toolbar/error-viewer/CodeView.tsx index 2d1289d20..129a1d47f 100644 --- a/packages/start/src/shared/dev-toolbar/error-viewer/CodeView.tsx +++ b/packages/start/src/shared/dev-toolbar/error-viewer/CodeView.tsx @@ -28,7 +28,7 @@ export interface CodeViewProps { line: number; } -const RANGE = 8; +const RANGE = 15; export function CodeView(props: CodeViewProps): JSX.Element | null { const lines = () => @@ -51,9 +51,16 @@ export function CodeView(props: CodeViewProps): JSX.Element | null { async value => { const highlighter = await loadHighlighter(); const fileExtension = props.fileName.split(/[#?]/)[0]!.split(".").pop()?.trim(); - let lang = fileExtension as BuiltinLanguage; - if (fileExtension === "mjs" || fileExtension === "cjs") { - lang = "js"; + // Only these grammars are loaded — anything else would make shiki + // throw. Fall back to plain JS highlighting for unknown sources. + let lang: BuiltinLanguage = "js"; + if ( + fileExtension === "jsx" || + fileExtension === "ts" || + fileExtension === "tsx" || + fileExtension === "js" + ) { + lang = fileExtension; } return highlighter.codeToHtml(value, { theme: "dark-plus", diff --git a/packages/start/src/shared/dev-toolbar/error-viewer/create-stack-frame.ts b/packages/start/src/shared/dev-toolbar/error-viewer/create-stack-frame.ts index 926eae5d2..24538a821 100644 --- a/packages/start/src/shared/dev-toolbar/error-viewer/create-stack-frame.ts +++ b/packages/start/src/shared/dev-toolbar/error-viewer/create-stack-frame.ts @@ -1,3 +1,5 @@ +import { originalPositionFor, sourceContentFor } from "@jridgewell/trace-mapping"; +import type { StackFrameLite } from "error-stack-parser-es/lite"; import { type Accessor, createMemo, createResource } from "solid-js"; import getSourceMap from "./get-source-map.ts"; @@ -30,31 +32,39 @@ function getActualFileSource(path: string): string { return path; } -export function createStackFrame(stackframe: StackFrame, isCompiled: () => boolean) { +export function createStackFrame(stackframe: StackFrameLite, isCompiled: () => boolean) { const [data] = createResource( () => ({ - fileName: stackframe.fileName, - line: stackframe.lineNumber, - column: stackframe.columnNumber, - functionName: stackframe.functionName, + fileName: stackframe.file, + line: stackframe.line, + column: stackframe.col, + functionName: stackframe.function, }), async source => { if (!source.fileName) { return null; } - const url = getActualFileSource(source.fileName); - const response = await fetch(url); - if (!response.ok) { + // Sources can be unreachable — node internals, extension scripts, + // files outside the dev server's allowlist. Treat any failure as + // "no source" instead of throwing into the error boundary. + try { + const url = getActualFileSource(source.fileName); + const response = await fetch(url); + if (!response.ok) { + return null; + } + const content = await response.text(); + const sourceMap = await getSourceMap(url, content); + return { + source, + content, + sourceMap, + isServer: isServerSource(source.fileName), + }; + } catch (error) { + console.warn("[start dev toolbar] failed to load source for stack frame", error); return null; } - const content = await response.text(); - const sourceMap = await getSourceMap(url, content); - return { - source, - content, - sourceMap, - isServer: isServerSource(source.fileName), - }; }, ); @@ -69,9 +79,10 @@ export function createStackFrame(stackframe: StackFrame, isCompiled: () => boole if (isServer) { // The position is already original; only the original content needs // to be pulled out of the source map. - const originalContent = sourceMap.sources.length - ? sourceMap.sourceContentFor(sourceMap.sources[0]!, true) - : null; + const originalContent = + sourceMap.sources.length && sourceMap.sources[0] != null + ? sourceContentFor(sourceMap, sourceMap.sources[0]) + : null; if (originalContent) { return { source: source.fileName, @@ -82,14 +93,14 @@ export function createStackFrame(stackframe: StackFrame, isCompiled: () => boole } as StackFrameSource; } } else { - const result = sourceMap.originalPositionFor({ + const result = originalPositionFor(sourceMap, { line: source.line, column: source.column, }); if (result.source) { return { ...result, - content: sourceMap.sourceContentFor(result.source, true), + content: sourceContentFor(sourceMap, result.source), } as StackFrameSource; } } diff --git a/packages/start/src/shared/dev-toolbar/error-viewer/get-source-map.ts b/packages/start/src/shared/dev-toolbar/error-viewer/get-source-map.ts index 61d469e3c..9e1742845 100644 --- a/packages/start/src/shared/dev-toolbar/error-viewer/get-source-map.ts +++ b/packages/start/src/shared/dev-toolbar/error-viewer/get-source-map.ts @@ -1,13 +1,10 @@ -import { type RawSourceMap, SourceMapConsumer } from "source-map-js"; +import { AnyMap, type SourceMapInput, type TraceMap } from "@jridgewell/trace-mapping"; const INLINE_SOURCEMAP_REGEX = /^data:application\/json[^,]+base64,/; const SOURCEMAP_REGEX = /(?:\/\/[@#][ \t]+sourceMappingURL=([^\s'"]+?)[ \t]*$)|(?:\/\*[@#][ \t]+sourceMappingURL=([^*]+?)[ \t]*(?:\*\/)[ \t]*$)/; -export default async function getSourceMap( - url: string, - content: string, -): Promise { +export default async function getSourceMap(url: string, content: string): Promise { const lines = content.split("\n"); let sourceMapUrl: string | undefined; for (let i = lines.length - 1; i >= 0 && !sourceMapUrl; i--) { @@ -28,6 +25,7 @@ export default async function getSourceMap( sourceMapUrl = parsedURL.join("/"); } const response = await fetch(sourceMapUrl); - const rawSourceMap: RawSourceMap = await response.json(); - return new SourceMapConsumer(rawSourceMap); + const rawSourceMap: SourceMapInput = await response.json(); + // AnyMap also handles indexed ("sections") source maps + return new AnyMap(rawSourceMap); } diff --git a/packages/start/src/shared/dev-toolbar/error-viewer/index.tsx b/packages/start/src/shared/dev-toolbar/error-viewer/index.tsx index d1d0bfea0..a92740d9e 100644 --- a/packages/start/src/shared/dev-toolbar/error-viewer/index.tsx +++ b/packages/start/src/shared/dev-toolbar/error-viewer/index.tsx @@ -1,5 +1,5 @@ // @refresh skip -import ErrorStackParser from "error-stack-parser"; +import { parse as parseErrorStack, type StackFrameLite } from "error-stack-parser-es/lite"; import * as htmlToImage from "html-to-image"; import type { JSX } from "solid-js"; import { createMemo, createSignal, ErrorBoundary, For, Show, Suspense } from "solid-js"; @@ -80,68 +80,56 @@ function CodeFallback(): JSX.Element { ); } +interface RawStackFrameOptionProps { + frame: StackFrameLite; + disabled?: boolean; +} + +// Frame rendered from the raw (unmapped) stack info — used while the source +// is loading or when it is unreachable, so the frame never vanishes from +// the list. +function RawStackFrameOption(props: RawStackFrameOptionProps): JSX.Element { + const fileName = props.frame.file; + return ( + + + {props.frame.function ?? ""} + + + {fileName + ? getFilePath({ + source: fileName, + content: "", + line: props.frame.line!, + column: props.frame.col!, + name: props.frame.function, + }) + : ""} + + + ); +} + function StackFramesContent(props: StackFramesContentProps) { - const stackframes = ErrorStackParser.parse(props.error); + const stackframes = parseErrorStack(props.error, { allowEmpty: true }); const [selectedFrame, setSelectedFrame] = createSignal(stackframes[0]!); return (
-
- - {(() => { - const data = createStackFrame(selectedFrame(), () => props.isCompiled); - return ( - }> - }> - {source => ( - <> - {source.source} -
- -
- - )} -
-
- ); - })()} -
-
- + data-start-error-viewer-stack-frames value={selectedFrame()} onChange={setSelectedFrame} > {current => ( - - - {current.functionName ?? ""} - - - {getFilePath({ - source: current.getFileName()!, - content: "", - line: current.getLineNumber()!, - column: current.getColumnNumber()!, - name: current.getFunctionName(), - })} - - - } - > + }> {(() => { const data = createStackFrame(current, () => props.isCompiled); return ( - - + }> + }> {source => ( @@ -160,6 +148,40 @@ function StackFramesContent(props: StackFramesContentProps) { )} +
+ + {frame => ( + // Keyed on the frame so a failure inspecting one frame (an + // unreachable source, a failed highlight) resets when another + // frame is selected instead of leaving the pane stuck. + }> + {(() => { + const data = createStackFrame(frame, () => props.isCompiled); + return ( + }> + }> + {source => ( + <> + + {source.source} + +
+ +
+ + )} +
+
+ ); + })()} +
+ )} +
+
); } diff --git a/packages/start/src/shared/dev-toolbar/error-viewer/styles.css b/packages/start/src/shared/dev-toolbar/error-viewer/styles.css index 5a422cd8f..f4b7236cc 100644 --- a/packages/start/src/shared/dev-toolbar/error-viewer/styles.css +++ b/packages/start/src/shared/dev-toolbar/error-viewer/styles.css @@ -3,6 +3,8 @@ flex-direction: column; width: 100%; + height: 100%; + min-height: 0; } [data-start-error-viewer-navbar] { @@ -11,6 +13,16 @@ align-items: center; justify-content: space-between; gap: 0.5rem; + + padding: 0.5rem 0.75rem; + + position: sticky; + top: 0; + z-index: 1; + + background: var(--start-dt-bg-glass); + backdrop-filter: blur(16px) saturate(140%); + -webkit-backdrop-filter: blur(16px) saturate(140%); } [data-start-error-viewer-pagination] { @@ -20,8 +32,8 @@ justify-content: center; gap: 0.5rem; border-radius: 9999px; - color: rgb(249 250 251); - background-color: rgb(17 24 39); + color: var(--start-dt-text); + background-color: var(--start-dt-surface); padding: 0.25rem; } @@ -30,23 +42,23 @@ } [data-start-error-viewer-button]:hover { - color: rgb(229 231 235); - background-color: rgb(55 65 81); + color: var(--start-dt-text); + background-color: var(--start-dt-surface-hover); } [data-start-error-viewer-button]:focus { outline: 2px solid transparent; outline-offset: 2px; - outline-color: rgb(229 231 235); + outline-color: var(--start-dt-accent); } [data-start-error-viewer-button]:focus-visible { - box-shadow: 0 0 0 calc(3px) rgb(17 24 39 / 0.75); + box-shadow: 0 0 0 2px var(--start-dt-accent); } [data-start-error-viewer-button]:active { - color: rgb(243 244 246); - background-color: rgb(31 41 55); + color: var(--start-dt-accent); + background-color: var(--start-dt-surface-active); } [data-start-error-viewer-button] > svg { @@ -55,9 +67,11 @@ } [data-start-error-viewer-page-counter] { - font-size: 0.875rem; + font-size: 0.8125rem; line-height: 1.25rem; font-weight: 600; + font-variant-numeric: tabular-nums; + color: var(--start-dt-text-muted); } [data-start-error-viewer-navbar-left] { @@ -73,7 +87,7 @@ justify-content: center; gap: 0.25rem; border-radius: 9999px; - color: rgb(249 250 251); + color: var(--start-dt-text); padding: 0.25rem; } @@ -83,9 +97,13 @@ gap: 0.75rem; padding: 1rem; + flex: 1; + min-height: 0; + overflow: hidden; + border-top-width: 1px; border-top-style: solid; - border-top-color: oklch(87% 0.065 274.039); + border-top-color: var(--start-dt-border-soft); } [data-start-error-viewer-error-info] { @@ -95,107 +113,137 @@ } [data-start-error-viewer-error-info-name] { - font-weight: 700; - color: rgb(249 250 251); + font-weight: 600; + font-size: 0.8125rem; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--start-dt-text-muted); } [data-start-error-viewer-error-info-message] { - font-size: 1.5rem; - line-height: 2rem; - color: rgb(239 68 68); + font-size: 1.25rem; + line-height: 1.75rem; + color: var(--start-dt-danger); font-weight: 700; + font-family: + ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", + monospace; } [data-start-error-viewer-stack-frames-content] { display: flex; - flex-direction: column; + flex-direction: row; gap: 0.75rem; + + flex: 1; + min-height: 0; } [data-start-error-viewer-stack-frames] { - flex: 1 1 0%; - max-height: 16rem; + width: 38%; + max-width: 20rem; + flex-shrink: 0; overflow-y: auto; + + border: 1px var(--start-dt-border-soft) solid; + border-radius: 0.5rem; } [data-start-error-viewer-stack-frame] { display: flex; - flex-direction: row; - align-items: center; - justify-content: space-between; - gap: 1rem; - color: rgb(249 250 251); + flex-direction: column; + align-items: flex-start; + gap: 0.125rem; + color: var(--start-dt-text); cursor: default; user-select: none; padding-top: 0.5rem; padding-bottom: 0.5rem; - padding-left: 1rem; - padding-right: 1rem; + padding-left: 0.75rem; + padding-right: 0.75rem; font-size: 0.875rem; line-height: 1.25rem; outline: none; + + min-width: 0; + max-width: 100%; } [data-start-error-viewer-stack-frame][tc-active], [data-start-error-viewer-stack-frame][tc-selected], [data-start-error-viewer-stack-frame]:focus { - color: rgb(219 234 254); - background-color: rgb(30 58 138); + color: var(--start-dt-text); + background-color: var(--start-dt-accent-soft); + border-left-color: var(--start-dt-accent); } [data-start-error-viewer-stack-frame-function] { - font-weight: 700; + font-weight: 600; + font-family: + ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", + monospace; } [data-start-error-viewer-stack-frame-file] { white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 100%; font-size: 0.75rem; - /* 12px */ line-height: 1rem; - /* 16px */ + color: var(--start-dt-text-muted); } [data-start-error-viewer-stack-frames-code] { - padding: 0.5rem 1rem; - gap: 0.25rem; + padding: 0.5rem 0.75rem; + gap: 0.375rem; flex: 1 1 0%; + min-width: 0; + min-height: 0; display: flex; flex-direction: column; - align-items: center; - justify-content: center; + align-items: stretch; + justify-content: flex-start; border-radius: 0.5rem; - background-color: rgb(17 24 39); + background-color: var(--start-dt-surface); + border: 1px var(--start-dt-border-soft) solid; } [data-start-error-viewer-stack-frames-code-fallback] { display: flex; align-items: center; justify-content: center; - color: rgb(249 250 251); - min-height: 16rem; + color: var(--start-dt-text-muted); + flex: 1; + min-height: 8rem; font-size: 0.875rem; line-height: 1.25rem; } [data-start-error-viewer-stack-frames-code-source] { - font-size: 0.875rem; + font-size: 0.8125rem; line-height: 1.25rem; - color: rgb(249 250 251); + color: var(--start-dt-text-muted); + font-family: + ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", + monospace; } [data-start-error-viewer-stack-frames-code-container] { width: 100%; overflow: hidden; min-width: 100%; - max-height: 16rem; + flex: 1; + min-height: 0; border-top-width: 1px; border-top-style: solid; - border-top-color: oklch(87% 0.065 274.039); + border-top-color: var(--start-dt-border-soft); } [data-start-error-viewer-code-view] { overflow: auto; min-width: 100%; + height: 100%; } [data-start-error-viewer-code-view] > .shiki { @@ -220,5 +268,5 @@ } [data-start-error-viewer-error-line] { - background-color: rgba(239, 68, 68, 0.4); + background-color: oklch(0.63 0.21 25 / 0.25); } diff --git a/packages/start/src/shared/dev-toolbar/functions/BlobViewer.css b/packages/start/src/shared/dev-toolbar/functions/BlobViewer.css index 01ff66a87..863ff19c4 100644 --- a/packages/start/src/shared/dev-toolbar/functions/BlobViewer.css +++ b/packages/start/src/shared/dev-toolbar/functions/BlobViewer.css @@ -1,4 +1,52 @@ +[data-start-blob-viewer] { + display: flex; + align-items: center; + gap: 0.625rem; + + align-self: flex-start; + + padding: 0.5rem 0.75rem; + text-align: left; +} + [data-start-blob-viewer] svg { - width: 1rem; - height: 1rem; + width: 1.5rem; + height: 1.5rem; + flex-shrink: 0; + + color: var(--start-dt-accent, currentColor); +} + +[data-start-blob-viewer-info] { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 0.25rem; + + min-width: 0; +} + +[data-start-blob-viewer-name] { + font-family: + ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", + monospace; + font-size: 0.8125rem; + line-height: 1rem; + font-weight: 600; + + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +[data-start-blob-viewer-meta] { + display: flex; + align-items: center; + gap: 0.5rem; + + font-size: 0.75rem; + line-height: 1rem; + + color: var(--start-dt-text-muted, inherit); } diff --git a/packages/start/src/shared/dev-toolbar/functions/BlobViewer.tsx b/packages/start/src/shared/dev-toolbar/functions/BlobViewer.tsx index 2a1bd2f03..e5384f338 100644 --- a/packages/start/src/shared/dev-toolbar/functions/BlobViewer.tsx +++ b/packages/start/src/shared/dev-toolbar/functions/BlobViewer.tsx @@ -28,6 +28,16 @@ interface BlobViewerInnerProps { source: File | Blob; } +function formatSize(size: number): string { + if (size < 1024) { + return `${size} B`; + } + if (size < 1024 * 1024) { + return `${(size / 1024).toFixed(1)} KB`; + } + return `${(size / (1024 * 1024)).toFixed(1)} MB`; +} + function BlobViewerInner(props: BlobViewerInnerProps): JSX.Element { const fileURL = createMemo(() => URL.createObjectURL(props.source)); @@ -45,16 +55,23 @@ function BlobViewerInner(props: BlobViewerInnerProps): JSX.Element { document.body.removeChild(link); } + const name = createMemo(() => { + if (props.source instanceof File) { + return props.source.name; + } + return props.source.type || "Blob"; + }); + return ( ); } diff --git a/packages/start/src/shared/dev-toolbar/functions/FormDataViewer.tsx b/packages/start/src/shared/dev-toolbar/functions/FormDataViewer.tsx index 3334561e6..fe1bc60e9 100644 --- a/packages/start/src/shared/dev-toolbar/functions/FormDataViewer.tsx +++ b/packages/start/src/shared/dev-toolbar/functions/FormDataViewer.tsx @@ -1,7 +1,7 @@ import { createResource, For, type JSX, Show, Suspense } from "solid-js"; import { Section } from "../../ui/Section.tsx"; +import { Text } from "../../ui/Text.tsx"; import { BlobViewer } from "./BlobViewer.tsx"; -import { PropertySeparator, SerovalValue } from "./SerovalValue.tsx"; interface FormDataViewerInnerProps { source: FormData; @@ -10,14 +10,20 @@ interface FormDataViewerInnerProps { function FormDataViewerInner(props: FormDataViewerInnerProps): JSX.Element { return (
-
+
{([key, value]) => ( -
- - +
+ + {key} + {typeof value === "string" ? ( - + + {JSON.stringify(value)} + ) : ( )} diff --git a/packages/start/src/shared/dev-toolbar/functions/HeadersViewer.css b/packages/start/src/shared/dev-toolbar/functions/HeadersViewer.css index e6b8c1c4d..8e5ea0ac5 100644 --- a/packages/start/src/shared/dev-toolbar/functions/HeadersViewer.css +++ b/packages/start/src/shared/dev-toolbar/functions/HeadersViewer.css @@ -2,7 +2,3 @@ font-size: 0.75rem; line-height: 1rem; } - -[data-start-headers-viewer] > [data-start-property] > *:first-child { - text-transform: capitalize; -} diff --git a/packages/start/src/shared/dev-toolbar/functions/HeadersViewer.tsx b/packages/start/src/shared/dev-toolbar/functions/HeadersViewer.tsx index a8712b702..f25f89858 100644 --- a/packages/start/src/shared/dev-toolbar/functions/HeadersViewer.tsx +++ b/packages/start/src/shared/dev-toolbar/functions/HeadersViewer.tsx @@ -1,8 +1,7 @@ import { For } from "solid-js"; -import { PropertySeparator, SerovalValue } from "./SerovalValue.tsx"; +import { Text } from "../../ui/Text.tsx"; import "./HeadersViewer.css"; -import { Text } from "../../ui/Text.tsx"; interface HeadersViewerProps { headers: Headers; @@ -10,13 +9,19 @@ interface HeadersViewerProps { export function HeadersViewer(props: HeadersViewerProps) { return ( -
+
{([key, value]) => ( -
- {key} - - +
+ + {key} + + + {value} +
)} diff --git a/packages/start/src/shared/dev-toolbar/functions/HexViewer.css b/packages/start/src/shared/dev-toolbar/functions/HexViewer.css index 9e2a9f996..48c6db579 100644 --- a/packages/start/src/shared/dev-toolbar/functions/HexViewer.css +++ b/packages/start/src/shared/dev-toolbar/functions/HexViewer.css @@ -1,34 +1,53 @@ [data-start-hex-viewer] { - display: flex; - border: 1px oklch(87% 0.065 274.039) solid; - overflow: auto; -} - -[data-start-hex-viewer-bytes] { - flex: 1; display: flex; flex-direction: column; - gap: 0.5rem; - padding: 0.5rem; -} -[data-start-hex-viewer-text] { - flex: 1; - display: flex; - flex-direction: column; - gap: 0.5rem; - padding: 0.5rem; - border-left: 1px oklch(87% 0.065 274.039) solid; + border: 1px var(--start-dt-border-soft, oklch(87% 0.065 274.039)) solid; + border-radius: 0.5rem; + background-color: var(--start-dt-surface, transparent); + + overflow: auto; + + font-family: + ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", + monospace; } [data-start-hex-row] { display: flex; align-items: center; gap: 1rem; + + padding: 0.125rem 0.625rem; + + white-space: nowrap; +} + +[data-start-hex-row]:nth-child(even) { + background-color: oklch(1 0 0 / 0.03); +} + +[data-start-hex-row]:hover { + background-color: var(--start-dt-surface-hover, transparent); +} + +[data-start-hex-offset] { + color: var(--start-dt-text-muted, inherit); +} + +[data-start-hex-chunk] { + letter-spacing: 0.05em; + color: var(--start-dt-text, inherit); } [data-start-hex-text] { display: flex; align-items: center; justify-content: start; + + margin-left: auto; + padding-left: 1rem; + border-left: 1px var(--start-dt-border-soft, oklch(87% 0.065 274.039)) solid; + + color: oklch(0.78 0.11 150); } diff --git a/packages/start/src/shared/dev-toolbar/functions/HexViewer.tsx b/packages/start/src/shared/dev-toolbar/functions/HexViewer.tsx index 020e6fe69..44f3546d6 100644 --- a/packages/start/src/shared/dev-toolbar/functions/HexViewer.tsx +++ b/packages/start/src/shared/dev-toolbar/functions/HexViewer.tsx @@ -23,7 +23,11 @@ function HexChunk(props: HexViewerInnerProps) { ); } -function HexRow(props: HexViewerInnerProps) { +interface HexRowProps extends HexViewerInnerProps { + offset: number; +} + +function HexRow(props: HexRowProps) { const chunk1 = createMemo(() => props.bytes.subarray(0, 4)); const chunk2 = createMemo(() => props.bytes.subarray(4, 8)); const chunk3 = createMemo(() => props.bytes.subarray(8, 12)); @@ -31,10 +35,14 @@ function HexRow(props: HexViewerInnerProps) { return (
+ + {toHex(props.offset, 8)} + +
); } @@ -76,12 +84,9 @@ export function HexViewerInner(props: HexViewerInnerProps): JSX.Element { return (
-
- {current => } -
-
- {current => } -
+ + {(current, index) => } +
); } diff --git a/packages/start/src/shared/dev-toolbar/functions/SerovalValue.css b/packages/start/src/shared/dev-toolbar/functions/SerovalValue.css index 17a44b954..0cd0f58d8 100644 --- a/packages/start/src/shared/dev-toolbar/functions/SerovalValue.css +++ b/packages/start/src/shared/dev-toolbar/functions/SerovalValue.css @@ -3,3 +3,24 @@ gap: 0.25rem; align-items: center; } + +[data-start-seroval-value="key"] { + color: oklch(0.78 0.1 305); +} + +[data-start-seroval-value="string"] { + color: oklch(0.78 0.11 150); +} + +[data-start-seroval-value="number"] { + color: oklch(0.8 0.11 80); +} + +[data-start-seroval-value="keyword"] { + color: oklch(0.74 0.1 250); + font-style: italic; +} + +[data-start-seroval-separator] { + color: var(--start-dt-text-muted); +} diff --git a/packages/start/src/shared/dev-toolbar/functions/SerovalValue.tsx b/packages/start/src/shared/dev-toolbar/functions/SerovalValue.tsx index 4685a97ea..85fdd0462 100644 --- a/packages/start/src/shared/dev-toolbar/functions/SerovalValue.tsx +++ b/packages/start/src/shared/dev-toolbar/functions/SerovalValue.tsx @@ -3,16 +3,24 @@ import "./SerovalValue.css"; interface SerovalValueProps { value: string | number | boolean | undefined | null; + kind?: "key" | "string" | "number" | "keyword"; } export function SerovalValue(props: SerovalValueProps) { return ( - + {`${props.value}`} ); } export function PropertySeparator() { - return :; + return ( + + : + + ); } diff --git a/packages/start/src/shared/dev-toolbar/functions/SerovalViewer.css b/packages/start/src/shared/dev-toolbar/functions/SerovalViewer.css index f893feded..b7649bf88 100644 --- a/packages/start/src/shared/dev-toolbar/functions/SerovalViewer.css +++ b/packages/start/src/shared/dev-toolbar/functions/SerovalViewer.css @@ -1,70 +1,132 @@ [data-start-seroval-viewer] { - display: grid; - grid-template-columns: 1fr 2fr; + height: 100%; + min-height: 12rem; + + overflow: auto; + + padding: 0.375rem 0.5rem; + + border: 1px var(--start-dt-border-soft, oklch(87% 0.065 274.039)) solid; + border-radius: 0.5rem; + background-color: var(--start-dt-surface, transparent); + + color: var(--start-dt-text, rgb(249 250 251)); + font-family: + ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", + monospace; } -[data-start-seroval-promise] { +[data-start-seroval-tree-node] { display: flex; - gap: 0.5rem; - align-items: center; + flex-direction: column; } -[data-start-seroval-renderer] { +[data-start-seroval-tree-row] { display: flex; - border-top: 1px oklch(87% 0.065 274.039) solid; - border-bottom: 1px oklch(87% 0.065 274.039) solid; + align-items: center; + gap: 0.375rem; + + padding: 0.125rem 0.25rem; + border-radius: 0.25rem; + + min-width: 0; + width: 100%; + + border: none; + background: none; + color: inherit; + font: inherit; + text-align: left; + outline: none; } -[data-start-seroval-renderer] > * { - border-left: 1px oklch(87% 0.065 274.039) solid; +button[data-start-seroval-tree-row] { + cursor: pointer; } -[data-start-seroval-renderer]:last-child { - border-right: 1px oklch(87% 0.065 274.039) solid; +button[data-start-seroval-tree-row]:hover, +button[data-start-seroval-tree-row]:focus-visible { + background-color: var(--start-dt-surface-hover, transparent); } -[data-start-seroval-node] { - display: flex; - flex-direction: column; - gap: 0.25rem; - padding: 0.25rem; +[data-start-seroval-tree-chevron] { + display: inline-flex; + align-items: center; + justify-content: center; - width: 100%; - max-width: 16rem; + width: 0.875rem; + flex-shrink: 0; + + color: var(--start-dt-text-muted, inherit); } -[data-start-seroval-node-header] { - position: sticky; - top: 0; - display: flex; - align-items: center; - justify-content: space-between; - color: rgb(249 250 251); +[data-start-seroval-tree-chevron]::before { + content: ""; + + width: 0.3125rem; + height: 0.3125rem; + + border-right: 1.5px currentColor solid; + border-bottom: 1.5px currentColor solid; + + transform: rotate(-45deg); + transition: transform 150ms cubic-bezier(0.4, 0, 0.2, 1); +} + +[data-start-seroval-tree-chevron][data-leaf]::before { + content: none; +} + +[data-start-seroval-tree-row][data-expanded] > [data-start-seroval-tree-chevron]::before { + transform: rotate(45deg); } -[data-start-seroval-node-content] { +[data-start-seroval-tree-children] { display: flex; flex-direction: column; - gap: 0.25rem; -} -[data-start-seroval-value] { - color: rgb(249 250 251); + margin-left: 0.6875rem; + padding-left: 0.625rem; + + border-left: 1px var(--start-dt-border-soft, oklch(87% 0.065 274.039)) solid; } -[data-start-seroval-value-wrapper] { +[data-start-seroval-tree-key] { display: inline-flex; - gap: 0.25rem; align-items: center; + gap: 0.125rem; + + flex-shrink: 0; } -[data-start-seroval-link] { +[data-start-seroval-tree-preview] { + color: var(--start-dt-text-muted, inherit); + font-size: 0.75rem; + line-height: 1rem; + + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +[data-start-seroval-tree-circular] { display: inline-flex; - gap: 0.25rem; align-items: center; + gap: 0.25rem; } -[data-start-seroval-link] > svg { +[data-start-seroval-tree-circular] > svg { width: 1rem; height: 1rem; + + color: var(--start-dt-accent, currentColor); +} + +[data-start-seroval-tree-raw] { + padding: 0.25rem 0; +} + +[data-start-seroval-value="plain"] { + color: var(--start-dt-text, rgb(249 250 251)); } diff --git a/packages/start/src/shared/dev-toolbar/functions/SerovalViewer.tsx b/packages/start/src/shared/dev-toolbar/functions/SerovalViewer.tsx index e6af11736..84ae6a522 100644 --- a/packages/start/src/shared/dev-toolbar/functions/SerovalViewer.tsx +++ b/packages/start/src/shared/dev-toolbar/functions/SerovalViewer.tsx @@ -1,9 +1,7 @@ import type { SerovalNode } from "seroval"; -import { createEffect, createSignal, For, type JSX, Show, splitProps } from "solid-js"; +import { createEffect, createSignal, For, type JSX, Show } from "solid-js"; import { Badge } from "../../ui/Badge.tsx"; -import { Cascade, CascadeOption } from "../../ui/Cascade.tsx"; -import { Section } from "../../ui/Section.tsx"; import { HexViewer } from "./HexViewer.tsx"; import { PropertySeparator, SerovalValue } from "./SerovalValue.tsx"; @@ -146,7 +144,7 @@ function getNodeType(node: SerovalNode) { case 35: return "Sequence"; } - throw new Error("unsupported node type"); + return "unknown"; } function traverse(node: SerovalNode, handler: (node: SerovalNode) => void): void { @@ -401,272 +399,416 @@ function getStreamKeyword(t: 32 | 33 | 34): string { } } -function renderSerovalNode( - ctx: RenderContext, - node: SerovalNode, - onSelect: (index: number | undefined) => void, - inner?: boolean, -): JSX.Element { - if ( - node.t >= 4 && - (inner || node.t === 4) && - node.i != null && - !(node.t === 5 || node.t === 6 || node.t === 17) - ) { +const PREVIEW_LENGTH = 32; +const PREVIEW_KEYS = 3; + +function truncate(value: string): string { + if (value.length > PREVIEW_LENGTH) { + return `${value.slice(0, PREVIEW_LENGTH)}…`; + } + return value; +} + +function previewNode(ctx: RenderContext, node: SerovalNode, depth: number): string { + switch (node.t) { + case 0: + return `${node.s}`; + case 1: + return truncate(`"${node.s}"`); + case 2: + return getConstantValue(node.s); + case 3: + return `${node.s}n`; + case 4: { + if (depth >= 1) { + return `#${node.i}`; + } + const target = ctx.getNode(node.i); + return target ? previewNode(ctx, target, depth + 1) : `#${node.i}`; + } + case 5: + return truncate(`${node.s}`); + case 6: + return truncate(`/${node.c}/${node.m}`); + case 7: + return `Set(${node.a.length})`; + case 8: + return `Map(${node.e.k.length})`; + case 9: + return `Array(${node.a.length})`; + case 10: + case 11: { + if (depth >= 1) { + return "{…}"; + } + const keys = node.p.k.filter(key => typeof key === "string").slice(0, PREVIEW_KEYS); + const rest = node.p.k.length > keys.length ? ", …" : ""; + return keys.length ? `{${keys.join(", ")}${rest}}` : "{}"; + } + case 12: + return "Promise"; + case 13: + case 14: + return truncate(`${getNodeType(node)}: ${node.m}`); + case 15: + case 16: + return `${node.c}(${node.l})`; + case 17: + return getSymbolValue(node.s); + case 19: + return "ArrayBuffer"; + case 20: + return `DataView(${node.l})`; + case 21: + return previewNode(ctx, node.f, depth); + case 22: { + const result = ctx.getPromise(node.s); + if (result) { + return `Promise<${result.t === 23 ? "success" : "failure"}>`; + } + return "Promise"; + } + case 25: + return `Plugin(${node.c})`; + case 28: + return "Iterator"; + case 30: + return "AsyncIterator"; + case 31: + return `Stream(${(ctx.getStream(node.i) || []).length})`; + case 35: + return `Sequence(${node.a.length})`; + default: + return getNodeType(node); + } +} + +interface EntryKeyProps { + value: string | number; + kind?: "key" | "number" | "keyword"; +} + +function EntryKey(props: EntryKeyProps): JSX.Element { + return ( + + + + + ); +} + +interface LeafRowProps { + label?: JSX.Element; + children: JSX.Element; +} + +function LeafRow(props: LeafRowProps): JSX.Element { + return ( +
+
+ + {props.label} + {props.children} +
+
+ ); +} + +interface ExpandableRowProps { + label?: JSX.Element; + badges?: JSX.Element; + preview: JSX.Element; + open?: boolean; + children: JSX.Element; +} + +function ExpandableRow(props: ExpandableRowProps): JSX.Element { + const [open, setOpen] = createSignal(props.open ?? false); + return ( +
+ + +
{props.children}
+
+
+ ); +} + +interface TreeValueProps { + ctx: RenderContext; + node: SerovalNode; + seen: number[]; + label?: JSX.Element; + open?: boolean; +} + +function TreeValue(props: TreeValueProps): JSX.Element { + const ctx = props.ctx; + const node = props.node; + + // Indexed reference: resolve reactively, guard cycles + if (node.t === 4) { const index = node.i; - const description = `id: ${index}`; - const lookup = ctx.getNode(index)!; + if (props.seen.includes(index)) { + return ( + + + + {`circular #${index}`} + + + ); + } return ( - - - {getNodeType(lookup)} - {description} - + + {`#${index} pending`} + + } + > + {target => ( + + )} + ); } + + const seen = node.i != null ? [...props.seen, node.i] : props.seen; + const flag = "o" in node ? getObjectFlag(node.o ?? 0) : "none"; + const badges = ( + <> + {node.i != null && {`#${node.i}`}} + {flag !== "none" && {flag}} + + ); + switch (node.t) { // Number = 0, case 0: - return ; + return ( + + + + ); // String = 1, case 1: - return ; + return ( + + + + ); // Constant = 2, case 2: - return ; + return ( + + + + ); // BigInt = 3, case 3: return ( -
- bigint - -
+ + + ); // Date = 5, case 5: return ( -
+ Date - -
+ + ); // RegExp = 6, case 6: return ( -
+ RegExp - -
+ + + ); + // WKSymbol = 17, + case 17: + return ( + + + ); // Set = 7, case 7: return ( - <> -
-
- - - -
-
-
- - data-start-properties - defaultValue={undefined} - onChange={onSelect} - > - [index, node] as const)}> - {([key, value]) => ( -
- - - {renderSerovalNode(ctx, value, onSelect, true)} -
- )} -
- -
- + + + {(child, index) => ( + } + /> + )} + + ); // Map = 8, case 8: return ( - <> -
-
- - - -
-
-
- - data-start-properties - defaultValue={undefined} - onChange={onSelect} - > - }> - {([key, value]) => ( -
- {renderSerovalNode(ctx, key, onSelect, true)} - - {renderSerovalNode(ctx, value, onSelect, true)} -
- )} -
- -
- + + + {([key, value], index) => ( + } + preview={`{${previewNode(ctx, key, 1)} => ${previewNode(ctx, value, 1)}}`} + > + } + /> + } + /> + + )} + + ); // Array = 9, case 9: return ( - <> -
-
- - - -
-
- - - {getObjectFlag(node.o)} -
-
-
- - data-start-properties - defaultValue={undefined} - onChange={onSelect} - > - [index, node] as const)} - fallback={} - > - {([key, value]) => ( -
- - - {value === 0 ? ( - - ) : ( - renderSerovalNode(ctx, value, onSelect, true) - )} -
- )} -
- -
- + + + {(child, index) => + child === 0 ? ( + }> + + + ) : ( + } + /> + ) + } + + ); // Object = 10, case 10: // NullConstructor = 11, case 11: return ( - <> -
-
- - - -
-
- - - {getObjectFlag(node.o)} -
-
-
- - data-start-properties - defaultValue={undefined} - onChange={onSelect} - > - }> - {([key, value]) => ( -
- {typeof key === "string" ? ( - - ) : ( - renderSerovalNode(ctx, key, onSelect, true) - )} - - {renderSerovalNode(ctx, value, onSelect, true)} -
- )} -
- -
- + + + {([key, value]) => ( + + } + /> + )} + + ); // Promise = 12, case 12: return ( - - data-start-properties - defaultValue={undefined} - onChange={onSelect} + - {renderSerovalNode(ctx, node.f, onSelect, true)} - + } + /> + ); // Error = 13, case 13: // AggregateError = 14, case 14: return ( - <> -
-
- - - -
-
+ + }> + + - {current => ( -
- - data-start-properties - defaultValue={undefined} - onChange={onSelect} - > - } - > - {([key, value]) => ( -
- {typeof key === "string" ? ( - - ) : ( - renderSerovalNode(ctx, key, onSelect, true) - )} - - {renderSerovalNode(ctx, value, onSelect, true)} -
- )} -
- -
+ {properties => ( + + {([key, value]) => ( + + } + /> + )} + )}
- +
); - // WKSymbol = 17, - case 17: - return ; - // Reference = 18, - case 18: - break; - // ArrayBuffer = 19, - case 19: { - const data = atob(node.s); - const result = new TextEncoder().encode(data); - return ; - } // TypedArray = 15, case 15: // BigIntTypedArray = 16, @@ -674,207 +816,200 @@ function renderSerovalNode( // DataView = 20, case 20: return ( - <> -
-
- - - -
-
- - - -
-
-
- - data-start-properties - defaultValue={undefined} - onChange={onSelect} - > - {renderSerovalNode(ctx, node.f, onSelect, true)} - -
- + + }> + + + }> + + + } + /> + + ); + // ArrayBuffer = 19, + case 19: + return ( + +
+ {(() => { + const data = atob(node.s); + const result = new TextEncoder().encode(data); + return ; + })()} +
+
); // Boxed = 21, case 21: return ( - - data-start-properties - defaultValue={undefined} - onChange={onSelect} + - {renderSerovalNode(ctx, node.f, onSelect, true)} - + } + /> + ); + // PromiseConstructor = 22, case 22: return ( - <> - {(() => { - const result = ctx.getPromise(node.s); - if (result) { - const status = result.t === 23 ? "success" : ("failure" as const); - return ( - - data-start-properties - defaultValue={undefined} - onChange={onSelect} - > -
- - - {status} -
- - - - {renderSerovalNode(ctx, result.a[1], onSelect, true)} - - - ); + {previewNode(ctx, node, 0)}} + open={props.open} + > + }> + pending + } - return pending; - })()} - + > + {result => ( + <> + }> + + {result.t === 23 ? "success" : "failure"} + + + } + /> + + )} + + ); - // Plugin = 25 + // Plugin = 25, case 25: return ( - <> -
-
- - - -
-
-
- - data-start-properties - defaultValue={undefined} - onChange={onSelect} - > - }> - {([key, value]) => ( -
- - - {renderSerovalNode(ctx, value, onSelect, true)} -
- )} -
- -
- + + + {([key, value]) => ( + } /> + )} + + ); - // IteratorFactory = 27, - case 27: - break; // IteratorFactoryInstance = 28, case 28: - return renderSerovalNode(ctx, node.a[1], onSelect, true); - // AsyncIteratorFactory = 29, - case 29: - break; // AsyncIteratorFactoryInstance = 30, case 30: - return renderSerovalNode(ctx, node.a[1], onSelect, true); + return ( + + } + /> + + ); // StreamConstructor = 31, case 31: return ( - <> - {(() => { - const result = ctx.getStream(node.i) || []; - return ( - - data-start-properties - defaultValue={undefined} - onChange={onSelect} - > - }> - {current => ( -
- - - {renderSerovalNode(ctx, current.f, onSelect, true)} -
- )} -
- - ); - })()} - + {previewNode(ctx, node, 0)}} + open={props.open} + > + }> + waiting + + } + > + {chunk => ( + } + /> + )} + + ); case 35: return ( - - data-start-properties - defaultValue={undefined} - onChange={onSelect} + - }> - {(current, index) => ( -
- - - {renderSerovalNode(ctx, current, onSelect, true)} -
+ + {(child, index) => ( + + {current => ( + + } + /> + )} + )} - +
+ ); + default: + return ( + + {getNodeType(node)} + ); } } -interface SerovalNodeRendererProps extends RenderContext { - node: SerovalNode; -} - -function SerovalNodeRenderer(props: SerovalNodeRendererProps): JSX.Element { - const [, rest] = splitProps(props, ["node"]); - const [next, setNext] = createSignal(); - - function onSelect(index: number | undefined) { - if (index == null) { - setNext(undefined); - } else { - setNext(props.getNode(index)); - } - } - - return ( - <> -
-
- {getNodeType(props.node)} - {props.node.i != null && {`id: ${props.node.i}`}} -
-
{renderSerovalNode(props, props.node, onSelect)}
-
- - {current => } - - - ); -} - -interface SerovalRendererProps extends Omit { - node?: SerovalNode; -} - -function SerovalRenderer(props: SerovalRendererProps): JSX.Element { - const [, rest] = splitProps(props, ["node"]); - return ( -
- {current => } -
- ); -} - function createSimpleStore>(initial: T) { const [state, setState] = createSignal(initial); @@ -986,12 +1121,20 @@ export function SerovalViewer(props: SerovalViewerProps): JSX.Element { return (
- references.read(index)} - getPromise={index => promises.read(index)} - getStream={index => streams.read(index)} - /> + + {root => ( + references.read(index), + getPromise: index => promises.read(index), + getStream: index => streams.read(index), + }} + node={root} + seen={[]} + open + /> + )} +
); } diff --git a/packages/start/src/shared/dev-toolbar/functions/URLSearchParamsViewer.tsx b/packages/start/src/shared/dev-toolbar/functions/URLSearchParamsViewer.tsx index 1b2a0046a..49982ba60 100644 --- a/packages/start/src/shared/dev-toolbar/functions/URLSearchParamsViewer.tsx +++ b/packages/start/src/shared/dev-toolbar/functions/URLSearchParamsViewer.tsx @@ -1,6 +1,6 @@ import { createResource, For, type JSX, Show, Suspense } from "solid-js"; import { Section } from "../../ui/Section.tsx"; -import { PropertySeparator, SerovalValue } from "./SerovalValue.tsx"; +import { Text } from "../../ui/Text.tsx"; interface URLSearchParamsViewerInnerProps { source: URLSearchParams; @@ -9,13 +9,19 @@ interface URLSearchParamsViewerInnerProps { function URLSearchParamsViewerInner(props: URLSearchParamsViewerInnerProps): JSX.Element { return (
-
+
{([key, value]) => ( -
- - - +
+ + {key} + + + {JSON.stringify(value)} +
)} diff --git a/packages/start/src/shared/dev-toolbar/functions/index.tsx b/packages/start/src/shared/dev-toolbar/functions/index.tsx index 4249f94fc..fc6a935ec 100644 --- a/packages/start/src/shared/dev-toolbar/functions/index.tsx +++ b/packages/start/src/shared/dev-toolbar/functions/index.tsx @@ -7,12 +7,11 @@ import { Section } from "../../ui/Section.tsx"; import { Select, SelectOption } from "../../ui/Select.tsx"; import { Tab, TabGroup, TabList, TabPanel } from "../../ui/Tabs.tsx"; import { Text } from "../../ui/Text.tsx"; -import { ArrowLeftIcon, FunctionIcon, TrashIcon } from "../icons.tsx"; +import { FunctionIcon, TrashIcon } from "../icons.tsx"; import { BlobViewer } from "./BlobViewer.tsx"; import { FormDataViewer } from "./FormDataViewer.tsx"; import { HeadersViewer } from "./HeadersViewer.tsx"; import { HexViewer } from "./HexViewer.tsx"; -import { PropertySeparator, SerovalValue } from "./SerovalValue.tsx"; import { SerovalViewer } from "./SerovalViewer.tsx"; import "./styles.css"; import { type ServerFunctionRequest, type ServerFunctionResponse } from "./tracker.ts"; @@ -37,38 +36,45 @@ interface ContentViewerProps { } function ContentViewer(props: ContentViewerProps): JSX.Element { + const body = createMemo(() => { + const original = props.source.source; + if (!original.body) { + return undefined; + } + const source = original.clone(); + const startType = source.headers.get(BODY_FORMAT_KEY); + const contentType = source.headers.get("Content-Type"); + switch (true) { + case startType === "true": + case startType === BodyFormat.Seroval: + return ; + case startType === BodyFormat.String: + return ; + case startType === BodyFormat.File: + return ; + case startType === BodyFormat.FormData: + case contentType?.startsWith("multipart/form-data"): + return ; + case startType === BodyFormat.URLSearchParams: + case contentType?.startsWith("application/x-www-form-urlencoded"): + return ; + case startType === BodyFormat.Blob: + return ; + case startType === BodyFormat.ArrayBuffer: + case startType === BodyFormat.Uint8Array: + return ; + } + return undefined; + }); + return ( <> -
+ +
{body()}
+
+
-
- {(() => { - const source = props.source.source.clone(); - const startType = source.headers.get(BODY_FORMAT_KEY); - const contentType = source.headers.get("Content-Type"); - switch (true) { - case startType === "true": - case startType === BodyFormat.Seroval: - return ; - case startType === BodyFormat.String: - return ; - case startType === BodyFormat.File: - return ; - case startType === BodyFormat.FormData: - case contentType?.startsWith("multipart/form-data"): - return ; - case startType === BodyFormat.URLSearchParams: - case contentType?.startsWith("application/x-www-form-urlencoded"): - return ; - case startType === BodyFormat.Blob: - return ; - case startType === BodyFormat.ArrayBuffer: - case startType === BodyFormat.Uint8Array: - return ; - } - })()} -
); } @@ -95,18 +101,26 @@ function convertRequestToEntries(request: Request) { function RequestViewer(props: RequestViewerProps): JSX.Element { return ( -
- - {([key, value]) => ( -
- {key} - - -
- )} -
-
+
+
+ + {([key, value]) => ( +
+ + {key} + + + {`${value}`} + +
+ )} +
+
+
); } @@ -130,28 +144,50 @@ function convertResponseToEntries(response: Response) { function ResponseViewer(props: ResponseViewerProps): JSX.Element { return ( - + + Waiting for response. + + } + > {instance => ( <> -
- - {([key, value]) => ( -
- {key} - - -
- )} -
-
- Timing - - + +
+
+ + {([key, value]) => ( +
+ + {key} + + + {`${value}`} + +
+ )} +
+
+ + Timing + + + {`${((instance().time - props.request.time) / 1000).toFixed(2)}s`} + +
- )} @@ -192,7 +228,6 @@ interface ServerFunctionInstanceViewerProps { id: string; instance: ServerFunctionInstance; onDelete: () => void; - onReturn: () => void; } function ServerFunctionInstanceViewer(props: ServerFunctionInstanceViewerProps): JSX.Element { @@ -201,14 +236,20 @@ function ServerFunctionInstanceViewer(props: ServerFunctionInstanceViewerProps):
- - -
+ + {response => ( + + + {`${((response().time - props.instance.request.time) / 1000).toFixed(2)}s`} + + + )} + @@ -251,27 +292,8 @@ export function ServerFunctionViewer(props: ServerFunctionViewerProps): JSX.Elem
- {/* request/response viewer */} - - {value => ( - - {instance => ( - { - setCurrentInstance(undefined); - }} - onDelete={() => { - props.onDeleteInstance(value()); - }} - /> - )} - - )} - - - {/* list of calls */} + {/* list of calls */} +
Server functions @@ -298,7 +320,33 @@ export function ServerFunctionViewer(props: ServerFunctionViewerProps): JSX.Elem
- +
+ {/* request/response viewer */} +
+ + Select a server function call. + + } + > + {value => ( + + {instance => ( + { + setCurrentInstance(undefined); + props.onDeleteInstance(value()); + }} + /> + )} + + )} + +
diff --git a/packages/start/src/shared/dev-toolbar/functions/styles.css b/packages/start/src/shared/dev-toolbar/functions/styles.css index ae3bfe46a..0efd25d68 100644 --- a/packages/start/src/shared/dev-toolbar/functions/styles.css +++ b/packages/start/src/shared/dev-toolbar/functions/styles.css @@ -2,21 +2,51 @@ display: flex; align-items: center; gap: 0.5rem; + + min-width: 0; +} + +[data-start-functions-instance-detail] > [data-start-text-size] { + overflow: hidden; + text-overflow: ellipsis; } [data-start-functions-viewer] { - color: rgb(249 250 251); + color: var(--start-dt-text); + + display: flex; + flex-direction: row; + + height: 100%; +} +[data-start-functions-sidebar] { display: flex; flex-direction: column; - min-height: 100%; + width: 17rem; + flex-shrink: 0; + min-height: 0; + + border-right: var(--start-dt-border-soft) 1px solid; +} + +[data-start-functions-detail] { + display: flex; + flex-direction: column; + + flex: 1; + min-width: 0; + min-height: 0; + + overflow: auto; } [data-start-functions-instances-container] { - color: rgb(249 250 251); + color: var(--start-dt-text); width: 100%; - min-height: 100%; + flex: 1; + min-height: 0; overflow: auto; } @@ -31,8 +61,12 @@ display: flex; align-items: center; justify-content: space-between; - padding: 0.5rem; - color: rgb(249 250 251); + gap: 0.5rem; + padding: 0.5rem 0.75rem; + color: var(--start-dt-text); + font-family: + ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", + monospace; } [data-start-headers] { @@ -60,16 +94,21 @@ gap: 0.25rem; } +[data-start-property] > [data-start-text-weight="semibold"] { + color: var(--start-dt-text-muted); +} + [data-start-function-instance-viewer] { display: flex; flex-direction: column; width: 100%; + min-height: 100%; } [data-start-function-instance-viewer-nav], [data-start-functions-nav] { - padding: 0.5rem; + padding: 0.5rem 0.75rem; display: flex; align-items: center; gap: 0.5rem; @@ -78,32 +117,126 @@ position: sticky; top: 0; + z-index: 1; + + border-bottom: var(--start-dt-border-soft) 1px solid; - border-bottom: oklch(87% 0.065 274.039) 1px solid; + background: var(--start-dt-bg-glass); + backdrop-filter: blur(16px) saturate(140%); + -webkit-backdrop-filter: blur(16px) saturate(140%); +} - background: oklch(25.7% 0.09 281.288); +[data-start-functions-nav] { + justify-content: flex-start; + flex-shrink: 0; } [data-start-functions-nav] svg { padding: 0.25rem; + color: var(--start-dt-accent); +} + +[data-start-function-instance-viewer-nav] > div:last-child { + display: flex; + align-items: center; + gap: 0.5rem; + + flex-shrink: 0; +} + +[data-start-function-instance-timing] { + display: inline-flex; + align-items: center; + + color: var(--start-dt-text-muted); + background: var(--start-dt-surface); + border-radius: 9999px; + padding: 0.125rem 0.5rem; +} + +/* Body section (the only non-collapsible
section in a tab panel) fills the pane; + the minimum keeps it inspectable even when Headers/Information are expanded — the + tab panel scrolls instead of squeezing the body. */ +[data-start-tab-panel] > div[data-start-section]:first-child { + flex: 1 1 auto; + min-height: 18rem; +} + +[data-start-tab-panel] > div[data-start-section]:first-child > [data-start-section-content] { + flex: 1; + min-height: 0; } [data-start-function-instance-viewer-nav-left] { display: flex; align-items: center; gap: 0.5rem; + + min-width: 0; } [data-start-function-instance-viewer-nav-left] > div { display: flex; align-items: center; gap: 0.5rem; + + min-width: 0; } [data-start-function-instance-viewer-content] { - height: 100%; + flex: 1; + min-height: 0; } [data-start-function-instance-viewer-content] > [tc-tab-group] { height: 100%; + + display: flex; + flex-direction: column; +} + +/* shared key-value table (headers, form data, url search params) */ +[data-start-kv-table] { + display: flex; + flex-direction: column; + gap: 0; + + border: 1px var(--start-dt-border-soft) solid; + border-radius: 0.5rem; + overflow: hidden; +} + +[data-start-kv-row] { + display: flex; + align-items: baseline; + gap: 0.75rem; + + padding: 0.375rem 0.625rem; +} + +[data-start-kv-row]:nth-child(even) { + background-color: oklch(1 0 0 / 0.03); +} + +[data-start-kv-row]:hover { + background-color: var(--start-dt-surface-hover); +} + +[data-start-kv-key] { + flex: 0 0 11rem; + min-width: 0; + + overflow: hidden; + text-overflow: ellipsis; + + color: oklch(0.78 0.1 305); +} + +[data-start-kv-value] { + flex: 1; + min-width: 0; + + word-break: break-all; + + color: var(--start-dt-text); } diff --git a/packages/start/src/shared/dev-toolbar/index.css b/packages/start/src/shared/dev-toolbar/index.css index 5b69569dd..943de35dd 100644 --- a/packages/start/src/shared/dev-toolbar/index.css +++ b/packages/start/src/shared/dev-toolbar/index.css @@ -1,4 +1,19 @@ [data-start-dev-toolbar] { + /* design tokens — inherited by every panel and ui component inside */ + --start-dt-bg: oklch(0.16 0.015 265); + --start-dt-bg-glass: oklch(0.17 0.015 265 / 0.88); + --start-dt-surface: oklch(0.22 0.02 265); + --start-dt-surface-hover: oklch(0.27 0.025 265); + --start-dt-surface-active: oklch(0.3 0.03 265); + --start-dt-border: oklch(0.34 0.02 265); + --start-dt-border-soft: oklch(0.27 0.02 265); + --start-dt-text: oklch(0.93 0.005 265); + --start-dt-text-muted: oklch(0.68 0.015 265); + --start-dt-accent: oklch(0.68 0.13 245); + --start-dt-accent-soft: oklch(0.68 0.13 245 / 0.16); + --start-dt-danger: oklch(0.63 0.21 25); + --start-dt-shadow: 0 12px 32px oklch(0 0 0 / 0.4), 0 2px 8px oklch(0 0 0 / 0.25); + position: fixed; bottom: 0px; @@ -8,19 +23,31 @@ flex-direction: column-reverse; align-items: flex-start; + z-index: 2147483647; + + color: var(--start-dt-text); + font-family: + system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", + "Noto Color Emoji"; +} + +[data-start-dev-toolbar] > [tc-toolbar] { cursor: grab; } -[data-start-dev-toolbar]:active { +[data-start-dev-toolbar] > [tc-toolbar]:active { cursor: grabbing; } [data-start-dev-toolbar] > [tc-toolbar] { position: relative; - background: oklch(25.7% 0.09 281.288); + background: var(--start-dt-bg-glass); + backdrop-filter: blur(16px) saturate(140%); + -webkit-backdrop-filter: blur(16px) saturate(140%); + margin: 1rem; - padding: 0.25rem 0.5rem; + padding: 0.375rem 0.625rem; overflow: hidden; border-radius: 9999px; @@ -29,9 +56,10 @@ flex-direction: row; align-items: center; - gap: 0.5rem; + gap: 0.625rem; - border: oklch(87% 0.065 274.039) 1px solid; + border: var(--start-dt-border) 1px solid; + box-shadow: var(--start-dt-shadow); } [data-start-dev-toolbar] > [tc-toolbar] > div { @@ -40,25 +68,30 @@ flex-direction: row; align-items: center; justify-content: center; + + gap: 0.125rem; } [data-start-dev-toolbar] > [tc-toolbar] > div:last-child { - border-left: rgb(249 250 251) 1px solid; - padding-left: 0.5rem; + border-left: var(--start-dt-border) 1px solid; + padding-left: 0.625rem; gap: 0.5rem; + + color: var(--start-dt-text-muted); } [data-start-dev-toolbar-version] { border-radius: 9999px; - color: rgb(249 250 251); + color: var(--start-dt-accent); + background: var(--start-dt-accent-soft); padding: 0.125rem 0.5rem; font-size: 0.875rem; line-height: 1.25rem; font-weight: 600; - border: oklch(87% 0.065 274.039) 1px solid; + border: none; } [data-start-dev-toolbar-version] > div { @@ -72,14 +105,21 @@ } [data-start-dev-toolbar-panel] { - border: oklch(87% 0.065 274.039) 1px solid; - background: oklch(25.7% 0.09 281.288); + border: var(--start-dt-border) 1px solid; + background: var(--start-dt-bg-glass); + backdrop-filter: blur(16px) saturate(140%); + -webkit-backdrop-filter: blur(16px) saturate(140%); + box-shadow: var(--start-dt-shadow); + margin: 0rem 1rem; - overflow: auto; + overflow: hidden; + + display: flex; + flex-direction: column; - width: 50rem; - height: 30rem; + width: 56rem; + height: min(40rem, calc(100vh - 4rem)); max-width: calc(100vw - 10rem); max-height: calc(100vh - 2rem); diff --git a/packages/start/src/shared/dev-toolbar/index.tsx b/packages/start/src/shared/dev-toolbar/index.tsx index 500dce993..e92610c94 100644 --- a/packages/start/src/shared/dev-toolbar/index.tsx +++ b/packages/start/src/shared/dev-toolbar/index.tsx @@ -35,6 +35,10 @@ export function DevToolbar(props: DevToolbarProps) { const current = ref(); if (current) { + // Only the toolbar pill itself is the drag handle — panels/popovers are not draggable. + const handle = + (current.querySelector(":scope > [tc-toolbar]") as HTMLElement | null) ?? current; + let isDown = false; // Offsets of the mouse relatively to the element's position @@ -58,7 +62,7 @@ export function DevToolbar(props: DevToolbarProps) { const ac = new AbortController(); - current.addEventListener( + handle.addEventListener( "mousedown", e => { isDown = true; @@ -165,10 +169,16 @@ export function DevToolbar(props: DevToolbarProps) { pushError(error.error ?? error); }; + const onRejectionEvent = (event: PromiseRejectionEvent) => { + pushError(event.reason); + }; + window.addEventListener("error", onErrorEvent); + window.addEventListener("unhandledrejection", onRejectionEvent); onCleanup(() => { window.removeEventListener("error", onErrorEvent); + window.removeEventListener("unhandledrejection", onRejectionEvent); }); }); diff --git a/packages/start/src/shared/ui/Badge.css b/packages/start/src/shared/ui/Badge.css index 845eb3e40..cfad5d30f 100644 --- a/packages/start/src/shared/ui/Badge.css +++ b/packages/start/src/shared/ui/Badge.css @@ -5,29 +5,40 @@ gap: 0.25rem; border-radius: 0.375rem; - padding: 0.125rem 0.25rem; + padding: 0.125rem 0.375rem; + + font-size: 0.75rem; + line-height: 1rem; + font-weight: 600; + font-family: + ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", + monospace; background-color: var(--background); - border: 1px var(--foreground) solid; + border: 1px var(--border) solid; color: var(--foreground); } [data-start-badge="success"] { - --background: oklch(98.2% 0.018 155.826); - --foreground: oklch(52.7% 0.154 150.069); + --background: oklch(0.63 0.15 150 / 0.14); + --border: oklch(0.63 0.15 150 / 0.4); + --foreground: oklch(0.8 0.14 152); } [data-start-badge="failure"] { - --background: oklch(97.1% 0.013 17.38); - --foreground: oklch(50.5% 0.213 27.518); + --background: oklch(0.63 0.21 25 / 0.14); + --border: oklch(0.63 0.21 25 / 0.4); + --foreground: oklch(0.78 0.15 22); } [data-start-badge="warning"] { - --background: oklch(98.7% 0.026 102.212); - --foreground: oklch(47.6% 0.114 61.907); + --background: oklch(0.75 0.15 85 / 0.14); + --border: oklch(0.75 0.15 85 / 0.4); + --foreground: oklch(0.85 0.13 88); } [data-start-badge="info"] { - --background: oklch(97% 0.014 254.604); - --foreground: oklch(48.8% 0.243 264.376); + --background: oklch(0.68 0.13 245 / 0.14); + --border: oklch(0.68 0.13 245 / 0.4); + --foreground: oklch(0.78 0.11 245); } diff --git a/packages/start/src/shared/ui/Button.css b/packages/start/src/shared/ui/Button.css index e48c1c75c..018974351 100644 --- a/packages/start/src/shared/ui/Button.css +++ b/packages/start/src/shared/ui/Button.css @@ -7,13 +7,13 @@ border-radius: 0.5rem; - color: oklch(97% 0.014 254.604); + color: var(--start-dt-text, oklch(97% 0.014 254.604)); - background-color: rgb(17 24 39); - padding: 0.25rem 0.5rem; + background-color: var(--start-dt-surface, rgb(17 24 39)); + padding: 0.25rem 0.625rem; font-size: 0.875rem; line-height: 1.25rem; - border: none; + border: 1px var(--start-dt-border, transparent) solid; outline: none; transition-property: color, background-color, border-color, box-shadow; @@ -27,6 +27,7 @@ [data-start-button][tc-selected], [data-start-button]:hover, [data-start-button]:focus { - color: rgb(219 234 254); - background-color: rgb(30 58 138); + color: var(--start-dt-text, rgb(219 234 254)); + background-color: var(--start-dt-surface-hover, rgb(30 58 138)); + border-color: var(--start-dt-accent, transparent); } diff --git a/packages/start/src/shared/ui/Cascade.css b/packages/start/src/shared/ui/Cascade.css deleted file mode 100644 index db25b019d..000000000 --- a/packages/start/src/shared/ui/Cascade.css +++ /dev/null @@ -1,29 +0,0 @@ -[data-start-cascade] { - display: flex; - flex-direction: column; - margin: 0; - margin-block: 0; - list-style-type: none; - padding-inline: 0; -} - -[data-start-cascade-option] { - border-radius: 0.25rem; - font-size: 0.875rem; - line-height: 1.25rem; - outline: none; - cursor: pointer; - - transition-property: color, background-color, border-color, box-shadow; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - transition-duration: 150ms; - - white-space: nowrap; -} - -[data-start-cascade-option][tc-active], -[data-start-cascade-option][tc-selected], -[data-start-cascade-option]:focus { - background-color: rgb(219 234 254); - color: rgb(30 58 138); -} diff --git a/packages/start/src/shared/ui/Cascade.tsx b/packages/start/src/shared/ui/Cascade.tsx deleted file mode 100644 index 5bfd5f8a2..000000000 --- a/packages/start/src/shared/ui/Cascade.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import { Select as BaseSelect, SelectOption as BaseSelectOption } from "terracotta"; - -import "./Cascade.css"; - -export const Cascade: typeof BaseSelect = props => ; -export const CascadeOption: typeof BaseSelectOption = props => ( - -); diff --git a/packages/start/src/shared/ui/Dialog.css b/packages/start/src/shared/ui/Dialog.css deleted file mode 100644 index 8bfff92f5..000000000 --- a/packages/start/src/shared/ui/Dialog.css +++ /dev/null @@ -1,25 +0,0 @@ -[data-start-dialog] { - position: fixed; - inset: 0px; - z-index: 50; - overflow-y: auto; -} - -[data-start-dialog] > div { - position: relative; - display: flex; - align-items: center; - justify-content: center; - min-height: 100vh; -} - -[data-start-dialog-overlay] { - position: absolute; - inset: 0px; - background-color: rgb(17 24 39 / 0.5); -} - -[data-start-dialog-panel] { - margin: 2rem; - z-index: 10; -} diff --git a/packages/start/src/shared/ui/Dialog.tsx b/packages/start/src/shared/ui/Dialog.tsx deleted file mode 100644 index 23078dc4e..000000000 --- a/packages/start/src/shared/ui/Dialog.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { - Dialog as BaseDialog, - DialogOverlay as BaseDialogOverlay, - DialogPanel as BaseDialogPanel, -} from "terracotta"; - -import "./Dialog.css"; - -export const Dialog: typeof BaseDialog = props => ; -export const DialogOverlay: typeof BaseDialogOverlay = props => ( - -); -export const DialogPanel: typeof BaseDialogPanel = props => ( - -); diff --git a/packages/start/src/shared/ui/IconButton.css b/packages/start/src/shared/ui/IconButton.css index 3a50ec5b8..41dfc8078 100644 --- a/packages/start/src/shared/ui/IconButton.css +++ b/packages/start/src/shared/ui/IconButton.css @@ -1,36 +1,42 @@ [data-start-icon-button] { display: flex; - padding: 0.25rem; - border-radius: 9999px; + padding: 0.375rem; + border-radius: 0.5rem; border-width: 0px; align-items: center; justify-content: center; cursor: pointer; - color: rgb(249 250 251); + color: var(--start-dt-text, rgb(249 250 251)); background: none; - transition-property: color, background-color, border-color, box-shadow; + transition-property: color, background-color, border-color, box-shadow, opacity; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 150ms; } [data-start-icon-button]:hover { - color: rgb(229 231 235); - background-color: rgb(55 65 81); + color: var(--start-dt-text, rgb(229 231 235)); + background-color: var(--start-dt-surface-hover, rgb(55 65 81)); } [data-start-icon-button]:focus { outline: 2px solid transparent; - outline-color: rgb(249 250 251); + outline-color: var(--start-dt-accent, rgb(249 250 251)); outline-offset: 2px; } [data-start-icon-button]:focus-visible { - box-shadow: 0 0 0 calc(3px) rgb(17 24 39 / 0.75); + box-shadow: 0 0 0 2px var(--start-dt-accent, rgb(17 24 39 / 0.75)); } [data-start-icon-button]:active { - color: rgb(243 244 246); - background-color: rgb(31 41 55); + color: var(--start-dt-accent, rgb(243 244 246)); + background-color: var(--start-dt-surface-active, rgb(31 41 55)); +} + +[data-start-icon-button]:disabled { + opacity: 0.35; + cursor: default; + pointer-events: none; } [data-start-icon-button] > svg { diff --git a/packages/start/src/shared/ui/Placeholder.css b/packages/start/src/shared/ui/Placeholder.css index cf51a4bf2..9796d3363 100644 --- a/packages/start/src/shared/ui/Placeholder.css +++ b/packages/start/src/shared/ui/Placeholder.css @@ -5,6 +5,8 @@ width: 100%; height: 100%; + + color: var(--start-dt-text-muted, inherit); } [data-start-placeholder] > span { diff --git a/packages/start/src/shared/ui/Section.css b/packages/start/src/shared/ui/Section.css index fb573550f..02c5e9721 100644 --- a/packages/start/src/shared/ui/Section.css +++ b/packages/start/src/shared/ui/Section.css @@ -1,16 +1,55 @@ [data-start-section] { display: flex; flex-direction: column; + gap: 0.375rem; } [data-start-section-title] { text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--start-dt-text-muted, inherit); +} + +/* collapsible variant */ +summary[data-start-section-title] { + display: flex; + align-items: center; + gap: 0.375rem; + + cursor: pointer; + list-style: none; + user-select: none; +} + +summary[data-start-section-title]::-webkit-details-marker { + display: none; +} + +summary[data-start-section-title]::before { + content: ""; + + width: 0.375rem; + height: 0.375rem; + + border-right: 1.5px currentColor solid; + border-bottom: 1.5px currentColor solid; + + transform: rotate(-45deg); + transition: transform 150ms cubic-bezier(0.4, 0, 0.2, 1); +} + +details[open] > summary[data-start-section-title]::before { + transform: rotate(45deg); +} + +summary[data-start-section-title]:hover { + color: var(--start-dt-text, inherit); } [data-start-section-content] { - margin-left: 0.5rem; - padding-left: 0.5rem; - border-left: 1px oklch(87% 0.065 274.039) solid; + margin-left: 0.25rem; + padding-left: 0.625rem; + border-left: 2px var(--start-dt-border-soft, oklch(87% 0.065 274.039)) solid; width: calc(100% - 2rem); diff --git a/packages/start/src/shared/ui/Section.tsx b/packages/start/src/shared/ui/Section.tsx index 3cbb54216..280108af6 100644 --- a/packages/start/src/shared/ui/Section.tsx +++ b/packages/start/src/shared/ui/Section.tsx @@ -1,4 +1,4 @@ -import type { JSX } from "solid-js"; +import { Show, type JSX } from "solid-js"; import { Text, type TextProps } from "./Text.tsx"; import "./Section.css"; @@ -6,16 +6,33 @@ import "./Section.css"; export interface SectionProps { title: string; options?: TextProps<"span">["options"]; + collapsible?: boolean; + defaultOpen?: boolean; children: JSX.Element; } export function Section(props: SectionProps): JSX.Element { return ( -
- - {props.title} - -
{props.children}
-
+ + + {props.title} + +
{props.children}
+
+ } + > +
+ + {props.title} + +
{props.children}
+
+ ); } diff --git a/packages/start/src/shared/ui/Select.css b/packages/start/src/shared/ui/Select.css index 1cc2a3131..f7914e7b1 100644 --- a/packages/start/src/shared/ui/Select.css +++ b/packages/start/src/shared/ui/Select.css @@ -8,11 +8,11 @@ } [data-start-select] > * + * { - border-top: 1px oklch(87% 0.065 274.039) solid; + border-top: 1px var(--start-dt-border-soft, oklch(87% 0.065 274.039)) solid; } [data-start-select-option] { - color: rgb(249 250 251); + color: var(--start-dt-text, rgb(249 250 251)); padding-top: 0.5rem; padding-bottom: 0.5rem; padding-left: 1rem; @@ -22,6 +22,8 @@ outline: none; cursor: pointer; + border-left: 2px solid transparent; + transition-property: color, background-color, border-color, box-shadow; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 150ms; @@ -29,9 +31,14 @@ white-space: nowrap; } +[data-start-select-option]:hover { + background-color: var(--start-dt-surface-hover, rgb(55 65 81)); +} + [data-start-select-option][tc-active], [data-start-select-option][tc-selected], [data-start-select-option]:focus { - color: rgb(229 231 235); - background-color: rgb(55 65 81); + color: var(--start-dt-text, rgb(229 231 235)); + background-color: var(--start-dt-accent-soft, rgb(55 65 81)); + border-left-color: var(--start-dt-accent, transparent); } diff --git a/packages/start/src/shared/ui/Table.css b/packages/start/src/shared/ui/Table.css deleted file mode 100644 index 6703f4d96..000000000 --- a/packages/start/src/shared/ui/Table.css +++ /dev/null @@ -1,22 +0,0 @@ -[data-start-table] { - border: 1px oklch(70.7% 0.165 254.624) solid; -} - -[data-start-table-header] { - display: flex; - flex-direction: row; - background-color: oklch(93.2% 0.032 255.585); - font-weight: 600; -} - -[data-start-table-row] { - display: flex; - flex-direction: row; - border-top: 1px oklch(70.7% 0.165 254.624) solid; -} - -[data-start-table-cell] { - flex: 1; - padding: 0.5rem 1rem; - overflow: auto; -} diff --git a/packages/start/src/shared/ui/Table.tsx b/packages/start/src/shared/ui/Table.tsx deleted file mode 100644 index 5609f174f..000000000 --- a/packages/start/src/shared/ui/Table.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import type { ComponentProps, JSX } from "solid-js"; - -import "./Table.css"; - -type TableProps = ComponentProps<"div">; - -export function Table(props: TableProps): JSX.Element { - return
; -} - -type TableHeaderProps = ComponentProps<"div">; - -export function TableHeader(props: TableHeaderProps): JSX.Element { - return
; -} - -type TableRowProps = ComponentProps<"div">; - -export function TableRow(props: TableRowProps): JSX.Element { - return
; -} - -type TableCellProps = ComponentProps<"div">; - -export function TableCell(props: TableCellProps): JSX.Element { - return
; -} diff --git a/packages/start/src/shared/ui/Tabs.css b/packages/start/src/shared/ui/Tabs.css index 6143573b2..29adda327 100644 --- a/packages/start/src/shared/ui/Tabs.css +++ b/packages/start/src/shared/ui/Tabs.css @@ -3,49 +3,63 @@ align-items: center; justify-content: center; - border-radius: 0.5rem; + border-radius: 0.375rem; cursor: pointer; - padding-top: 0.5rem; - padding-bottom: 0.5rem; - padding-left: 1rem; - padding-right: 1rem; - font-size: 0.875rem; + padding-top: 0.25rem; + padding-bottom: 0.25rem; + padding-left: 0.875rem; + padding-right: 0.875rem; + font-size: 0.8125rem; line-height: 1.25rem; + font-weight: 500; border: none; outline: none; + color: var(--start-dt-text-muted, rgb(229 231 235)); + background: none; + transition-property: color, background-color, border-color, box-shadow; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 150ms; } -[data-start-tab][tc-active], -[data-start-tab][tc-selected], [data-start-tab]:hover, [data-start-tab]:focus { - color: rgb(229 231 235); - background-color: rgb(55 65 81); + color: var(--start-dt-text, rgb(229 231 235)); +} + +[data-start-tab][tc-active], +[data-start-tab][tc-selected] { + color: var(--start-dt-text, rgb(229 231 235)); + background-color: var(--start-dt-surface-hover, rgb(55 65 81)); + box-shadow: 0 1px 2px oklch(0 0 0 / 0.25); } [data-start-tab-group] { } [data-start-tab-list] { - display: flex; - gap: 0.5rem; + display: inline-flex; + gap: 0.25rem; margin: 0.5rem; + padding: 0.25rem; + + border-radius: 0.5rem; + background-color: var(--start-dt-surface, rgb(17 24 39)); + align-self: flex-start; } [data-start-tab-panel] { - height: 100%; + flex: 1 1 0%; + min-height: 0; overflow: auto; display: flex; flex-direction: column; - gap: 0.5rem; - padding: 0.5rem; + gap: 0.75rem; + padding: 0.75rem; - border-top: oklch(87% 0.065 274.039) 1px solid; + border-top: var(--start-dt-border-soft, oklch(87% 0.065 274.039)) 1px solid; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2a89d4a33..e0183f769 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -320,6 +320,9 @@ importers: '@babel/types': specifier: ^7.29.7 version: 7.29.7 + '@jridgewell/trace-mapping': + specifier: ^0.3.31 + version: 0.3.31 '@solidjs/meta': specifier: ^0.29.4 version: 0.29.4(solid-js@1.9.15) @@ -338,9 +341,9 @@ importers: defu: specifier: ^6.1.7 version: 6.1.7 - error-stack-parser: - specifier: ^2.1.4 - version: 2.1.4 + error-stack-parser-es: + specifier: ^2.0.1 + version: 2.0.1 fast-glob: specifier: ^3.3.3 version: 3.3.3 @@ -377,15 +380,12 @@ importers: solid-js: specifier: ^1.9.15 version: 1.9.15 - source-map-js: - specifier: ^1.2.1 - version: 1.2.1 srvx: specifier: ^0.12.4 version: 0.12.4 terracotta: - specifier: ^1.1.1 - version: 1.1.1(solid-js@1.9.15) + specifier: ^1.2.4 + version: 1.2.4(solid-js@1.9.15) vite-plugin-solid: specifier: ^2.11.13 version: 2.11.13(@testing-library/jest-dom@6.10.0(@testing-library/dom@10.4.1))(solid-js@1.9.15)(vite@8.1.5(@types/node@26.1.1)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)) @@ -2077,8 +2077,8 @@ packages: wrangler: optional: true - error-stack-parser@2.1.4: - resolution: {integrity: sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==} + error-stack-parser-es@2.0.1: + resolution: {integrity: sha512-J36ntO+rMQVRuR/umlmxmfLi4TpWwtmTnHoJoXTiC2xDNazs0VDPKcX6pbhZMDd2HFtH9isMBJXMdbki+A++Pg==} es-module-lexer@2.3.1: resolution: {integrity: sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==} @@ -2948,9 +2948,6 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - stackframe@1.3.4: - resolution: {integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==} - standard-as-callback@2.1.0: resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} @@ -2994,8 +2991,8 @@ packages: resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} engines: {node: '>=8'} - terracotta@1.1.1: - resolution: {integrity: sha512-Sa6wnvkGMFmPq8N/wQb2aKkIW2eXH0LJvUOEk6QZLBEjqy+LsbzAOpDuqEfOmfA/hexssE6eQve2i1IIOeOh4g==} + terracotta@1.2.4: + resolution: {integrity: sha512-lVBegOwHeQ4483/H0736+D6orU8YDgG5msLyEsoB/ZBess/RWe8NRBKh0lEfpZTIyKYiLm2VzzWd6axkkWt0kQ==} engines: {node: '>=10'} peerDependencies: solid-js: ^1.8 @@ -4837,9 +4834,7 @@ snapshots: httpxy: 0.5.5 srvx: 0.11.22 - error-stack-parser@2.1.4: - dependencies: - stackframe: 1.3.4 + error-stack-parser-es@2.0.1: {} es-module-lexer@2.3.1: {} @@ -5732,8 +5727,6 @@ snapshots: stackback@0.0.2: {} - stackframe@1.3.4: {} - standard-as-callback@2.1.0: optional: true @@ -5768,7 +5761,7 @@ snapshots: term-size@2.2.1: {} - terracotta@1.1.1(solid-js@1.9.15): + terracotta@1.2.4(solid-js@1.9.15): dependencies: solid-js: 1.9.15 solid-use: 0.9.1(solid-js@1.9.15)