@tanstack/pacer-devtools
Advanced tools
| import { className, createComponent, delegateEvents, effect, insert, memo, setStyleProperty, style, template, use } from "solid-js/web"; | ||
| import { Header, HeaderLogo, JsonTree, MainPanel, ThemeContextProvider, createTheme } from "@tanstack/devtools-ui"; | ||
| import { createStore, produce } from "solid-js/store"; | ||
| import { For, Show, createContext, createEffect, createMemo, createSignal, onCleanup, onMount, useContext } from "solid-js"; | ||
| import { getPacerDevtoolsInstance, pacerEventClient } from "@tanstack/pacer/event-client"; | ||
| import * as goober from "goober"; | ||
| import { shallow, useSelector } from "@tanstack/solid-store"; | ||
| import clsx from "clsx"; | ||
| import { emitChange } from "@tanstack/pacer"; | ||
| import dayjs from "dayjs"; | ||
| import relativeTime from "dayjs/plugin/relativeTime.js"; | ||
| //#region src/PacerContextProvider.tsx | ||
| const initialPacerDevtoolsStore = { | ||
| asyncBatchers: [], | ||
| asyncDebouncers: [], | ||
| asyncQueuers: [], | ||
| asyncRateLimiters: [], | ||
| asyncThrottlers: [], | ||
| batchers: [], | ||
| debouncers: [], | ||
| queuers: [], | ||
| rateLimiters: [], | ||
| throttlers: [], | ||
| lastUpdatedByKey: {} | ||
| }; | ||
| const PacerDevtoolsContext = createContext([initialPacerDevtoolsStore, () => {}]); | ||
| /** | ||
| * Match TanStack Form devtools: subscribe with {@link pacerEventClient.on} per | ||
| * event suffix so the same `pacer:${suffix}` channel the library emits on is | ||
| * observed. `onAllPluginEvents` only sees `tanstack-devtools-global`, which is | ||
| * not always relayed the same way by the shell. | ||
| * | ||
| * `d-*` suffixes are used when the devtools panel pushes state back (see | ||
| * ActionButtons); those must update the panel store too. | ||
| */ | ||
| const PACER_DEVTOOLS_UTIL_EVENTS = [ | ||
| { | ||
| listKey: "asyncBatchers", | ||
| suffixes: ["AsyncBatcher", "d-AsyncBatcher"] | ||
| }, | ||
| { | ||
| listKey: "asyncDebouncers", | ||
| suffixes: ["AsyncDebouncer", "d-AsyncDebouncer"] | ||
| }, | ||
| { | ||
| listKey: "asyncQueuers", | ||
| suffixes: ["AsyncQueuer", "d-AsyncQueuer"] | ||
| }, | ||
| { | ||
| listKey: "asyncRateLimiters", | ||
| suffixes: ["AsyncRateLimiter", "d-AsyncRateLimiter"] | ||
| }, | ||
| { | ||
| listKey: "asyncThrottlers", | ||
| suffixes: ["AsyncThrottler", "d-AsyncThrottler"] | ||
| }, | ||
| { | ||
| listKey: "batchers", | ||
| suffixes: ["Batcher", "d-Batcher"] | ||
| }, | ||
| { | ||
| listKey: "debouncers", | ||
| suffixes: ["Debouncer", "d-Debouncer"] | ||
| }, | ||
| { | ||
| listKey: "queuers", | ||
| suffixes: ["Queuer", "d-Queuer"] | ||
| }, | ||
| { | ||
| listKey: "rateLimiters", | ||
| suffixes: ["RateLimiter", "d-RateLimiter"] | ||
| }, | ||
| { | ||
| listKey: "throttlers", | ||
| suffixes: ["Throttler", "d-Throttler"] | ||
| } | ||
| ]; | ||
| function PacerContextProvider(props) { | ||
| const [store, setStore] = createStore(initialPacerDevtoolsStore); | ||
| createEffect(() => { | ||
| const cleanups = []; | ||
| for (const { listKey, suffixes } of PACER_DEVTOOLS_UTIL_EVENTS) for (const suffix of suffixes) cleanups.push(pacerEventClient.on(suffix, (e) => { | ||
| const key = e.payload.key; | ||
| if (!key) return; | ||
| const instance = getPacerDevtoolsInstance(key); | ||
| if (!instance || typeof instance !== "object") return; | ||
| setStore(produce((draft) => { | ||
| const list = draft[listKey]; | ||
| const index = list.findIndex((item) => item.key === key); | ||
| const inst = instance; | ||
| if (index !== -1) list[index] = inst; | ||
| else list.push(inst); | ||
| draft.lastUpdatedByKey[key] = Date.now(); | ||
| })); | ||
| })); | ||
| onCleanup(() => { | ||
| for (const cleanup of cleanups) cleanup(); | ||
| }); | ||
| }); | ||
| return createComponent(PacerDevtoolsContext.Provider, { | ||
| value: [store, setStore], | ||
| get children() { | ||
| return props.children; | ||
| } | ||
| }); | ||
| } | ||
| const usePacerDevtoolsContext = () => { | ||
| return useContext(PacerDevtoolsContext); | ||
| }; | ||
| const usePacerDevtoolsState = () => { | ||
| const [state] = usePacerDevtoolsContext(); | ||
| return state; | ||
| }; | ||
| //#endregion | ||
| //#region src/styles/tokens.ts | ||
| const tokens = { | ||
| colors: { | ||
| inherit: "inherit", | ||
| current: "currentColor", | ||
| transparent: "transparent", | ||
| black: "#000000", | ||
| white: "#ffffff", | ||
| neutral: { | ||
| 50: "#f9fafb", | ||
| 100: "#f2f4f7", | ||
| 200: "#eaecf0", | ||
| 300: "#d0d5dd", | ||
| 400: "#98a2b3", | ||
| 500: "#667085", | ||
| 600: "#475467", | ||
| 700: "#344054", | ||
| 800: "#1d2939", | ||
| 900: "#101828" | ||
| }, | ||
| darkGray: { | ||
| 50: "#525c7a", | ||
| 100: "#49536e", | ||
| 200: "#414962", | ||
| 300: "#394056", | ||
| 400: "#313749", | ||
| 500: "#292e3d", | ||
| 600: "#212530", | ||
| 700: "#191c24", | ||
| 800: "#111318", | ||
| 900: "#0b0d10" | ||
| }, | ||
| gray: { | ||
| 50: "#f9fafb", | ||
| 100: "#f2f4f7", | ||
| 200: "#eaecf0", | ||
| 300: "#d0d5dd", | ||
| 400: "#98a2b3", | ||
| 500: "#667085", | ||
| 600: "#475467", | ||
| 700: "#344054", | ||
| 800: "#1d2939", | ||
| 900: "#101828" | ||
| }, | ||
| blue: { | ||
| 25: "#F5FAFF", | ||
| 50: "#EFF8FF", | ||
| 100: "#D1E9FF", | ||
| 200: "#B2DDFF", | ||
| 300: "#84CAFF", | ||
| 400: "#53B1FD", | ||
| 500: "#2E90FA", | ||
| 600: "#1570EF", | ||
| 700: "#175CD3", | ||
| 800: "#1849A9", | ||
| 900: "#194185" | ||
| }, | ||
| green: { | ||
| 25: "#F6FEF9", | ||
| 50: "#ECFDF3", | ||
| 100: "#D1FADF", | ||
| 200: "#A6F4C5", | ||
| 300: "#6CE9A6", | ||
| 400: "#32D583", | ||
| 500: "#12B76A", | ||
| 600: "#039855", | ||
| 700: "#027A48", | ||
| 800: "#05603A", | ||
| 900: "#054F31" | ||
| }, | ||
| red: { | ||
| 50: "#fef2f2", | ||
| 100: "#fee2e2", | ||
| 200: "#fecaca", | ||
| 300: "#fca5a5", | ||
| 400: "#f87171", | ||
| 500: "#ef4444", | ||
| 600: "#dc2626", | ||
| 700: "#b91c1c", | ||
| 800: "#991b1b", | ||
| 900: "#7f1d1d", | ||
| 950: "#450a0a" | ||
| }, | ||
| yellow: { | ||
| 25: "#FFFCF5", | ||
| 50: "#FFFAEB", | ||
| 100: "#FEF0C7", | ||
| 200: "#FEDF89", | ||
| 300: "#FEC84B", | ||
| 400: "#FDB022", | ||
| 500: "#F79009", | ||
| 600: "#DC6803", | ||
| 700: "#B54708", | ||
| 800: "#93370D", | ||
| 900: "#7A2E0E" | ||
| }, | ||
| purple: { | ||
| 25: "#FAFAFF", | ||
| 50: "#F4F3FF", | ||
| 100: "#EBE9FE", | ||
| 200: "#D9D6FE", | ||
| 300: "#BDB4FE", | ||
| 400: "#9B8AFB", | ||
| 500: "#7A5AF8", | ||
| 600: "#6938EF", | ||
| 700: "#5925DC", | ||
| 800: "#4A1FB8", | ||
| 900: "#3E1C96" | ||
| }, | ||
| teal: { | ||
| 25: "#F6FEFC", | ||
| 50: "#F0FDF9", | ||
| 100: "#CCFBEF", | ||
| 200: "#99F6E0", | ||
| 300: "#5FE9D0", | ||
| 400: "#2ED3B7", | ||
| 500: "#15B79E", | ||
| 600: "#0E9384", | ||
| 700: "#107569", | ||
| 800: "#125D56", | ||
| 900: "#134E48" | ||
| }, | ||
| pink: { | ||
| 25: "#fdf2f8", | ||
| 50: "#fce7f3", | ||
| 100: "#fbcfe8", | ||
| 200: "#f9a8d4", | ||
| 300: "#f472b6", | ||
| 400: "#ec4899", | ||
| 500: "#db2777", | ||
| 600: "#be185d", | ||
| 700: "#9d174d", | ||
| 800: "#831843", | ||
| 900: "#500724" | ||
| }, | ||
| cyan: { | ||
| 25: "#ecfeff", | ||
| 50: "#cffafe", | ||
| 100: "#a5f3fc", | ||
| 200: "#67e8f9", | ||
| 300: "#22d3ee", | ||
| 400: "#06b6d4", | ||
| 500: "#0891b2", | ||
| 600: "#0e7490", | ||
| 700: "#155e75", | ||
| 800: "#164e63", | ||
| 900: "#083344" | ||
| } | ||
| }, | ||
| alpha: { | ||
| 100: "ff", | ||
| 90: "e5", | ||
| 80: "cc", | ||
| 70: "b3", | ||
| 60: "99", | ||
| 50: "80", | ||
| 40: "66", | ||
| 30: "4d", | ||
| 20: "33", | ||
| 10: "1a", | ||
| 0: "00" | ||
| }, | ||
| font: { | ||
| size: { | ||
| "2xs": "calc(var(--tsrd-font-size) * 0.625)", | ||
| xs: "calc(var(--tsrd-font-size) * 0.75)", | ||
| sm: "calc(var(--tsrd-font-size) * 0.875)", | ||
| md: "var(--tsrd-font-size)", | ||
| lg: "calc(var(--tsrd-font-size) * 1.125)", | ||
| xl: "calc(var(--tsrd-font-size) * 1.25)", | ||
| "2xl": "calc(var(--tsrd-font-size) * 1.5)", | ||
| "3xl": "calc(var(--tsrd-font-size) * 1.875)", | ||
| "4xl": "calc(var(--tsrd-font-size) * 2.25)", | ||
| "5xl": "calc(var(--tsrd-font-size) * 3)", | ||
| "6xl": "calc(var(--tsrd-font-size) * 3.75)", | ||
| "7xl": "calc(var(--tsrd-font-size) * 4.5)", | ||
| "8xl": "calc(var(--tsrd-font-size) * 6)", | ||
| "9xl": "calc(var(--tsrd-font-size) * 8)" | ||
| }, | ||
| lineHeight: { | ||
| "3xs": "calc(var(--tsrd-font-size) * 0.75)", | ||
| "2xs": "calc(var(--tsrd-font-size) * 0.875)", | ||
| xs: "calc(var(--tsrd-font-size) * 1)", | ||
| sm: "calc(var(--tsrd-font-size) * 1.25)", | ||
| md: "calc(var(--tsrd-font-size) * 1.5)", | ||
| lg: "calc(var(--tsrd-font-size) * 1.75)", | ||
| xl: "calc(var(--tsrd-font-size) * 2)", | ||
| "2xl": "calc(var(--tsrd-font-size) * 2.25)", | ||
| "3xl": "calc(var(--tsrd-font-size) * 2.5)", | ||
| "4xl": "calc(var(--tsrd-font-size) * 2.75)", | ||
| "5xl": "calc(var(--tsrd-font-size) * 3)", | ||
| "6xl": "calc(var(--tsrd-font-size) * 3.25)", | ||
| "7xl": "calc(var(--tsrd-font-size) * 3.5)", | ||
| "8xl": "calc(var(--tsrd-font-size) * 3.75)", | ||
| "9xl": "calc(var(--tsrd-font-size) * 4)" | ||
| }, | ||
| weight: { | ||
| thin: "100", | ||
| extralight: "200", | ||
| light: "300", | ||
| normal: "400", | ||
| medium: "500", | ||
| semibold: "600", | ||
| bold: "700", | ||
| extrabold: "800", | ||
| black: "900" | ||
| }, | ||
| fontFamily: { | ||
| sans: "ui-sans-serif, Inter, system-ui, sans-serif, sans-serif", | ||
| mono: `ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace` | ||
| } | ||
| }, | ||
| breakpoints: { | ||
| xs: "320px", | ||
| sm: "640px", | ||
| md: "768px", | ||
| lg: "1024px", | ||
| xl: "1280px", | ||
| "2xl": "1536px" | ||
| }, | ||
| border: { radius: { | ||
| none: "0px", | ||
| xs: "calc(var(--tsrd-font-size) * 0.125)", | ||
| sm: "calc(var(--tsrd-font-size) * 0.25)", | ||
| md: "calc(var(--tsrd-font-size) * 0.375)", | ||
| lg: "calc(var(--tsrd-font-size) * 0.5)", | ||
| xl: "calc(var(--tsrd-font-size) * 0.75)", | ||
| "2xl": "calc(var(--tsrd-font-size) * 1)", | ||
| "3xl": "calc(var(--tsrd-font-size) * 1.5)", | ||
| full: "9999px" | ||
| } }, | ||
| size: { | ||
| 0: "0px", | ||
| .25: "calc(var(--tsrd-font-size) * 0.0625)", | ||
| .5: "calc(var(--tsrd-font-size) * 0.125)", | ||
| 1: "calc(var(--tsrd-font-size) * 0.25)", | ||
| 1.5: "calc(var(--tsrd-font-size) * 0.375)", | ||
| 2: "calc(var(--tsrd-font-size) * 0.5)", | ||
| 2.5: "calc(var(--tsrd-font-size) * 0.625)", | ||
| 3: "calc(var(--tsrd-font-size) * 0.75)", | ||
| 3.5: "calc(var(--tsrd-font-size) * 0.875)", | ||
| 4: "calc(var(--tsrd-font-size) * 1)", | ||
| 4.5: "calc(var(--tsrd-font-size) * 1.125)", | ||
| 5: "calc(var(--tsrd-font-size) * 1.25)", | ||
| 5.5: "calc(var(--tsrd-font-size) * 1.375)", | ||
| 6: "calc(var(--tsrd-font-size) * 1.5)", | ||
| 6.5: "calc(var(--tsrd-font-size) * 1.625)", | ||
| 7: "calc(var(--tsrd-font-size) * 1.75)", | ||
| 8: "calc(var(--tsrd-font-size) * 2)", | ||
| 9: "calc(var(--tsrd-font-size) * 2.25)", | ||
| 10: "calc(var(--tsrd-font-size) * 2.5)", | ||
| 11: "calc(var(--tsrd-font-size) * 2.75)", | ||
| 12: "calc(var(--tsrd-font-size) * 3)", | ||
| 14: "calc(var(--tsrd-font-size) * 3.5)", | ||
| 16: "calc(var(--tsrd-font-size) * 4)", | ||
| 20: "calc(var(--tsrd-font-size) * 5)", | ||
| 24: "calc(var(--tsrd-font-size) * 6)", | ||
| 28: "calc(var(--tsrd-font-size) * 7)", | ||
| 32: "calc(var(--tsrd-font-size) * 8)", | ||
| 36: "calc(var(--tsrd-font-size) * 9)", | ||
| 40: "calc(var(--tsrd-font-size) * 10)", | ||
| 44: "calc(var(--tsrd-font-size) * 11)", | ||
| 48: "calc(var(--tsrd-font-size) * 12)", | ||
| 52: "calc(var(--tsrd-font-size) * 13)", | ||
| 56: "calc(var(--tsrd-font-size) * 14)", | ||
| 60: "calc(var(--tsrd-font-size) * 15)", | ||
| 64: "calc(var(--tsrd-font-size) * 16)", | ||
| 72: "calc(var(--tsrd-font-size) * 18)", | ||
| 80: "calc(var(--tsrd-font-size) * 20)", | ||
| 96: "calc(var(--tsrd-font-size) * 24)" | ||
| }, | ||
| shadow: { | ||
| xs: (_ = "rgb(0 0 0 / 0.1)") => `0 1px 2px 0 rgb(0 0 0 / 0.05)`, | ||
| sm: (color = "rgb(0 0 0 / 0.1)") => `0 1px 3px 0 ${color}, 0 1px 2px -1px ${color}`, | ||
| md: (color = "rgb(0 0 0 / 0.1)") => `0 4px 6px -1px ${color}, 0 2px 4px -2px ${color}`, | ||
| lg: (color = "rgb(0 0 0 / 0.1)") => `0 10px 15px -3px ${color}, 0 4px 6px -4px ${color}`, | ||
| xl: (color = "rgb(0 0 0 / 0.1)") => `0 20px 25px -5px ${color}, 0 8px 10px -6px ${color}`, | ||
| "2xl": (color = "rgb(0 0 0 / 0.25)") => `0 25px 50px -12px ${color}`, | ||
| inner: (color = "rgb(0 0 0 / 0.05)") => `inset 0 2px 4px 0 ${color}`, | ||
| none: () => `none` | ||
| }, | ||
| zIndices: { | ||
| hide: -1, | ||
| auto: "auto", | ||
| base: 0, | ||
| docked: 10, | ||
| dropdown: 1e3, | ||
| sticky: 1100, | ||
| banner: 1200, | ||
| overlay: 1300, | ||
| modal: 1400, | ||
| popover: 1500, | ||
| skipLink: 1600, | ||
| toast: 1700, | ||
| tooltip: 1800 | ||
| } | ||
| }; | ||
| //#endregion | ||
| //#region src/styles/use-styles.ts | ||
| const stylesFactory = (theme) => { | ||
| const { colors, font, size, alpha, border } = tokens; | ||
| const { fontFamily, size: fontSize } = font; | ||
| const css = goober.css; | ||
| const t = (light, dark) => theme === "light" ? light : dark; | ||
| return { | ||
| /** | ||
| * Fills the devtools-utils portal div (height: 100%) and is a flex parent so MainPanel | ||
| * gets a real flex height budget (not just height: 100% without a flex chain). | ||
| */ | ||
| shellRoot: css` | ||
| display: flex; | ||
| flex-direction: column; | ||
| box-sizing: border-box; | ||
| width: 100%; | ||
| height: 100%; | ||
| min-height: 0; | ||
| overflow: hidden; | ||
| `, | ||
| /** Overrides devtools-ui MainPanel overflow-y: auto so inner columns own scrolling */ | ||
| mainPanelShell: css` | ||
| display: flex; | ||
| flex-direction: column; | ||
| flex: 1 1 0%; | ||
| min-height: 0; | ||
| overflow: hidden !important; | ||
| `, | ||
| mainContainer: css` | ||
| display: flex; | ||
| flex: 1 1 0%; | ||
| min-height: 0; | ||
| overflow: hidden; | ||
| padding: ${size[2]}; | ||
| padding-top: 0; | ||
| margin-top: ${size[2]}; | ||
| `, | ||
| dragHandle: css` | ||
| width: 8px; | ||
| background: ${t(colors.gray[300], colors.darkGray[600])}; | ||
| cursor: col-resize; | ||
| position: relative; | ||
| transition: all 0.2s ease; | ||
| user-select: none; | ||
| pointer-events: all; | ||
| margin: 0 ${size[1]}; | ||
| border-radius: 2px; | ||
| &:hover { | ||
| background: ${t(colors.blue[600], colors.blue[500])}; | ||
| margin: 0 ${size[1]}; | ||
| } | ||
| &.dragging { | ||
| background: ${t(colors.blue[700], colors.blue[600])}; | ||
| margin: 0 ${size[1]}; | ||
| } | ||
| &::after { | ||
| content: ''; | ||
| position: absolute; | ||
| top: 50%; | ||
| left: 50%; | ||
| transform: translate(-50%, -50%); | ||
| width: 2px; | ||
| height: 20px; | ||
| background: ${t(colors.gray[400], colors.darkGray[400])}; | ||
| border-radius: 1px; | ||
| pointer-events: none; | ||
| } | ||
| &:hover::after, | ||
| &.dragging::after { | ||
| background: ${t(colors.blue[500], colors.blue[300])}; | ||
| } | ||
| `, | ||
| leftPanel: css` | ||
| background: ${t(colors.gray[100], colors.darkGray[800])}; | ||
| border-radius: ${border.radius.lg}; | ||
| border: 1px solid ${t(colors.gray[200], colors.darkGray[700])}; | ||
| display: flex; | ||
| flex-direction: column; | ||
| overflow: hidden; | ||
| min-height: 0; | ||
| flex-shrink: 0; | ||
| `, | ||
| rightPanel: css` | ||
| background: ${t(colors.gray[100], colors.darkGray[800])}; | ||
| border-radius: ${border.radius.lg}; | ||
| border: 1px solid ${t(colors.gray[200], colors.darkGray[700])}; | ||
| display: flex; | ||
| flex-direction: column; | ||
| overflow: hidden; | ||
| min-height: 0; | ||
| flex: 1; | ||
| `, | ||
| panelHeader: css` | ||
| font-size: ${fontSize.md}; | ||
| font-weight: ${font.weight.bold}; | ||
| color: ${t(colors.blue[700], colors.blue[400])}; | ||
| padding: ${size[2]}; | ||
| border-bottom: 1px solid ${t(colors.gray[200], colors.darkGray[700])}; | ||
| background: ${t(colors.gray[100], colors.darkGray[800])}; | ||
| flex-shrink: 0; | ||
| `, | ||
| utilList: css` | ||
| flex: 1; | ||
| overflow-y: auto; | ||
| padding: ${size[1]}; | ||
| min-height: 0; | ||
| `, | ||
| utilGroup: css` | ||
| margin-bottom: ${size[2]}; | ||
| `, | ||
| utilGroupHeader: css` | ||
| font-size: ${fontSize.xs}; | ||
| font-weight: ${font.weight.semibold}; | ||
| color: ${t(colors.gray[600], colors.gray[400])}; | ||
| text-transform: uppercase; | ||
| letter-spacing: 0.05em; | ||
| margin-bottom: ${size[1]}; | ||
| padding: ${size[1]} ${size[2]}; | ||
| background: ${t(colors.gray[200], colors.darkGray[700])}; | ||
| border-radius: ${border.radius.md}; | ||
| `, | ||
| utilRow: css` | ||
| display: flex; | ||
| justify-content: space-between; | ||
| align-items: center; | ||
| padding: ${size[2]}; | ||
| margin-bottom: ${size[1]}; | ||
| background: ${t(colors.gray[200], colors.darkGray[700])}; | ||
| border-radius: ${border.radius.md}; | ||
| cursor: pointer; | ||
| transition: all 0.2s ease; | ||
| border: 1px solid transparent; | ||
| &:hover { | ||
| background: ${t(colors.gray[300], colors.darkGray[600])}; | ||
| border-color: ${t(colors.gray[400], colors.darkGray[500])}; | ||
| } | ||
| `, | ||
| utilRowSelected: css` | ||
| background: ${t(colors.blue[100], colors.blue[900] + alpha[20])}; | ||
| border-color: ${t(colors.blue[600], colors.blue[500])}; | ||
| box-shadow: 0 0 0 1px | ||
| ${t(colors.blue[600] + alpha[30], colors.blue[500] + alpha[30])}; | ||
| `, | ||
| utilKey: css` | ||
| font-family: ${fontFamily.mono}; | ||
| font-size: ${fontSize.xs}; | ||
| color: ${t(colors.gray[900], colors.gray[100])}; | ||
| flex: 1; | ||
| overflow: hidden; | ||
| text-overflow: ellipsis; | ||
| white-space: nowrap; | ||
| `, | ||
| utilStatus: css` | ||
| font-size: ${fontSize.xs}; | ||
| color: ${t(colors.gray[600], colors.gray[400])}; | ||
| text-transform: uppercase; | ||
| letter-spacing: 0.05em; | ||
| padding: ${size[1]} ${size[1]}; | ||
| background: ${t(colors.gray[300], colors.darkGray[600])}; | ||
| border-radius: ${border.radius.sm}; | ||
| margin-left: ${size[1]}; | ||
| `, | ||
| stateDetails: css` | ||
| flex: 1; | ||
| overflow-y: auto; | ||
| padding: ${size[2]}; | ||
| min-height: 0; | ||
| `, | ||
| stateHeader: css` | ||
| margin-bottom: ${size[2]}; | ||
| padding-bottom: ${size[2]}; | ||
| border-bottom: 1px solid ${t(colors.gray[200], colors.darkGray[700])}; | ||
| `, | ||
| stateTitle: css` | ||
| font-size: ${fontSize.md}; | ||
| font-weight: ${font.weight.bold}; | ||
| color: ${t(colors.blue[700], colors.blue[400])}; | ||
| margin-bottom: ${size[1]}; | ||
| `, | ||
| stateKey: css` | ||
| font-family: ${fontFamily.mono}; | ||
| font-size: ${fontSize.xs}; | ||
| color: ${t(colors.gray[600], colors.gray[400])}; | ||
| word-break: break-all; | ||
| `, | ||
| stateContent: css` | ||
| background: ${t(colors.gray[100], colors.darkGray[700])}; | ||
| border-radius: ${border.radius.md}; | ||
| padding: ${size[2]}; | ||
| border: 1px solid ${t(colors.gray[300], colors.darkGray[600])}; | ||
| `, | ||
| detailsGrid: css` | ||
| display: grid; | ||
| grid-template-columns: 1fr; | ||
| gap: ${size[2]}; | ||
| align-items: start; | ||
| `, | ||
| detailSection: css` | ||
| background: ${t(colors.white, colors.darkGray[700])}; | ||
| border: 1px solid ${t(colors.gray[300], colors.darkGray[600])}; | ||
| border-radius: ${border.radius.md}; | ||
| padding: ${size[2]}; | ||
| `, | ||
| detailSectionHeader: css` | ||
| font-size: ${fontSize.sm}; | ||
| font-weight: ${font.weight.bold}; | ||
| color: ${t(colors.gray[800], colors.gray[200])}; | ||
| margin-bottom: ${size[1]}; | ||
| text-transform: uppercase; | ||
| letter-spacing: 0.04em; | ||
| `, | ||
| actionsRow: css` | ||
| display: flex; | ||
| flex-wrap: wrap; | ||
| gap: ${size[2]}; | ||
| `, | ||
| actionButton: css` | ||
| display: inline-flex; | ||
| align-items: center; | ||
| gap: ${size[1]}; | ||
| padding: ${size[1]} ${size[2]}; | ||
| border-radius: ${border.radius.md}; | ||
| border: 1px solid ${t(colors.gray[300], colors.darkGray[500])}; | ||
| background: ${t(colors.gray[200], colors.darkGray[600])}; | ||
| color: ${t(colors.gray[900], colors.gray[100])}; | ||
| font-size: ${fontSize.xs}; | ||
| cursor: pointer; | ||
| user-select: none; | ||
| transition: | ||
| background 0.15s, | ||
| border-color 0.15s; | ||
| &:hover { | ||
| background: ${t(colors.gray[300], colors.darkGray[500])}; | ||
| border-color: ${t(colors.gray[400], colors.darkGray[400])}; | ||
| } | ||
| &:disabled { | ||
| opacity: 0.5; | ||
| cursor: not-allowed; | ||
| &:hover { | ||
| background: ${t(colors.gray[200], colors.darkGray[600])}; | ||
| border-color: ${t(colors.gray[300], colors.darkGray[500])}; | ||
| } | ||
| } | ||
| `, | ||
| actionDotBlue: css` | ||
| width: 6px; | ||
| height: 6px; | ||
| border-radius: 9999px; | ||
| background: ${colors.blue[400]}; | ||
| `, | ||
| actionDotGreen: css` | ||
| width: 6px; | ||
| height: 6px; | ||
| border-radius: 9999px; | ||
| background: ${colors.green[400]}; | ||
| `, | ||
| actionDotRed: css` | ||
| width: 6px; | ||
| height: 6px; | ||
| border-radius: 9999px; | ||
| background: ${colors.red[400]}; | ||
| `, | ||
| actionDotYellow: css` | ||
| width: 6px; | ||
| height: 6px; | ||
| border-radius: 9999px; | ||
| background: ${colors.yellow[400]}; | ||
| `, | ||
| actionDotOrange: css` | ||
| width: 6px; | ||
| height: 6px; | ||
| border-radius: 9999px; | ||
| background: ${colors.pink[400]}; | ||
| `, | ||
| actionDotPurple: css` | ||
| width: 6px; | ||
| height: 6px; | ||
| border-radius: 9999px; | ||
| background: ${colors.purple[400]}; | ||
| `, | ||
| infoGrid: css` | ||
| display: grid; | ||
| grid-template-columns: auto 1fr; | ||
| gap: ${size[1]}; | ||
| row-gap: ${size[1]}; | ||
| align-items: center; | ||
| `, | ||
| infoLabel: css` | ||
| color: ${t(colors.gray[600], colors.gray[400])}; | ||
| font-size: ${fontSize.xs}; | ||
| text-transform: uppercase; | ||
| letter-spacing: 0.05em; | ||
| `, | ||
| infoValueMono: css` | ||
| font-family: ${fontFamily.mono}; | ||
| font-size: ${fontSize.xs}; | ||
| color: ${t(colors.gray[900], colors.gray[100])}; | ||
| word-break: break-all; | ||
| `, | ||
| noSelection: css` | ||
| flex: 1; | ||
| display: flex; | ||
| align-items: center; | ||
| justify-content: center; | ||
| color: ${t(colors.gray[500], colors.gray[500])}; | ||
| font-style: italic; | ||
| text-align: center; | ||
| padding: ${size[4]}; | ||
| `, | ||
| sectionContainer: css` | ||
| display: flex; | ||
| flex-wrap: wrap; | ||
| gap: ${size[4]}; | ||
| `, | ||
| section: css` | ||
| background: ${t(colors.gray[100], colors.darkGray[800])}; | ||
| border-radius: ${border.radius.lg}; | ||
| box-shadow: ${tokens.shadow.md(t(colors.gray[400] + alpha[80], colors.black + alpha[80]))}; | ||
| padding: ${size[4]}; | ||
| margin-bottom: ${size[4]}; | ||
| border: 1px solid ${t(colors.gray[200], colors.darkGray[700])}; | ||
| min-width: 0; | ||
| max-width: 33%; | ||
| max-height: fit-content; | ||
| `, | ||
| sectionHeader: css` | ||
| font-size: ${fontSize.lg}; | ||
| font-weight: ${font.weight.bold}; | ||
| margin-bottom: ${size[2]}; | ||
| color: ${t(colors.blue[600], colors.blue[400])}; | ||
| letter-spacing: 0.01em; | ||
| display: flex; | ||
| align-items: center; | ||
| gap: ${size[2]}; | ||
| `, | ||
| sectionEmpty: css` | ||
| color: ${t(colors.gray[500], colors.gray[500])}; | ||
| font-size: ${fontSize.sm}; | ||
| font-style: italic; | ||
| margin: ${size[2]} 0; | ||
| `, | ||
| instanceList: css` | ||
| display: flex; | ||
| flex-direction: column; | ||
| gap: ${size[2]}; | ||
| background: ${t(colors.gray[200], colors.darkGray[700])}; | ||
| border: 1px solid ${t(colors.gray[300], colors.darkGray[600])}; | ||
| `, | ||
| instanceCard: css` | ||
| background: ${t(colors.gray[200], colors.darkGray[700])}; | ||
| border-radius: ${border.radius.md}; | ||
| padding: ${size[3]}; | ||
| border: 1px solid ${t(colors.gray[300], colors.darkGray[600])}; | ||
| font-size: ${fontSize.sm}; | ||
| color: ${t(colors.gray[900], colors.gray[100])}; | ||
| font-family: ${fontFamily.mono}; | ||
| overflow-x: auto; | ||
| transition: | ||
| box-shadow 0.3s, | ||
| background 0.3s; | ||
| ` | ||
| }; | ||
| }; | ||
| function useStyles() { | ||
| const { theme } = createTheme(); | ||
| const [styles, setStyles] = createSignal(stylesFactory(theme())); | ||
| createEffect(() => { | ||
| setStyles(stylesFactory(theme())); | ||
| }); | ||
| return styles; | ||
| } | ||
| //#endregion | ||
| //#region src/components/util-groups.ts | ||
| const UTIL_GROUPS = [ | ||
| { | ||
| key: "debouncers", | ||
| label: "Debouncers", | ||
| displayName: "Debouncer" | ||
| }, | ||
| { | ||
| key: "throttlers", | ||
| label: "Throttlers", | ||
| displayName: "Throttler" | ||
| }, | ||
| { | ||
| key: "batchers", | ||
| label: "Batchers", | ||
| displayName: "Batcher" | ||
| }, | ||
| { | ||
| key: "queuers", | ||
| label: "Queuers", | ||
| displayName: "Queuer" | ||
| }, | ||
| { | ||
| key: "rateLimiters", | ||
| label: "Rate Limiters", | ||
| displayName: "Rate Limiter" | ||
| }, | ||
| { | ||
| key: "asyncDebouncers", | ||
| label: "Async Debouncers", | ||
| displayName: "Async Debouncer" | ||
| }, | ||
| { | ||
| key: "asyncThrottlers", | ||
| label: "Async Throttlers", | ||
| displayName: "Async Throttler" | ||
| }, | ||
| { | ||
| key: "asyncBatchers", | ||
| label: "Async Batchers", | ||
| displayName: "Async Batcher" | ||
| }, | ||
| { | ||
| key: "asyncQueuers", | ||
| label: "Async Queuers", | ||
| displayName: "Async Queuer" | ||
| }, | ||
| { | ||
| key: "asyncRateLimiters", | ||
| label: "Async Rate Limiters", | ||
| displayName: "Async Rate Limiter" | ||
| } | ||
| ]; | ||
| //#endregion | ||
| //#region src/components/UtilList.tsx | ||
| var _tmpl$$4 = /*#__PURE__*/ template(`<div>`); | ||
| var _tmpl$2$3 = /*#__PURE__*/ template(`<div><div>`); | ||
| var _tmpl$3$2 = /*#__PURE__*/ template(`<div><div></div><div>`); | ||
| function UtilList(props) { | ||
| const styles = useStyles(); | ||
| return (() => { | ||
| var _el$ = _tmpl$$4(); | ||
| insert(_el$, createComponent(For, { | ||
| each: UTIL_GROUPS, | ||
| children: (group) => memo(() => memo(() => props.utilState()[group.key].length > 0)() && (() => { | ||
| var _el$2 = _tmpl$2$3(), _el$3 = _el$2.firstChild; | ||
| insert(_el$3, () => group.label); | ||
| insert(_el$2, createComponent(For, { | ||
| get each() { | ||
| return props.utilState()[group.key]; | ||
| }, | ||
| children: (instance) => { | ||
| const status = (() => { | ||
| try { | ||
| const statusAccessor = useSelector(instance.store, (s) => s.status); | ||
| return () => statusAccessor() ?? "unknown"; | ||
| } catch { | ||
| return () => "unknown"; | ||
| } | ||
| })(); | ||
| return (() => { | ||
| var _el$4 = _tmpl$3$2(), _el$5 = _el$4.firstChild, _el$6 = _el$5.nextSibling; | ||
| _el$4.$$click = () => props.setSelectedKey(instance.key ?? null); | ||
| insert(_el$5, () => instance.key); | ||
| insert(_el$6, status); | ||
| effect((_p$) => { | ||
| var _v$3 = clsx(styles().utilRow, props.selectedKey() === instance.key && styles().utilRowSelected), _v$4 = styles().utilKey, _v$5 = styles().utilStatus; | ||
| _v$3 !== _p$.e && className(_el$4, _p$.e = _v$3); | ||
| _v$4 !== _p$.t && className(_el$5, _p$.t = _v$4); | ||
| _v$5 !== _p$.a && className(_el$6, _p$.a = _v$5); | ||
| return _p$; | ||
| }, { | ||
| e: void 0, | ||
| t: void 0, | ||
| a: void 0 | ||
| }); | ||
| return _el$4; | ||
| })(); | ||
| } | ||
| }), null); | ||
| effect((_p$) => { | ||
| var _v$ = styles().utilGroup, _v$2 = styles().utilGroupHeader; | ||
| _v$ !== _p$.e && className(_el$2, _p$.e = _v$); | ||
| _v$2 !== _p$.t && className(_el$3, _p$.t = _v$2); | ||
| return _p$; | ||
| }, { | ||
| e: void 0, | ||
| t: void 0 | ||
| }); | ||
| return _el$2; | ||
| })()) | ||
| })); | ||
| effect(() => className(_el$, styles().utilList)); | ||
| return _el$; | ||
| })(); | ||
| } | ||
| delegateEvents(["click"]); | ||
| //#endregion | ||
| //#region src/utils/read-pacer-store-state.ts | ||
| /** TanStack Store (`@tanstack/store`) used by pacer utils. */ | ||
| function isPacerUtilTanStackStore(x) { | ||
| return typeof x === "object" && x !== null && "subscribe" in x && typeof x.subscribe === "function" && "get" in x && typeof x.get === "function"; | ||
| } | ||
| /** | ||
| * Core pacer utils expose TanStack `Store` (`get()` / `state`). React hooks may | ||
| * also expose a reactive `state` field. Use this for devtools UI that needs a | ||
| * plain snapshot (e.g. JsonTree). | ||
| */ | ||
| function getPacerUtilStoreState(instance) { | ||
| const store = instance.store; | ||
| if (store && typeof store.get === "function") return store.get(); | ||
| if (store && store.state !== void 0) return store.state; | ||
| return instance.state; | ||
| } | ||
| //#endregion | ||
| //#region src/components/ActionButtons.tsx | ||
| var _tmpl$$3 = /*#__PURE__*/ template(`<div>No actions available for this util`); | ||
| var _tmpl$2$2 = /*#__PURE__*/ template(`<div>`); | ||
| var _tmpl$3$1 = /*#__PURE__*/ template(`<button><span>`); | ||
| var _tmpl$4 = /*#__PURE__*/ template(`<button><span></span>Flush`); | ||
| var _tmpl$5 = /*#__PURE__*/ template(`<button><span></span>Cancel`); | ||
| var _tmpl$6 = /*#__PURE__*/ template(`<button><span></span>Reset`); | ||
| var _tmpl$7 = /*#__PURE__*/ template(`<button><span></span>Clear`); | ||
| function ActionButtons(props) { | ||
| const styles = useStyles(); | ||
| const utilInstance = props.instance; | ||
| const store = utilInstance?.store; | ||
| const stateAccessor = isPacerUtilTanStackStore(store) ? useSelector(store, (s) => s !== null && s !== void 0 && typeof s === "object" ? s : {}, { compare: shallow }) : () => getPacerUtilStoreState(utilInstance); | ||
| const getState = () => stateAccessor(); | ||
| const hasPending = () => { | ||
| return "isPending" in getState(); | ||
| }; | ||
| const isPending = () => { | ||
| const u = getState(); | ||
| return "isPending" in u ? !!u.isPending : false; | ||
| }; | ||
| const isEmptyFlag = () => { | ||
| const u = getState(); | ||
| return "isEmpty" in u ? !!u.isEmpty : false; | ||
| }; | ||
| const hasFlush = typeof utilInstance.flush === "function"; | ||
| const hasCancel = typeof utilInstance.cancel === "function"; | ||
| const hasReset = typeof utilInstance.reset === "function"; | ||
| const hasClear = typeof utilInstance.clear === "function"; | ||
| const hasStart = typeof utilInstance.start === "function"; | ||
| const hasStop = typeof utilInstance.stop === "function"; | ||
| const hasStartStop = hasStart && hasStop; | ||
| const isRunning = () => { | ||
| const u = getState(); | ||
| return "isRunning" in u ? !!u.isRunning : true; | ||
| }; | ||
| if (!hasPending() && !hasFlush && !hasCancel && !hasReset && !hasClear) return (() => { | ||
| var _el$ = _tmpl$$3(); | ||
| effect(() => className(_el$, styles().sectionEmpty)); | ||
| return _el$; | ||
| })(); | ||
| const emitName = `d-${props.utilName}`; | ||
| const utilNameLower = props.utilName.toLowerCase(); | ||
| return (() => { | ||
| var _el$2 = _tmpl$2$2(); | ||
| insert(_el$2, (() => { | ||
| var _c$ = memo(() => !!hasPending()); | ||
| return () => _c$() && (() => { | ||
| var _el$3 = _tmpl$3$1(), _el$4 = _el$3.firstChild; | ||
| _el$3.$$mousedown = () => { | ||
| const next = !isPending(); | ||
| utilInstance.store.setState((prev) => ({ | ||
| ...prev, | ||
| isPending: next | ||
| })); | ||
| emitChange(emitName, utilInstance); | ||
| }; | ||
| insert(_el$3, () => isPending() ? "Restore Pending" : "Trigger Pending", null); | ||
| effect((_p$) => { | ||
| var _v$ = styles().actionButton, _v$2 = styles().actionDotBlue; | ||
| _v$ !== _p$.e && className(_el$3, _p$.e = _v$); | ||
| _v$2 !== _p$.t && className(_el$4, _p$.t = _v$2); | ||
| return _p$; | ||
| }, { | ||
| e: void 0, | ||
| t: void 0 | ||
| }); | ||
| return _el$3; | ||
| })(); | ||
| })(), null); | ||
| insert(_el$2, hasFlush && (() => { | ||
| var _el$5 = _tmpl$4(), _el$6 = _el$5.firstChild; | ||
| _el$5.$$mousedown = () => { | ||
| utilInstance.flush(); | ||
| emitChange(emitName, utilInstance); | ||
| }; | ||
| effect((_p$) => { | ||
| var _v$3 = styles().actionButton, _v$4 = utilNameLower.includes("debouncer") && !isPending() || utilNameLower.includes("throttler") && !isPending() || utilNameLower.includes("batcher") && isEmptyFlag() || utilNameLower.includes("queuer") && isEmptyFlag(), _v$5 = styles().actionDotGreen; | ||
| _v$3 !== _p$.e && className(_el$5, _p$.e = _v$3); | ||
| _v$4 !== _p$.t && (_el$5.disabled = _p$.t = _v$4); | ||
| _v$5 !== _p$.a && className(_el$6, _p$.a = _v$5); | ||
| return _p$; | ||
| }, { | ||
| e: void 0, | ||
| t: void 0, | ||
| a: void 0 | ||
| }); | ||
| return _el$5; | ||
| })(), null); | ||
| insert(_el$2, hasCancel && (() => { | ||
| var _el$7 = _tmpl$5(), _el$8 = _el$7.firstChild; | ||
| _el$7.$$mousedown = () => { | ||
| utilInstance.cancel(); | ||
| emitChange(emitName, utilInstance); | ||
| }; | ||
| effect((_p$) => { | ||
| var _v$6 = styles().actionButton, _v$7 = utilNameLower.includes("debouncer") && !isPending() || utilNameLower.includes("throttler") && !isPending() || utilNameLower.includes("batcher") && isEmptyFlag() || utilNameLower.includes("queuer") && isEmptyFlag(), _v$8 = styles().actionDotRed; | ||
| _v$6 !== _p$.e && className(_el$7, _p$.e = _v$6); | ||
| _v$7 !== _p$.t && (_el$7.disabled = _p$.t = _v$7); | ||
| _v$8 !== _p$.a && className(_el$8, _p$.a = _v$8); | ||
| return _p$; | ||
| }, { | ||
| e: void 0, | ||
| t: void 0, | ||
| a: void 0 | ||
| }); | ||
| return _el$7; | ||
| })(), null); | ||
| insert(_el$2, hasReset && (() => { | ||
| var _el$9 = _tmpl$6(), _el$0 = _el$9.firstChild; | ||
| _el$9.$$mousedown = () => { | ||
| utilInstance.reset(); | ||
| emitChange(emitName, utilInstance); | ||
| }; | ||
| effect((_p$) => { | ||
| var _v$9 = styles().actionButton, _v$0 = styles().actionDotYellow; | ||
| _v$9 !== _p$.e && className(_el$9, _p$.e = _v$9); | ||
| _v$0 !== _p$.t && className(_el$0, _p$.t = _v$0); | ||
| return _p$; | ||
| }, { | ||
| e: void 0, | ||
| t: void 0 | ||
| }); | ||
| return _el$9; | ||
| })(), null); | ||
| insert(_el$2, hasClear && (() => { | ||
| var _el$1 = _tmpl$7(), _el$10 = _el$1.firstChild; | ||
| _el$1.$$mousedown = () => { | ||
| utilInstance.clear(); | ||
| emitChange(emitName, utilInstance); | ||
| }; | ||
| effect((_p$) => { | ||
| var _v$1 = styles().actionButton, _v$10 = utilNameLower.includes("batcher") && isEmptyFlag() || utilNameLower.includes("queuer") && isEmptyFlag(), _v$11 = styles().actionDotOrange; | ||
| _v$1 !== _p$.e && className(_el$1, _p$.e = _v$1); | ||
| _v$10 !== _p$.t && (_el$1.disabled = _p$.t = _v$10); | ||
| _v$11 !== _p$.a && className(_el$10, _p$.a = _v$11); | ||
| return _p$; | ||
| }, { | ||
| e: void 0, | ||
| t: void 0, | ||
| a: void 0 | ||
| }); | ||
| return _el$1; | ||
| })(), null); | ||
| insert(_el$2, hasStartStop && (() => { | ||
| var _el$11 = _tmpl$3$1(), _el$12 = _el$11.firstChild; | ||
| _el$11.$$mousedown = () => { | ||
| if (isRunning()) utilInstance.stop(); | ||
| else utilInstance.start(); | ||
| emitChange(emitName, utilInstance); | ||
| }; | ||
| insert(_el$11, () => isRunning() ? "Stop" : "Start", null); | ||
| effect((_p$) => { | ||
| var _v$12 = styles().actionButton, _v$13 = styles().actionDotPurple; | ||
| _v$12 !== _p$.e && className(_el$11, _p$.e = _v$12); | ||
| _v$13 !== _p$.t && className(_el$12, _p$.t = _v$13); | ||
| return _p$; | ||
| }, { | ||
| e: void 0, | ||
| t: void 0 | ||
| }); | ||
| return _el$11; | ||
| })(), null); | ||
| effect(() => className(_el$2, styles().actionsRow)); | ||
| return _el$2; | ||
| })(); | ||
| } | ||
| delegateEvents(["mousedown"]); | ||
| //#endregion | ||
| //#region src/components/StateHeader.tsx | ||
| var _tmpl$$2 = /*#__PURE__*/ template(`<div><div></div><div style=display:flex;align-items:center;gap:16px><div><div>Key</div><div></div><div>Last Updated</div><div> (<!>)</div></div><div style=margin-left:auto;font-weight:bold>% reduction`); | ||
| dayjs.extend(relativeTime); | ||
| function reductionFromState(entry, state) { | ||
| if (!state) return 0; | ||
| const completedExecutions = entry.type.toLowerCase().includes("async") ? Number(state.settleCount) || 0 : Number(state.executionCount) || 0; | ||
| if (entry.type.toLowerCase().includes("batcher")) { | ||
| const totalItemsProcessed = Number(state.totalItemsProcessed) || 0; | ||
| if (totalItemsProcessed === 0) return 0; | ||
| return Math.round((totalItemsProcessed - completedExecutions) / totalItemsProcessed * 100); | ||
| } | ||
| let requestCount = 0; | ||
| if (state.maybeExecuteCount !== void 0) requestCount = Number(state.maybeExecuteCount) || 0; | ||
| else if (state.addItemCount !== void 0) requestCount = Number(state.addItemCount) || 0; | ||
| else return 0; | ||
| if (requestCount === 0) return 0; | ||
| const reduction = requestCount - completedExecutions; | ||
| return Math.round(reduction / requestCount * 100); | ||
| } | ||
| function StateHeaderInner(props) { | ||
| const key = props.entry.instance.key; | ||
| const updatedAt = () => props.utilState().lastUpdatedByKey[key] ?? Date.now(); | ||
| const getRelativeTime = () => { | ||
| const at = updatedAt(); | ||
| const diffMs = props.now() - at; | ||
| const diffSeconds = Math.floor(diffMs / 1e3); | ||
| if (diffSeconds < 60) return `${diffSeconds} second${diffSeconds !== 1 ? "s" : ""} ago`; | ||
| return dayjs(at).fromNow(); | ||
| }; | ||
| const store = props.entry.instance?.store; | ||
| const stateAccessor = isPacerUtilTanStackStore(store) ? useSelector(store, (s) => s !== null && s !== void 0 && typeof s === "object" ? s : {}, { compare: shallow }) : () => getPacerUtilStoreState(props.entry.instance); | ||
| const reductionPercentage = () => reductionFromState(props.entry, stateAccessor()); | ||
| return (() => { | ||
| var _el$ = _tmpl$$2(), _el$2 = _el$.firstChild, _el$4 = _el$2.nextSibling.firstChild, _el$5 = _el$4.firstChild, _el$6 = _el$5.nextSibling, _el$7 = _el$6.nextSibling, _el$8 = _el$7.nextSibling, _el$9 = _el$8.firstChild, _el$1 = _el$9.nextSibling; | ||
| _el$1.nextSibling; | ||
| var _el$10 = _el$4.nextSibling, _el$11 = _el$10.firstChild; | ||
| insert(_el$2, () => props.entry.type); | ||
| insert(_el$6, key); | ||
| insert(_el$8, () => new Date(updatedAt()).toLocaleTimeString(), _el$9); | ||
| insert(_el$8, getRelativeTime, _el$1); | ||
| insert(_el$10, reductionPercentage, _el$11); | ||
| effect((_p$) => { | ||
| var _v$ = props.styles.stateHeader, _v$2 = props.styles.stateTitle, _v$3 = props.styles.infoGrid, _v$4 = props.styles.infoLabel, _v$5 = props.styles.infoValueMono, _v$6 = props.styles.infoLabel, _v$7 = props.styles.infoValueMono, _v$8 = props.styles.infoValueMono; | ||
| _v$ !== _p$.e && className(_el$, _p$.e = _v$); | ||
| _v$2 !== _p$.t && className(_el$2, _p$.t = _v$2); | ||
| _v$3 !== _p$.a && className(_el$4, _p$.a = _v$3); | ||
| _v$4 !== _p$.o && className(_el$5, _p$.o = _v$4); | ||
| _v$5 !== _p$.i && className(_el$6, _p$.i = _v$5); | ||
| _v$6 !== _p$.n && className(_el$7, _p$.n = _v$6); | ||
| _v$7 !== _p$.s && className(_el$8, _p$.s = _v$7); | ||
| _v$8 !== _p$.h && className(_el$10, _p$.h = _v$8); | ||
| return _p$; | ||
| }, { | ||
| e: void 0, | ||
| t: void 0, | ||
| a: void 0, | ||
| o: void 0, | ||
| i: void 0, | ||
| n: void 0, | ||
| s: void 0, | ||
| h: void 0 | ||
| }); | ||
| return _el$; | ||
| })(); | ||
| } | ||
| function StateHeader(props) { | ||
| const styles = useStyles(); | ||
| const [now, setNow] = createSignal(Date.now()); | ||
| onMount(() => { | ||
| const interval = setInterval(() => { | ||
| setNow(Date.now()); | ||
| }, 1e3); | ||
| onCleanup(() => { | ||
| clearInterval(interval); | ||
| }); | ||
| }); | ||
| return createComponent(Show, { | ||
| get when() { | ||
| return props.selectedInstance(); | ||
| }, | ||
| children: (entry) => createComponent(StateHeaderInner, { | ||
| get entry() { | ||
| return entry(); | ||
| }, | ||
| get styles() { | ||
| return styles(); | ||
| }, | ||
| now, | ||
| get utilState() { | ||
| return props.utilState; | ||
| } | ||
| }) | ||
| }); | ||
| } | ||
| //#endregion | ||
| //#region src/components/UtilStateJsonTree.tsx | ||
| /** | ||
| * Subscribes to the util's TanStack Store so the tree updates live (same | ||
| * idea as {@link UtilList} status). A plain snapshot would only refresh when the | ||
| * devtools shell re-renders, which does not happen on nested store updates. | ||
| */ | ||
| function UtilStateJsonTree(props) { | ||
| const store = props.instance?.store; | ||
| if (isPacerUtilTanStackStore(store)) { | ||
| const snapshot = useSelector(store, (s) => s); | ||
| return createComponent(JsonTree, { get value() { | ||
| return snapshot(); | ||
| } }); | ||
| } | ||
| return createComponent(JsonTree, { get value() { | ||
| return getPacerUtilStoreState(props.instance); | ||
| } }); | ||
| } | ||
| //#endregion | ||
| //#region src/components/DetailsPanel.tsx | ||
| var _tmpl$$1 = /*#__PURE__*/ template(`<div>`); | ||
| var _tmpl$2$1 = /*#__PURE__*/ template(`<div>Select a util from the left panel to view its state`); | ||
| var _tmpl$3 = /*#__PURE__*/ template(`<div><div><div>Actions</div></div><div><div>State</div><div></div></div><div><div>Options</div><div>`); | ||
| function DetailsPanel(props) { | ||
| const styles = useStyles(); | ||
| return (() => { | ||
| var _el$ = _tmpl$$1(); | ||
| insert(_el$, () => { | ||
| const entry = props.selectedInstance(); | ||
| if (!entry) return (() => { | ||
| var _el$2 = _tmpl$2$1(); | ||
| effect(() => className(_el$2, styles().noSelection)); | ||
| return _el$2; | ||
| })(); | ||
| return [createComponent(StateHeader, { | ||
| get selectedInstance() { | ||
| return props.selectedInstance; | ||
| }, | ||
| get utilState() { | ||
| return props.utilState; | ||
| } | ||
| }), (() => { | ||
| var _el$3 = _tmpl$3(), _el$4 = _el$3.firstChild, _el$5 = _el$4.firstChild, _el$6 = _el$4.nextSibling, _el$7 = _el$6.firstChild, _el$8 = _el$7.nextSibling, _el$9 = _el$6.nextSibling, _el$0 = _el$9.firstChild, _el$1 = _el$0.nextSibling; | ||
| insert(_el$4, createComponent(ActionButtons, { | ||
| get instance() { | ||
| return entry.instance; | ||
| }, | ||
| get utilName() { | ||
| return entry.type; | ||
| } | ||
| }), null); | ||
| insert(_el$8, createComponent(UtilStateJsonTree, { get instance() { | ||
| return entry.instance; | ||
| } })); | ||
| insert(_el$1, createComponent(JsonTree, { get value() { | ||
| return entry.instance.options; | ||
| } })); | ||
| effect((_p$) => { | ||
| var _v$ = styles().detailsGrid, _v$2 = styles().detailSection, _v$3 = styles().detailSectionHeader, _v$4 = styles().detailSection, _v$5 = styles().detailSectionHeader, _v$6 = styles().stateContent, _v$7 = styles().detailSection, _v$8 = styles().detailSectionHeader, _v$9 = styles().stateContent; | ||
| _v$ !== _p$.e && className(_el$3, _p$.e = _v$); | ||
| _v$2 !== _p$.t && className(_el$4, _p$.t = _v$2); | ||
| _v$3 !== _p$.a && className(_el$5, _p$.a = _v$3); | ||
| _v$4 !== _p$.o && className(_el$6, _p$.o = _v$4); | ||
| _v$5 !== _p$.i && className(_el$7, _p$.i = _v$5); | ||
| _v$6 !== _p$.n && className(_el$8, _p$.n = _v$6); | ||
| _v$7 !== _p$.s && className(_el$9, _p$.s = _v$7); | ||
| _v$8 !== _p$.h && className(_el$0, _p$.h = _v$8); | ||
| _v$9 !== _p$.r && className(_el$1, _p$.r = _v$9); | ||
| return _p$; | ||
| }, { | ||
| e: void 0, | ||
| t: void 0, | ||
| a: void 0, | ||
| o: void 0, | ||
| i: void 0, | ||
| n: void 0, | ||
| s: void 0, | ||
| h: void 0, | ||
| r: void 0 | ||
| }); | ||
| return _el$3; | ||
| })()]; | ||
| }); | ||
| effect(() => className(_el$, styles().stateDetails)); | ||
| return _el$; | ||
| })(); | ||
| } | ||
| //#endregion | ||
| //#region src/components/Shell.tsx | ||
| var _tmpl$ = /*#__PURE__*/ template(`<div><div style=min-width:150px;max-width:800px></div><div></div><div style=flex:1><div>Details`); | ||
| var _tmpl$2 = /*#__PURE__*/ template(`<div>`); | ||
| function readResizeObserverBlockSize(entry) { | ||
| const [box] = entry.borderBoxSize; | ||
| if (box !== void 0) return box.blockSize; | ||
| return entry.contentRect.height; | ||
| } | ||
| /** Outer plugin slot from TanStack Devtools (not the inner React mount div, which can report a tiny height). */ | ||
| function resolvePluginHost(el) { | ||
| const byId = el.closest("[id^=\"plugin-container-\"]"); | ||
| if (byId) return byId; | ||
| return el.parentElement?.parentElement?.parentElement ?? el.parentElement?.parentElement ?? el.parentElement; | ||
| } | ||
| function Shell() { | ||
| const styles = useStyles(); | ||
| const state = usePacerDevtoolsState(); | ||
| const utilState = () => state; | ||
| const [selectedKey, setSelectedKey] = createSignal(null); | ||
| const [leftPanelWidth, setLeftPanelWidth] = createSignal(300); | ||
| const [isDragging, setIsDragging] = createSignal(false); | ||
| const [shellRootEl, setShellRootEl] = createSignal(null); | ||
| const [slotHeightPx, setSlotHeightPx] = createSignal(void 0); | ||
| const selectedInstance = createMemo(() => { | ||
| const key = selectedKey(); | ||
| if (!key) return null; | ||
| for (const group of UTIL_GROUPS) { | ||
| const instance = utilState()[group.key].find((inst) => inst.key === key); | ||
| if (instance) return { | ||
| instance, | ||
| type: group.displayName | ||
| }; | ||
| } | ||
| return null; | ||
| }); | ||
| let dragStartX = 0; | ||
| let dragStartWidth = 0; | ||
| const handleMouseDown = (e) => { | ||
| e.preventDefault(); | ||
| e.stopPropagation(); | ||
| setIsDragging(true); | ||
| document.body.style.cursor = "col-resize"; | ||
| document.body.style.userSelect = "none"; | ||
| dragStartX = e.clientX; | ||
| dragStartWidth = leftPanelWidth(); | ||
| }; | ||
| const handleMouseMove = (e) => { | ||
| if (!isDragging()) return; | ||
| e.preventDefault(); | ||
| const deltaX = e.clientX - dragStartX; | ||
| const newWidth = Math.max(150, Math.min(800, dragStartWidth + deltaX)); | ||
| setLeftPanelWidth(newWidth); | ||
| }; | ||
| const handleMouseUp = () => { | ||
| setIsDragging(false); | ||
| document.body.style.cursor = ""; | ||
| document.body.style.userSelect = ""; | ||
| }; | ||
| onMount(() => { | ||
| document.addEventListener("mousemove", handleMouseMove); | ||
| document.addEventListener("mouseup", handleMouseUp); | ||
| }); | ||
| onCleanup(() => { | ||
| document.removeEventListener("mousemove", handleMouseMove); | ||
| document.removeEventListener("mouseup", handleMouseUp); | ||
| }); | ||
| /** | ||
| * TanStack Devtools plugin slots use height: 100% without min-height: 0 on flex | ||
| * ancestors, so percentage height often never constrains our tree. Observing the | ||
| * outer `plugin-container-*` host (not the immediate React mount wrapper) and | ||
| * setting an explicit pixel height makes inner overflow-y regions scroll without | ||
| * capping to the mount div’s collapsed height. | ||
| */ | ||
| createEffect(() => { | ||
| const el = shellRootEl(); | ||
| if (!el || typeof ResizeObserver === "undefined") return; | ||
| const pluginSlot = resolvePluginHost(el); | ||
| if (!pluginSlot) return; | ||
| const ro = new ResizeObserver((entries) => { | ||
| const entry = entries[0]; | ||
| if (!entry) return; | ||
| const target = entry.target; | ||
| const h = target.clientHeight > 0 ? target.clientHeight : readResizeObserverBlockSize(entry); | ||
| if (h > 0) setSlotHeightPx(Math.round(h)); | ||
| }); | ||
| ro.observe(pluginSlot); | ||
| onCleanup(() => ro.disconnect()); | ||
| }); | ||
| return (() => { | ||
| var _el$ = _tmpl$2(); | ||
| use(setShellRootEl, _el$); | ||
| insert(_el$, createComponent(MainPanel, { | ||
| get ["class"]() { | ||
| return styles().mainPanelShell; | ||
| }, | ||
| get children() { | ||
| return [createComponent(Header, { get children() { | ||
| return createComponent(HeaderLogo, { | ||
| flavor: { | ||
| light: "#84cc16", | ||
| dark: "#84cc16" | ||
| }, | ||
| children: "TanStack Pacer" | ||
| }); | ||
| } }), (() => { | ||
| var _el$2 = _tmpl$(), _el$3 = _el$2.firstChild, _el$4 = _el$3.nextSibling, _el$5 = _el$4.nextSibling, _el$6 = _el$5.firstChild; | ||
| insert(_el$3, createComponent(UtilList, { | ||
| selectedKey, | ||
| setSelectedKey, | ||
| utilState | ||
| })); | ||
| _el$4.$$mousedown = handleMouseDown; | ||
| insert(_el$5, createComponent(DetailsPanel, { | ||
| selectedInstance, | ||
| utilState | ||
| }), null); | ||
| effect((_p$) => { | ||
| var _v$ = styles().mainContainer, _v$2 = styles().leftPanel, _v$3 = `${leftPanelWidth()}px`, _v$4 = `${styles().dragHandle} ${isDragging() ? "dragging" : ""}`, _v$5 = styles().rightPanel, _v$6 = styles().panelHeader; | ||
| _v$ !== _p$.e && className(_el$2, _p$.e = _v$); | ||
| _v$2 !== _p$.t && className(_el$3, _p$.t = _v$2); | ||
| _v$3 !== _p$.a && setStyleProperty(_el$3, "width", _p$.a = _v$3); | ||
| _v$4 !== _p$.o && className(_el$4, _p$.o = _v$4); | ||
| _v$5 !== _p$.i && className(_el$5, _p$.i = _v$5); | ||
| _v$6 !== _p$.n && className(_el$6, _p$.n = _v$6); | ||
| return _p$; | ||
| }, { | ||
| e: void 0, | ||
| t: void 0, | ||
| a: void 0, | ||
| o: void 0, | ||
| i: void 0, | ||
| n: void 0 | ||
| }); | ||
| return _el$2; | ||
| })()]; | ||
| } | ||
| })); | ||
| effect((_p$) => { | ||
| var _v$7 = styles().shellRoot, _v$8 = { ...slotHeightPx() !== void 0 ? { | ||
| height: `${slotHeightPx()}px`, | ||
| "max-height": `${slotHeightPx()}px` | ||
| } : {} }; | ||
| _v$7 !== _p$.e && className(_el$, _p$.e = _v$7); | ||
| _p$.t = style(_el$, _v$8, _p$.t); | ||
| return _p$; | ||
| }, { | ||
| e: void 0, | ||
| t: void 0 | ||
| }); | ||
| return _el$; | ||
| })(); | ||
| } | ||
| delegateEvents(["mousedown"]); | ||
| //#endregion | ||
| //#region src/components/index.tsx | ||
| function PacerDevtools(props) { | ||
| return createComponent(ThemeContextProvider, { | ||
| get theme() { | ||
| return props.theme; | ||
| }, | ||
| get children() { | ||
| return createComponent(PacerContextProvider, { get children() { | ||
| return createComponent(Shell, {}); | ||
| } }); | ||
| } | ||
| }); | ||
| } | ||
| //#endregion | ||
| export { PacerDevtools as default }; |
| import { createComponent, escape, ssr, ssrAttribute, ssrStyle, ssrStyleProperty } from "solid-js/web"; | ||
| import { Header, HeaderLogo, JsonTree, MainPanel, ThemeContextProvider, createTheme } from "@tanstack/devtools-ui"; | ||
| import { createStore, produce } from "solid-js/store"; | ||
| import { For, Show, createContext, createEffect, createMemo, createSignal, onCleanup, onMount, useContext } from "solid-js"; | ||
| import { getPacerDevtoolsInstance, pacerEventClient } from "@tanstack/pacer/event-client"; | ||
| import * as goober from "goober"; | ||
| import { shallow, useSelector } from "@tanstack/solid-store"; | ||
| import clsx from "clsx"; | ||
| import dayjs from "dayjs"; | ||
| import relativeTime from "dayjs/plugin/relativeTime.js"; | ||
| //#region src/PacerContextProvider.tsx | ||
| const initialPacerDevtoolsStore = { | ||
| asyncBatchers: [], | ||
| asyncDebouncers: [], | ||
| asyncQueuers: [], | ||
| asyncRateLimiters: [], | ||
| asyncThrottlers: [], | ||
| batchers: [], | ||
| debouncers: [], | ||
| queuers: [], | ||
| rateLimiters: [], | ||
| throttlers: [], | ||
| lastUpdatedByKey: {} | ||
| }; | ||
| const PacerDevtoolsContext = createContext([initialPacerDevtoolsStore, () => {}]); | ||
| /** | ||
| * Match TanStack Form devtools: subscribe with {@link pacerEventClient.on} per | ||
| * event suffix so the same `pacer:${suffix}` channel the library emits on is | ||
| * observed. `onAllPluginEvents` only sees `tanstack-devtools-global`, which is | ||
| * not always relayed the same way by the shell. | ||
| * | ||
| * `d-*` suffixes are used when the devtools panel pushes state back (see | ||
| * ActionButtons); those must update the panel store too. | ||
| */ | ||
| const PACER_DEVTOOLS_UTIL_EVENTS = [ | ||
| { | ||
| listKey: "asyncBatchers", | ||
| suffixes: ["AsyncBatcher", "d-AsyncBatcher"] | ||
| }, | ||
| { | ||
| listKey: "asyncDebouncers", | ||
| suffixes: ["AsyncDebouncer", "d-AsyncDebouncer"] | ||
| }, | ||
| { | ||
| listKey: "asyncQueuers", | ||
| suffixes: ["AsyncQueuer", "d-AsyncQueuer"] | ||
| }, | ||
| { | ||
| listKey: "asyncRateLimiters", | ||
| suffixes: ["AsyncRateLimiter", "d-AsyncRateLimiter"] | ||
| }, | ||
| { | ||
| listKey: "asyncThrottlers", | ||
| suffixes: ["AsyncThrottler", "d-AsyncThrottler"] | ||
| }, | ||
| { | ||
| listKey: "batchers", | ||
| suffixes: ["Batcher", "d-Batcher"] | ||
| }, | ||
| { | ||
| listKey: "debouncers", | ||
| suffixes: ["Debouncer", "d-Debouncer"] | ||
| }, | ||
| { | ||
| listKey: "queuers", | ||
| suffixes: ["Queuer", "d-Queuer"] | ||
| }, | ||
| { | ||
| listKey: "rateLimiters", | ||
| suffixes: ["RateLimiter", "d-RateLimiter"] | ||
| }, | ||
| { | ||
| listKey: "throttlers", | ||
| suffixes: ["Throttler", "d-Throttler"] | ||
| } | ||
| ]; | ||
| function PacerContextProvider(props) { | ||
| const [store, setStore] = createStore(initialPacerDevtoolsStore); | ||
| createEffect(() => { | ||
| const cleanups = []; | ||
| for (const { listKey, suffixes } of PACER_DEVTOOLS_UTIL_EVENTS) for (const suffix of suffixes) cleanups.push(pacerEventClient.on(suffix, (e) => { | ||
| const key = e.payload.key; | ||
| if (!key) return; | ||
| const instance = getPacerDevtoolsInstance(key); | ||
| if (!instance || typeof instance !== "object") return; | ||
| setStore(produce((draft) => { | ||
| const list = draft[listKey]; | ||
| const index = list.findIndex((item) => item.key === key); | ||
| const inst = instance; | ||
| if (index !== -1) list[index] = inst; | ||
| else list.push(inst); | ||
| draft.lastUpdatedByKey[key] = Date.now(); | ||
| })); | ||
| })); | ||
| onCleanup(() => { | ||
| for (const cleanup of cleanups) cleanup(); | ||
| }); | ||
| }); | ||
| return createComponent(PacerDevtoolsContext.Provider, { | ||
| value: [store, setStore], | ||
| get children() { | ||
| return props.children; | ||
| } | ||
| }); | ||
| } | ||
| const usePacerDevtoolsContext = () => { | ||
| return useContext(PacerDevtoolsContext); | ||
| }; | ||
| const usePacerDevtoolsState = () => { | ||
| const [state] = usePacerDevtoolsContext(); | ||
| return state; | ||
| }; | ||
| //#endregion | ||
| //#region src/styles/tokens.ts | ||
| const tokens = { | ||
| colors: { | ||
| inherit: "inherit", | ||
| current: "currentColor", | ||
| transparent: "transparent", | ||
| black: "#000000", | ||
| white: "#ffffff", | ||
| neutral: { | ||
| 50: "#f9fafb", | ||
| 100: "#f2f4f7", | ||
| 200: "#eaecf0", | ||
| 300: "#d0d5dd", | ||
| 400: "#98a2b3", | ||
| 500: "#667085", | ||
| 600: "#475467", | ||
| 700: "#344054", | ||
| 800: "#1d2939", | ||
| 900: "#101828" | ||
| }, | ||
| darkGray: { | ||
| 50: "#525c7a", | ||
| 100: "#49536e", | ||
| 200: "#414962", | ||
| 300: "#394056", | ||
| 400: "#313749", | ||
| 500: "#292e3d", | ||
| 600: "#212530", | ||
| 700: "#191c24", | ||
| 800: "#111318", | ||
| 900: "#0b0d10" | ||
| }, | ||
| gray: { | ||
| 50: "#f9fafb", | ||
| 100: "#f2f4f7", | ||
| 200: "#eaecf0", | ||
| 300: "#d0d5dd", | ||
| 400: "#98a2b3", | ||
| 500: "#667085", | ||
| 600: "#475467", | ||
| 700: "#344054", | ||
| 800: "#1d2939", | ||
| 900: "#101828" | ||
| }, | ||
| blue: { | ||
| 25: "#F5FAFF", | ||
| 50: "#EFF8FF", | ||
| 100: "#D1E9FF", | ||
| 200: "#B2DDFF", | ||
| 300: "#84CAFF", | ||
| 400: "#53B1FD", | ||
| 500: "#2E90FA", | ||
| 600: "#1570EF", | ||
| 700: "#175CD3", | ||
| 800: "#1849A9", | ||
| 900: "#194185" | ||
| }, | ||
| green: { | ||
| 25: "#F6FEF9", | ||
| 50: "#ECFDF3", | ||
| 100: "#D1FADF", | ||
| 200: "#A6F4C5", | ||
| 300: "#6CE9A6", | ||
| 400: "#32D583", | ||
| 500: "#12B76A", | ||
| 600: "#039855", | ||
| 700: "#027A48", | ||
| 800: "#05603A", | ||
| 900: "#054F31" | ||
| }, | ||
| red: { | ||
| 50: "#fef2f2", | ||
| 100: "#fee2e2", | ||
| 200: "#fecaca", | ||
| 300: "#fca5a5", | ||
| 400: "#f87171", | ||
| 500: "#ef4444", | ||
| 600: "#dc2626", | ||
| 700: "#b91c1c", | ||
| 800: "#991b1b", | ||
| 900: "#7f1d1d", | ||
| 950: "#450a0a" | ||
| }, | ||
| yellow: { | ||
| 25: "#FFFCF5", | ||
| 50: "#FFFAEB", | ||
| 100: "#FEF0C7", | ||
| 200: "#FEDF89", | ||
| 300: "#FEC84B", | ||
| 400: "#FDB022", | ||
| 500: "#F79009", | ||
| 600: "#DC6803", | ||
| 700: "#B54708", | ||
| 800: "#93370D", | ||
| 900: "#7A2E0E" | ||
| }, | ||
| purple: { | ||
| 25: "#FAFAFF", | ||
| 50: "#F4F3FF", | ||
| 100: "#EBE9FE", | ||
| 200: "#D9D6FE", | ||
| 300: "#BDB4FE", | ||
| 400: "#9B8AFB", | ||
| 500: "#7A5AF8", | ||
| 600: "#6938EF", | ||
| 700: "#5925DC", | ||
| 800: "#4A1FB8", | ||
| 900: "#3E1C96" | ||
| }, | ||
| teal: { | ||
| 25: "#F6FEFC", | ||
| 50: "#F0FDF9", | ||
| 100: "#CCFBEF", | ||
| 200: "#99F6E0", | ||
| 300: "#5FE9D0", | ||
| 400: "#2ED3B7", | ||
| 500: "#15B79E", | ||
| 600: "#0E9384", | ||
| 700: "#107569", | ||
| 800: "#125D56", | ||
| 900: "#134E48" | ||
| }, | ||
| pink: { | ||
| 25: "#fdf2f8", | ||
| 50: "#fce7f3", | ||
| 100: "#fbcfe8", | ||
| 200: "#f9a8d4", | ||
| 300: "#f472b6", | ||
| 400: "#ec4899", | ||
| 500: "#db2777", | ||
| 600: "#be185d", | ||
| 700: "#9d174d", | ||
| 800: "#831843", | ||
| 900: "#500724" | ||
| }, | ||
| cyan: { | ||
| 25: "#ecfeff", | ||
| 50: "#cffafe", | ||
| 100: "#a5f3fc", | ||
| 200: "#67e8f9", | ||
| 300: "#22d3ee", | ||
| 400: "#06b6d4", | ||
| 500: "#0891b2", | ||
| 600: "#0e7490", | ||
| 700: "#155e75", | ||
| 800: "#164e63", | ||
| 900: "#083344" | ||
| } | ||
| }, | ||
| alpha: { | ||
| 100: "ff", | ||
| 90: "e5", | ||
| 80: "cc", | ||
| 70: "b3", | ||
| 60: "99", | ||
| 50: "80", | ||
| 40: "66", | ||
| 30: "4d", | ||
| 20: "33", | ||
| 10: "1a", | ||
| 0: "00" | ||
| }, | ||
| font: { | ||
| size: { | ||
| "2xs": "calc(var(--tsrd-font-size) * 0.625)", | ||
| xs: "calc(var(--tsrd-font-size) * 0.75)", | ||
| sm: "calc(var(--tsrd-font-size) * 0.875)", | ||
| md: "var(--tsrd-font-size)", | ||
| lg: "calc(var(--tsrd-font-size) * 1.125)", | ||
| xl: "calc(var(--tsrd-font-size) * 1.25)", | ||
| "2xl": "calc(var(--tsrd-font-size) * 1.5)", | ||
| "3xl": "calc(var(--tsrd-font-size) * 1.875)", | ||
| "4xl": "calc(var(--tsrd-font-size) * 2.25)", | ||
| "5xl": "calc(var(--tsrd-font-size) * 3)", | ||
| "6xl": "calc(var(--tsrd-font-size) * 3.75)", | ||
| "7xl": "calc(var(--tsrd-font-size) * 4.5)", | ||
| "8xl": "calc(var(--tsrd-font-size) * 6)", | ||
| "9xl": "calc(var(--tsrd-font-size) * 8)" | ||
| }, | ||
| lineHeight: { | ||
| "3xs": "calc(var(--tsrd-font-size) * 0.75)", | ||
| "2xs": "calc(var(--tsrd-font-size) * 0.875)", | ||
| xs: "calc(var(--tsrd-font-size) * 1)", | ||
| sm: "calc(var(--tsrd-font-size) * 1.25)", | ||
| md: "calc(var(--tsrd-font-size) * 1.5)", | ||
| lg: "calc(var(--tsrd-font-size) * 1.75)", | ||
| xl: "calc(var(--tsrd-font-size) * 2)", | ||
| "2xl": "calc(var(--tsrd-font-size) * 2.25)", | ||
| "3xl": "calc(var(--tsrd-font-size) * 2.5)", | ||
| "4xl": "calc(var(--tsrd-font-size) * 2.75)", | ||
| "5xl": "calc(var(--tsrd-font-size) * 3)", | ||
| "6xl": "calc(var(--tsrd-font-size) * 3.25)", | ||
| "7xl": "calc(var(--tsrd-font-size) * 3.5)", | ||
| "8xl": "calc(var(--tsrd-font-size) * 3.75)", | ||
| "9xl": "calc(var(--tsrd-font-size) * 4)" | ||
| }, | ||
| weight: { | ||
| thin: "100", | ||
| extralight: "200", | ||
| light: "300", | ||
| normal: "400", | ||
| medium: "500", | ||
| semibold: "600", | ||
| bold: "700", | ||
| extrabold: "800", | ||
| black: "900" | ||
| }, | ||
| fontFamily: { | ||
| sans: "ui-sans-serif, Inter, system-ui, sans-serif, sans-serif", | ||
| mono: `ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace` | ||
| } | ||
| }, | ||
| breakpoints: { | ||
| xs: "320px", | ||
| sm: "640px", | ||
| md: "768px", | ||
| lg: "1024px", | ||
| xl: "1280px", | ||
| "2xl": "1536px" | ||
| }, | ||
| border: { radius: { | ||
| none: "0px", | ||
| xs: "calc(var(--tsrd-font-size) * 0.125)", | ||
| sm: "calc(var(--tsrd-font-size) * 0.25)", | ||
| md: "calc(var(--tsrd-font-size) * 0.375)", | ||
| lg: "calc(var(--tsrd-font-size) * 0.5)", | ||
| xl: "calc(var(--tsrd-font-size) * 0.75)", | ||
| "2xl": "calc(var(--tsrd-font-size) * 1)", | ||
| "3xl": "calc(var(--tsrd-font-size) * 1.5)", | ||
| full: "9999px" | ||
| } }, | ||
| size: { | ||
| 0: "0px", | ||
| .25: "calc(var(--tsrd-font-size) * 0.0625)", | ||
| .5: "calc(var(--tsrd-font-size) * 0.125)", | ||
| 1: "calc(var(--tsrd-font-size) * 0.25)", | ||
| 1.5: "calc(var(--tsrd-font-size) * 0.375)", | ||
| 2: "calc(var(--tsrd-font-size) * 0.5)", | ||
| 2.5: "calc(var(--tsrd-font-size) * 0.625)", | ||
| 3: "calc(var(--tsrd-font-size) * 0.75)", | ||
| 3.5: "calc(var(--tsrd-font-size) * 0.875)", | ||
| 4: "calc(var(--tsrd-font-size) * 1)", | ||
| 4.5: "calc(var(--tsrd-font-size) * 1.125)", | ||
| 5: "calc(var(--tsrd-font-size) * 1.25)", | ||
| 5.5: "calc(var(--tsrd-font-size) * 1.375)", | ||
| 6: "calc(var(--tsrd-font-size) * 1.5)", | ||
| 6.5: "calc(var(--tsrd-font-size) * 1.625)", | ||
| 7: "calc(var(--tsrd-font-size) * 1.75)", | ||
| 8: "calc(var(--tsrd-font-size) * 2)", | ||
| 9: "calc(var(--tsrd-font-size) * 2.25)", | ||
| 10: "calc(var(--tsrd-font-size) * 2.5)", | ||
| 11: "calc(var(--tsrd-font-size) * 2.75)", | ||
| 12: "calc(var(--tsrd-font-size) * 3)", | ||
| 14: "calc(var(--tsrd-font-size) * 3.5)", | ||
| 16: "calc(var(--tsrd-font-size) * 4)", | ||
| 20: "calc(var(--tsrd-font-size) * 5)", | ||
| 24: "calc(var(--tsrd-font-size) * 6)", | ||
| 28: "calc(var(--tsrd-font-size) * 7)", | ||
| 32: "calc(var(--tsrd-font-size) * 8)", | ||
| 36: "calc(var(--tsrd-font-size) * 9)", | ||
| 40: "calc(var(--tsrd-font-size) * 10)", | ||
| 44: "calc(var(--tsrd-font-size) * 11)", | ||
| 48: "calc(var(--tsrd-font-size) * 12)", | ||
| 52: "calc(var(--tsrd-font-size) * 13)", | ||
| 56: "calc(var(--tsrd-font-size) * 14)", | ||
| 60: "calc(var(--tsrd-font-size) * 15)", | ||
| 64: "calc(var(--tsrd-font-size) * 16)", | ||
| 72: "calc(var(--tsrd-font-size) * 18)", | ||
| 80: "calc(var(--tsrd-font-size) * 20)", | ||
| 96: "calc(var(--tsrd-font-size) * 24)" | ||
| }, | ||
| shadow: { | ||
| xs: (_ = "rgb(0 0 0 / 0.1)") => `0 1px 2px 0 rgb(0 0 0 / 0.05)`, | ||
| sm: (color = "rgb(0 0 0 / 0.1)") => `0 1px 3px 0 ${color}, 0 1px 2px -1px ${color}`, | ||
| md: (color = "rgb(0 0 0 / 0.1)") => `0 4px 6px -1px ${color}, 0 2px 4px -2px ${color}`, | ||
| lg: (color = "rgb(0 0 0 / 0.1)") => `0 10px 15px -3px ${color}, 0 4px 6px -4px ${color}`, | ||
| xl: (color = "rgb(0 0 0 / 0.1)") => `0 20px 25px -5px ${color}, 0 8px 10px -6px ${color}`, | ||
| "2xl": (color = "rgb(0 0 0 / 0.25)") => `0 25px 50px -12px ${color}`, | ||
| inner: (color = "rgb(0 0 0 / 0.05)") => `inset 0 2px 4px 0 ${color}`, | ||
| none: () => `none` | ||
| }, | ||
| zIndices: { | ||
| hide: -1, | ||
| auto: "auto", | ||
| base: 0, | ||
| docked: 10, | ||
| dropdown: 1e3, | ||
| sticky: 1100, | ||
| banner: 1200, | ||
| overlay: 1300, | ||
| modal: 1400, | ||
| popover: 1500, | ||
| skipLink: 1600, | ||
| toast: 1700, | ||
| tooltip: 1800 | ||
| } | ||
| }; | ||
| //#endregion | ||
| //#region src/styles/use-styles.ts | ||
| const stylesFactory = (theme) => { | ||
| const { colors, font, size, alpha, border } = tokens; | ||
| const { fontFamily, size: fontSize } = font; | ||
| const css = goober.css; | ||
| const t = (light, dark) => theme === "light" ? light : dark; | ||
| return { | ||
| /** | ||
| * Fills the devtools-utils portal div (height: 100%) and is a flex parent so MainPanel | ||
| * gets a real flex height budget (not just height: 100% without a flex chain). | ||
| */ | ||
| shellRoot: css` | ||
| display: flex; | ||
| flex-direction: column; | ||
| box-sizing: border-box; | ||
| width: 100%; | ||
| height: 100%; | ||
| min-height: 0; | ||
| overflow: hidden; | ||
| `, | ||
| /** Overrides devtools-ui MainPanel overflow-y: auto so inner columns own scrolling */ | ||
| mainPanelShell: css` | ||
| display: flex; | ||
| flex-direction: column; | ||
| flex: 1 1 0%; | ||
| min-height: 0; | ||
| overflow: hidden !important; | ||
| `, | ||
| mainContainer: css` | ||
| display: flex; | ||
| flex: 1 1 0%; | ||
| min-height: 0; | ||
| overflow: hidden; | ||
| padding: ${size[2]}; | ||
| padding-top: 0; | ||
| margin-top: ${size[2]}; | ||
| `, | ||
| dragHandle: css` | ||
| width: 8px; | ||
| background: ${t(colors.gray[300], colors.darkGray[600])}; | ||
| cursor: col-resize; | ||
| position: relative; | ||
| transition: all 0.2s ease; | ||
| user-select: none; | ||
| pointer-events: all; | ||
| margin: 0 ${size[1]}; | ||
| border-radius: 2px; | ||
| &:hover { | ||
| background: ${t(colors.blue[600], colors.blue[500])}; | ||
| margin: 0 ${size[1]}; | ||
| } | ||
| &.dragging { | ||
| background: ${t(colors.blue[700], colors.blue[600])}; | ||
| margin: 0 ${size[1]}; | ||
| } | ||
| &::after { | ||
| content: ''; | ||
| position: absolute; | ||
| top: 50%; | ||
| left: 50%; | ||
| transform: translate(-50%, -50%); | ||
| width: 2px; | ||
| height: 20px; | ||
| background: ${t(colors.gray[400], colors.darkGray[400])}; | ||
| border-radius: 1px; | ||
| pointer-events: none; | ||
| } | ||
| &:hover::after, | ||
| &.dragging::after { | ||
| background: ${t(colors.blue[500], colors.blue[300])}; | ||
| } | ||
| `, | ||
| leftPanel: css` | ||
| background: ${t(colors.gray[100], colors.darkGray[800])}; | ||
| border-radius: ${border.radius.lg}; | ||
| border: 1px solid ${t(colors.gray[200], colors.darkGray[700])}; | ||
| display: flex; | ||
| flex-direction: column; | ||
| overflow: hidden; | ||
| min-height: 0; | ||
| flex-shrink: 0; | ||
| `, | ||
| rightPanel: css` | ||
| background: ${t(colors.gray[100], colors.darkGray[800])}; | ||
| border-radius: ${border.radius.lg}; | ||
| border: 1px solid ${t(colors.gray[200], colors.darkGray[700])}; | ||
| display: flex; | ||
| flex-direction: column; | ||
| overflow: hidden; | ||
| min-height: 0; | ||
| flex: 1; | ||
| `, | ||
| panelHeader: css` | ||
| font-size: ${fontSize.md}; | ||
| font-weight: ${font.weight.bold}; | ||
| color: ${t(colors.blue[700], colors.blue[400])}; | ||
| padding: ${size[2]}; | ||
| border-bottom: 1px solid ${t(colors.gray[200], colors.darkGray[700])}; | ||
| background: ${t(colors.gray[100], colors.darkGray[800])}; | ||
| flex-shrink: 0; | ||
| `, | ||
| utilList: css` | ||
| flex: 1; | ||
| overflow-y: auto; | ||
| padding: ${size[1]}; | ||
| min-height: 0; | ||
| `, | ||
| utilGroup: css` | ||
| margin-bottom: ${size[2]}; | ||
| `, | ||
| utilGroupHeader: css` | ||
| font-size: ${fontSize.xs}; | ||
| font-weight: ${font.weight.semibold}; | ||
| color: ${t(colors.gray[600], colors.gray[400])}; | ||
| text-transform: uppercase; | ||
| letter-spacing: 0.05em; | ||
| margin-bottom: ${size[1]}; | ||
| padding: ${size[1]} ${size[2]}; | ||
| background: ${t(colors.gray[200], colors.darkGray[700])}; | ||
| border-radius: ${border.radius.md}; | ||
| `, | ||
| utilRow: css` | ||
| display: flex; | ||
| justify-content: space-between; | ||
| align-items: center; | ||
| padding: ${size[2]}; | ||
| margin-bottom: ${size[1]}; | ||
| background: ${t(colors.gray[200], colors.darkGray[700])}; | ||
| border-radius: ${border.radius.md}; | ||
| cursor: pointer; | ||
| transition: all 0.2s ease; | ||
| border: 1px solid transparent; | ||
| &:hover { | ||
| background: ${t(colors.gray[300], colors.darkGray[600])}; | ||
| border-color: ${t(colors.gray[400], colors.darkGray[500])}; | ||
| } | ||
| `, | ||
| utilRowSelected: css` | ||
| background: ${t(colors.blue[100], colors.blue[900] + alpha[20])}; | ||
| border-color: ${t(colors.blue[600], colors.blue[500])}; | ||
| box-shadow: 0 0 0 1px | ||
| ${t(colors.blue[600] + alpha[30], colors.blue[500] + alpha[30])}; | ||
| `, | ||
| utilKey: css` | ||
| font-family: ${fontFamily.mono}; | ||
| font-size: ${fontSize.xs}; | ||
| color: ${t(colors.gray[900], colors.gray[100])}; | ||
| flex: 1; | ||
| overflow: hidden; | ||
| text-overflow: ellipsis; | ||
| white-space: nowrap; | ||
| `, | ||
| utilStatus: css` | ||
| font-size: ${fontSize.xs}; | ||
| color: ${t(colors.gray[600], colors.gray[400])}; | ||
| text-transform: uppercase; | ||
| letter-spacing: 0.05em; | ||
| padding: ${size[1]} ${size[1]}; | ||
| background: ${t(colors.gray[300], colors.darkGray[600])}; | ||
| border-radius: ${border.radius.sm}; | ||
| margin-left: ${size[1]}; | ||
| `, | ||
| stateDetails: css` | ||
| flex: 1; | ||
| overflow-y: auto; | ||
| padding: ${size[2]}; | ||
| min-height: 0; | ||
| `, | ||
| stateHeader: css` | ||
| margin-bottom: ${size[2]}; | ||
| padding-bottom: ${size[2]}; | ||
| border-bottom: 1px solid ${t(colors.gray[200], colors.darkGray[700])}; | ||
| `, | ||
| stateTitle: css` | ||
| font-size: ${fontSize.md}; | ||
| font-weight: ${font.weight.bold}; | ||
| color: ${t(colors.blue[700], colors.blue[400])}; | ||
| margin-bottom: ${size[1]}; | ||
| `, | ||
| stateKey: css` | ||
| font-family: ${fontFamily.mono}; | ||
| font-size: ${fontSize.xs}; | ||
| color: ${t(colors.gray[600], colors.gray[400])}; | ||
| word-break: break-all; | ||
| `, | ||
| stateContent: css` | ||
| background: ${t(colors.gray[100], colors.darkGray[700])}; | ||
| border-radius: ${border.radius.md}; | ||
| padding: ${size[2]}; | ||
| border: 1px solid ${t(colors.gray[300], colors.darkGray[600])}; | ||
| `, | ||
| detailsGrid: css` | ||
| display: grid; | ||
| grid-template-columns: 1fr; | ||
| gap: ${size[2]}; | ||
| align-items: start; | ||
| `, | ||
| detailSection: css` | ||
| background: ${t(colors.white, colors.darkGray[700])}; | ||
| border: 1px solid ${t(colors.gray[300], colors.darkGray[600])}; | ||
| border-radius: ${border.radius.md}; | ||
| padding: ${size[2]}; | ||
| `, | ||
| detailSectionHeader: css` | ||
| font-size: ${fontSize.sm}; | ||
| font-weight: ${font.weight.bold}; | ||
| color: ${t(colors.gray[800], colors.gray[200])}; | ||
| margin-bottom: ${size[1]}; | ||
| text-transform: uppercase; | ||
| letter-spacing: 0.04em; | ||
| `, | ||
| actionsRow: css` | ||
| display: flex; | ||
| flex-wrap: wrap; | ||
| gap: ${size[2]}; | ||
| `, | ||
| actionButton: css` | ||
| display: inline-flex; | ||
| align-items: center; | ||
| gap: ${size[1]}; | ||
| padding: ${size[1]} ${size[2]}; | ||
| border-radius: ${border.radius.md}; | ||
| border: 1px solid ${t(colors.gray[300], colors.darkGray[500])}; | ||
| background: ${t(colors.gray[200], colors.darkGray[600])}; | ||
| color: ${t(colors.gray[900], colors.gray[100])}; | ||
| font-size: ${fontSize.xs}; | ||
| cursor: pointer; | ||
| user-select: none; | ||
| transition: | ||
| background 0.15s, | ||
| border-color 0.15s; | ||
| &:hover { | ||
| background: ${t(colors.gray[300], colors.darkGray[500])}; | ||
| border-color: ${t(colors.gray[400], colors.darkGray[400])}; | ||
| } | ||
| &:disabled { | ||
| opacity: 0.5; | ||
| cursor: not-allowed; | ||
| &:hover { | ||
| background: ${t(colors.gray[200], colors.darkGray[600])}; | ||
| border-color: ${t(colors.gray[300], colors.darkGray[500])}; | ||
| } | ||
| } | ||
| `, | ||
| actionDotBlue: css` | ||
| width: 6px; | ||
| height: 6px; | ||
| border-radius: 9999px; | ||
| background: ${colors.blue[400]}; | ||
| `, | ||
| actionDotGreen: css` | ||
| width: 6px; | ||
| height: 6px; | ||
| border-radius: 9999px; | ||
| background: ${colors.green[400]}; | ||
| `, | ||
| actionDotRed: css` | ||
| width: 6px; | ||
| height: 6px; | ||
| border-radius: 9999px; | ||
| background: ${colors.red[400]}; | ||
| `, | ||
| actionDotYellow: css` | ||
| width: 6px; | ||
| height: 6px; | ||
| border-radius: 9999px; | ||
| background: ${colors.yellow[400]}; | ||
| `, | ||
| actionDotOrange: css` | ||
| width: 6px; | ||
| height: 6px; | ||
| border-radius: 9999px; | ||
| background: ${colors.pink[400]}; | ||
| `, | ||
| actionDotPurple: css` | ||
| width: 6px; | ||
| height: 6px; | ||
| border-radius: 9999px; | ||
| background: ${colors.purple[400]}; | ||
| `, | ||
| infoGrid: css` | ||
| display: grid; | ||
| grid-template-columns: auto 1fr; | ||
| gap: ${size[1]}; | ||
| row-gap: ${size[1]}; | ||
| align-items: center; | ||
| `, | ||
| infoLabel: css` | ||
| color: ${t(colors.gray[600], colors.gray[400])}; | ||
| font-size: ${fontSize.xs}; | ||
| text-transform: uppercase; | ||
| letter-spacing: 0.05em; | ||
| `, | ||
| infoValueMono: css` | ||
| font-family: ${fontFamily.mono}; | ||
| font-size: ${fontSize.xs}; | ||
| color: ${t(colors.gray[900], colors.gray[100])}; | ||
| word-break: break-all; | ||
| `, | ||
| noSelection: css` | ||
| flex: 1; | ||
| display: flex; | ||
| align-items: center; | ||
| justify-content: center; | ||
| color: ${t(colors.gray[500], colors.gray[500])}; | ||
| font-style: italic; | ||
| text-align: center; | ||
| padding: ${size[4]}; | ||
| `, | ||
| sectionContainer: css` | ||
| display: flex; | ||
| flex-wrap: wrap; | ||
| gap: ${size[4]}; | ||
| `, | ||
| section: css` | ||
| background: ${t(colors.gray[100], colors.darkGray[800])}; | ||
| border-radius: ${border.radius.lg}; | ||
| box-shadow: ${tokens.shadow.md(t(colors.gray[400] + alpha[80], colors.black + alpha[80]))}; | ||
| padding: ${size[4]}; | ||
| margin-bottom: ${size[4]}; | ||
| border: 1px solid ${t(colors.gray[200], colors.darkGray[700])}; | ||
| min-width: 0; | ||
| max-width: 33%; | ||
| max-height: fit-content; | ||
| `, | ||
| sectionHeader: css` | ||
| font-size: ${fontSize.lg}; | ||
| font-weight: ${font.weight.bold}; | ||
| margin-bottom: ${size[2]}; | ||
| color: ${t(colors.blue[600], colors.blue[400])}; | ||
| letter-spacing: 0.01em; | ||
| display: flex; | ||
| align-items: center; | ||
| gap: ${size[2]}; | ||
| `, | ||
| sectionEmpty: css` | ||
| color: ${t(colors.gray[500], colors.gray[500])}; | ||
| font-size: ${fontSize.sm}; | ||
| font-style: italic; | ||
| margin: ${size[2]} 0; | ||
| `, | ||
| instanceList: css` | ||
| display: flex; | ||
| flex-direction: column; | ||
| gap: ${size[2]}; | ||
| background: ${t(colors.gray[200], colors.darkGray[700])}; | ||
| border: 1px solid ${t(colors.gray[300], colors.darkGray[600])}; | ||
| `, | ||
| instanceCard: css` | ||
| background: ${t(colors.gray[200], colors.darkGray[700])}; | ||
| border-radius: ${border.radius.md}; | ||
| padding: ${size[3]}; | ||
| border: 1px solid ${t(colors.gray[300], colors.darkGray[600])}; | ||
| font-size: ${fontSize.sm}; | ||
| color: ${t(colors.gray[900], colors.gray[100])}; | ||
| font-family: ${fontFamily.mono}; | ||
| overflow-x: auto; | ||
| transition: | ||
| box-shadow 0.3s, | ||
| background 0.3s; | ||
| ` | ||
| }; | ||
| }; | ||
| function useStyles() { | ||
| const { theme } = createTheme(); | ||
| const [styles, setStyles] = createSignal(stylesFactory(theme())); | ||
| createEffect(() => { | ||
| setStyles(stylesFactory(theme())); | ||
| }); | ||
| return styles; | ||
| } | ||
| //#endregion | ||
| //#region src/components/util-groups.ts | ||
| const UTIL_GROUPS = [ | ||
| { | ||
| key: "debouncers", | ||
| label: "Debouncers", | ||
| displayName: "Debouncer" | ||
| }, | ||
| { | ||
| key: "throttlers", | ||
| label: "Throttlers", | ||
| displayName: "Throttler" | ||
| }, | ||
| { | ||
| key: "batchers", | ||
| label: "Batchers", | ||
| displayName: "Batcher" | ||
| }, | ||
| { | ||
| key: "queuers", | ||
| label: "Queuers", | ||
| displayName: "Queuer" | ||
| }, | ||
| { | ||
| key: "rateLimiters", | ||
| label: "Rate Limiters", | ||
| displayName: "Rate Limiter" | ||
| }, | ||
| { | ||
| key: "asyncDebouncers", | ||
| label: "Async Debouncers", | ||
| displayName: "Async Debouncer" | ||
| }, | ||
| { | ||
| key: "asyncThrottlers", | ||
| label: "Async Throttlers", | ||
| displayName: "Async Throttler" | ||
| }, | ||
| { | ||
| key: "asyncBatchers", | ||
| label: "Async Batchers", | ||
| displayName: "Async Batcher" | ||
| }, | ||
| { | ||
| key: "asyncQueuers", | ||
| label: "Async Queuers", | ||
| displayName: "Async Queuer" | ||
| }, | ||
| { | ||
| key: "asyncRateLimiters", | ||
| label: "Async Rate Limiters", | ||
| displayName: "Async Rate Limiter" | ||
| } | ||
| ]; | ||
| //#endregion | ||
| //#region src/components/UtilList.tsx | ||
| var _tmpl$$4 = [ | ||
| "<div", | ||
| ">", | ||
| "</div>" | ||
| ]; | ||
| var _tmpl$2$3 = [ | ||
| "<div", | ||
| "><div", | ||
| ">", | ||
| "</div>", | ||
| "</div>" | ||
| ]; | ||
| var _tmpl$3$2 = [ | ||
| "<div", | ||
| "><div", | ||
| ">", | ||
| "</div><div", | ||
| ">", | ||
| "</div></div>" | ||
| ]; | ||
| function UtilList(props) { | ||
| const styles = useStyles(); | ||
| return ssr(_tmpl$$4, ssrAttribute("class", escape(styles().utilList, true), false), escape(createComponent(For, { | ||
| each: UTIL_GROUPS, | ||
| children: (group) => props.utilState()[group.key].length > 0 && ssr(_tmpl$2$3, ssrAttribute("class", escape(styles().utilGroup, true), false), ssrAttribute("class", escape(styles().utilGroupHeader, true), false), escape(group.label), escape(createComponent(For, { | ||
| get each() { | ||
| return props.utilState()[group.key]; | ||
| }, | ||
| children: (instance) => { | ||
| const status = (() => { | ||
| try { | ||
| const statusAccessor = useSelector(instance.store, (s) => s.status); | ||
| return () => statusAccessor() ?? "unknown"; | ||
| } catch { | ||
| return () => "unknown"; | ||
| } | ||
| })(); | ||
| return ssr(_tmpl$3$2, ssrAttribute("class", escape(clsx(styles().utilRow, props.selectedKey() === instance.key && styles().utilRowSelected), true), false), ssrAttribute("class", escape(styles().utilKey, true), false), escape(instance.key), ssrAttribute("class", escape(styles().utilStatus, true), false), escape(status())); | ||
| } | ||
| }))) | ||
| }))); | ||
| } | ||
| //#endregion | ||
| //#region src/utils/read-pacer-store-state.ts | ||
| /** TanStack Store (`@tanstack/store`) used by pacer utils. */ | ||
| function isPacerUtilTanStackStore(x) { | ||
| return typeof x === "object" && x !== null && "subscribe" in x && typeof x.subscribe === "function" && "get" in x && typeof x.get === "function"; | ||
| } | ||
| /** | ||
| * Core pacer utils expose TanStack `Store` (`get()` / `state`). React hooks may | ||
| * also expose a reactive `state` field. Use this for devtools UI that needs a | ||
| * plain snapshot (e.g. JsonTree). | ||
| */ | ||
| function getPacerUtilStoreState(instance) { | ||
| const store = instance.store; | ||
| if (store && typeof store.get === "function") return store.get(); | ||
| if (store && store.state !== void 0) return store.state; | ||
| return instance.state; | ||
| } | ||
| //#endregion | ||
| //#region src/components/ActionButtons.tsx | ||
| var _tmpl$$3 = ["<div", ">No actions available for this util</div>"]; | ||
| var _tmpl$2$2 = [ | ||
| "<div", | ||
| ">", | ||
| "", | ||
| "", | ||
| "", | ||
| "", | ||
| "", | ||
| "</div>" | ||
| ]; | ||
| var _tmpl$3$1 = [ | ||
| "<button", | ||
| "><span", | ||
| "></span>", | ||
| "</button>" | ||
| ]; | ||
| var _tmpl$4 = [ | ||
| "<button", | ||
| "", | ||
| "><span", | ||
| "></span>Flush</button>" | ||
| ]; | ||
| var _tmpl$5 = [ | ||
| "<button", | ||
| "", | ||
| "><span", | ||
| "></span>Cancel</button>" | ||
| ]; | ||
| var _tmpl$6 = [ | ||
| "<button", | ||
| "><span", | ||
| "></span>Reset</button>" | ||
| ]; | ||
| var _tmpl$7 = [ | ||
| "<button", | ||
| "", | ||
| "><span", | ||
| "></span>Clear</button>" | ||
| ]; | ||
| function ActionButtons(props) { | ||
| const styles = useStyles(); | ||
| const utilInstance = props.instance; | ||
| const store = utilInstance?.store; | ||
| const stateAccessor = isPacerUtilTanStackStore(store) ? useSelector(store, (s) => s !== null && s !== void 0 && typeof s === "object" ? s : {}, { compare: shallow }) : () => getPacerUtilStoreState(utilInstance); | ||
| const getState = () => stateAccessor(); | ||
| const hasPending = () => { | ||
| return "isPending" in getState(); | ||
| }; | ||
| const isPending = () => { | ||
| const u = getState(); | ||
| return "isPending" in u ? !!u.isPending : false; | ||
| }; | ||
| const isEmptyFlag = () => { | ||
| const u = getState(); | ||
| return "isEmpty" in u ? !!u.isEmpty : false; | ||
| }; | ||
| const hasFlush = typeof utilInstance.flush === "function"; | ||
| const hasCancel = typeof utilInstance.cancel === "function"; | ||
| const hasReset = typeof utilInstance.reset === "function"; | ||
| const hasClear = typeof utilInstance.clear === "function"; | ||
| const hasStart = typeof utilInstance.start === "function"; | ||
| const hasStop = typeof utilInstance.stop === "function"; | ||
| const hasStartStop = hasStart && hasStop; | ||
| const isRunning = () => { | ||
| const u = getState(); | ||
| return "isRunning" in u ? !!u.isRunning : true; | ||
| }; | ||
| if (!hasPending() && !hasFlush && !hasCancel && !hasReset && !hasClear) return ssr(_tmpl$$3, ssrAttribute("class", escape(styles().sectionEmpty, true), false)); | ||
| `${props.utilName}`; | ||
| const utilNameLower = props.utilName.toLowerCase(); | ||
| return ssr(_tmpl$2$2, ssrAttribute("class", escape(styles().actionsRow, true), false), hasPending() && ssr(_tmpl$3$1, ssrAttribute("class", escape(styles().actionButton, true), false), ssrAttribute("class", escape(styles().actionDotBlue, true), false), isPending() ? "Restore Pending" : "Trigger Pending"), hasFlush && ssr(_tmpl$4, ssrAttribute("class", escape(styles().actionButton, true), false), ssrAttribute("disabled", utilNameLower.includes("debouncer") && !isPending() || utilNameLower.includes("throttler") && !isPending() || utilNameLower.includes("batcher") && isEmptyFlag() || utilNameLower.includes("queuer") && isEmptyFlag(), true), ssrAttribute("class", escape(styles().actionDotGreen, true), false)), hasCancel && ssr(_tmpl$5, ssrAttribute("class", escape(styles().actionButton, true), false), ssrAttribute("disabled", utilNameLower.includes("debouncer") && !isPending() || utilNameLower.includes("throttler") && !isPending() || utilNameLower.includes("batcher") && isEmptyFlag() || utilNameLower.includes("queuer") && isEmptyFlag(), true), ssrAttribute("class", escape(styles().actionDotRed, true), false)), hasReset && ssr(_tmpl$6, ssrAttribute("class", escape(styles().actionButton, true), false), ssrAttribute("class", escape(styles().actionDotYellow, true), false)), hasClear && ssr(_tmpl$7, ssrAttribute("class", escape(styles().actionButton, true), false), ssrAttribute("disabled", utilNameLower.includes("batcher") && isEmptyFlag() || utilNameLower.includes("queuer") && isEmptyFlag(), true), ssrAttribute("class", escape(styles().actionDotOrange, true), false)), hasStartStop && ssr(_tmpl$3$1, ssrAttribute("class", escape(styles().actionButton, true), false), ssrAttribute("class", escape(styles().actionDotPurple, true), false), isRunning() ? "Stop" : "Start")); | ||
| } | ||
| //#endregion | ||
| //#region src/components/StateHeader.tsx | ||
| var _tmpl$$2 = [ | ||
| "<div", | ||
| "><div", | ||
| ">", | ||
| "</div><div style=\"", | ||
| "\"><div", | ||
| "><div", | ||
| ">Key</div><div", | ||
| ">", | ||
| "</div><div", | ||
| ">Last Updated</div><div", | ||
| ">", | ||
| " (", | ||
| ")</div></div><div", | ||
| " style=\"", | ||
| "\">", | ||
| "% reduction</div></div></div>" | ||
| ]; | ||
| dayjs.extend(relativeTime); | ||
| function reductionFromState(entry, state) { | ||
| if (!state) return 0; | ||
| const completedExecutions = entry.type.toLowerCase().includes("async") ? Number(state.settleCount) || 0 : Number(state.executionCount) || 0; | ||
| if (entry.type.toLowerCase().includes("batcher")) { | ||
| const totalItemsProcessed = Number(state.totalItemsProcessed) || 0; | ||
| if (totalItemsProcessed === 0) return 0; | ||
| return Math.round((totalItemsProcessed - completedExecutions) / totalItemsProcessed * 100); | ||
| } | ||
| let requestCount = 0; | ||
| if (state.maybeExecuteCount !== void 0) requestCount = Number(state.maybeExecuteCount) || 0; | ||
| else if (state.addItemCount !== void 0) requestCount = Number(state.addItemCount) || 0; | ||
| else return 0; | ||
| if (requestCount === 0) return 0; | ||
| const reduction = requestCount - completedExecutions; | ||
| return Math.round(reduction / requestCount * 100); | ||
| } | ||
| function StateHeaderInner(props) { | ||
| const key = props.entry.instance.key; | ||
| const updatedAt = () => props.utilState().lastUpdatedByKey[key] ?? Date.now(); | ||
| const getRelativeTime = () => { | ||
| const at = updatedAt(); | ||
| const diffMs = props.now() - at; | ||
| const diffSeconds = Math.floor(diffMs / 1e3); | ||
| if (diffSeconds < 60) return `${diffSeconds} second${diffSeconds !== 1 ? "s" : ""} ago`; | ||
| return dayjs(at).fromNow(); | ||
| }; | ||
| const store = props.entry.instance?.store; | ||
| const stateAccessor = isPacerUtilTanStackStore(store) ? useSelector(store, (s) => s !== null && s !== void 0 && typeof s === "object" ? s : {}, { compare: shallow }) : () => getPacerUtilStoreState(props.entry.instance); | ||
| const reductionPercentage = () => reductionFromState(props.entry, stateAccessor()); | ||
| return ssr(_tmpl$$2, ssrAttribute("class", escape(props.styles.stateHeader, true), false), ssrAttribute("class", escape(props.styles.stateTitle, true), false), escape(props.entry.type), ssrStyleProperty("display:", "flex") + ssrStyleProperty(";align-items:", "center") + ssrStyleProperty(";gap:", "16px"), ssrAttribute("class", escape(props.styles.infoGrid, true), false), ssrAttribute("class", escape(props.styles.infoLabel, true), false), ssrAttribute("class", escape(props.styles.infoValueMono, true), false), escape(key), ssrAttribute("class", escape(props.styles.infoLabel, true), false), ssrAttribute("class", escape(props.styles.infoValueMono, true), false), escape(new Date(updatedAt()).toLocaleTimeString()), escape(getRelativeTime()), ssrAttribute("class", escape(props.styles.infoValueMono, true), false), ssrStyleProperty("margin-left:", "auto") + ssrStyleProperty(";font-weight:", "bold"), escape(reductionPercentage())); | ||
| } | ||
| function StateHeader(props) { | ||
| const styles = useStyles(); | ||
| const [now, setNow] = createSignal(Date.now()); | ||
| onMount(() => { | ||
| const interval = setInterval(() => { | ||
| setNow(Date.now()); | ||
| }, 1e3); | ||
| onCleanup(() => { | ||
| clearInterval(interval); | ||
| }); | ||
| }); | ||
| return createComponent(Show, { | ||
| get when() { | ||
| return props.selectedInstance(); | ||
| }, | ||
| children: (entry) => createComponent(StateHeaderInner, { | ||
| get entry() { | ||
| return entry(); | ||
| }, | ||
| get styles() { | ||
| return styles(); | ||
| }, | ||
| now, | ||
| get utilState() { | ||
| return props.utilState; | ||
| } | ||
| }) | ||
| }); | ||
| } | ||
| //#endregion | ||
| //#region src/components/UtilStateJsonTree.tsx | ||
| /** | ||
| * Subscribes to the util's TanStack Store so the tree updates live (same | ||
| * idea as {@link UtilList} status). A plain snapshot would only refresh when the | ||
| * devtools shell re-renders, which does not happen on nested store updates. | ||
| */ | ||
| function UtilStateJsonTree(props) { | ||
| const store = props.instance?.store; | ||
| if (isPacerUtilTanStackStore(store)) { | ||
| const snapshot = useSelector(store, (s) => s); | ||
| return createComponent(JsonTree, { get value() { | ||
| return snapshot(); | ||
| } }); | ||
| } | ||
| return createComponent(JsonTree, { get value() { | ||
| return getPacerUtilStoreState(props.instance); | ||
| } }); | ||
| } | ||
| //#endregion | ||
| //#region src/components/DetailsPanel.tsx | ||
| var _tmpl$$1 = [ | ||
| "<div", | ||
| ">", | ||
| "</div>" | ||
| ]; | ||
| var _tmpl$2$1 = ["<div", ">Select a util from the left panel to view its state</div>"]; | ||
| var _tmpl$3 = [ | ||
| "<div", | ||
| "><div", | ||
| "><div", | ||
| ">Actions</div>", | ||
| "</div><div", | ||
| "><div", | ||
| ">State</div><div", | ||
| ">", | ||
| "</div></div><div", | ||
| "><div", | ||
| ">Options</div><div", | ||
| ">", | ||
| "</div></div></div>" | ||
| ]; | ||
| function DetailsPanel(props) { | ||
| const styles = useStyles(); | ||
| return ssr(_tmpl$$1, ssrAttribute("class", escape(styles().stateDetails, true), false), (() => { | ||
| const entry = props.selectedInstance(); | ||
| if (!entry) return ssr(_tmpl$2$1, ssrAttribute("class", escape(styles().noSelection, true), false)); | ||
| return escape([createComponent(StateHeader, { | ||
| get selectedInstance() { | ||
| return props.selectedInstance; | ||
| }, | ||
| get utilState() { | ||
| return props.utilState; | ||
| } | ||
| }), ssr(_tmpl$3, ssrAttribute("class", escape(styles().detailsGrid, true), false), ssrAttribute("class", escape(styles().detailSection, true), false), ssrAttribute("class", escape(styles().detailSectionHeader, true), false), escape(createComponent(ActionButtons, { | ||
| get instance() { | ||
| return entry.instance; | ||
| }, | ||
| get utilName() { | ||
| return entry.type; | ||
| } | ||
| })), ssrAttribute("class", escape(styles().detailSection, true), false), ssrAttribute("class", escape(styles().detailSectionHeader, true), false), ssrAttribute("class", escape(styles().stateContent, true), false), escape(createComponent(UtilStateJsonTree, { get instance() { | ||
| return entry.instance; | ||
| } })), ssrAttribute("class", escape(styles().detailSection, true), false), ssrAttribute("class", escape(styles().detailSectionHeader, true), false), ssrAttribute("class", escape(styles().stateContent, true), false), escape(createComponent(JsonTree, { get value() { | ||
| return entry.instance.options; | ||
| } })))]); | ||
| })()); | ||
| } | ||
| //#endregion | ||
| //#region src/components/Shell.tsx | ||
| var _tmpl$ = [ | ||
| "<div", | ||
| "><div", | ||
| " style=\"", | ||
| "\">", | ||
| "</div><div class=\"", | ||
| "\"></div><div", | ||
| " style=\"", | ||
| "\"><div", | ||
| ">Details</div>", | ||
| "</div></div>" | ||
| ]; | ||
| var _tmpl$2 = [ | ||
| "<div", | ||
| " style=\"", | ||
| "\">", | ||
| "</div>" | ||
| ]; | ||
| function readResizeObserverBlockSize(entry) { | ||
| const [box] = entry.borderBoxSize; | ||
| if (box !== void 0) return box.blockSize; | ||
| return entry.contentRect.height; | ||
| } | ||
| /** Outer plugin slot from TanStack Devtools (not the inner React mount div, which can report a tiny height). */ | ||
| function resolvePluginHost(el) { | ||
| const byId = el.closest("[id^=\"plugin-container-\"]"); | ||
| if (byId) return byId; | ||
| return el.parentElement?.parentElement?.parentElement ?? el.parentElement?.parentElement ?? el.parentElement; | ||
| } | ||
| function Shell() { | ||
| const styles = useStyles(); | ||
| const state = usePacerDevtoolsState(); | ||
| const utilState = () => state; | ||
| const [selectedKey, setSelectedKey] = createSignal(null); | ||
| const [leftPanelWidth, setLeftPanelWidth] = createSignal(300); | ||
| const [isDragging, setIsDragging] = createSignal(false); | ||
| const [shellRootEl, setShellRootEl] = createSignal(null); | ||
| const [slotHeightPx, setSlotHeightPx] = createSignal(void 0); | ||
| const selectedInstance = createMemo(() => { | ||
| const key = selectedKey(); | ||
| if (!key) return null; | ||
| for (const group of UTIL_GROUPS) { | ||
| const instance = utilState()[group.key].find((inst) => inst.key === key); | ||
| if (instance) return { | ||
| instance, | ||
| type: group.displayName | ||
| }; | ||
| } | ||
| return null; | ||
| }); | ||
| let dragStartX = 0; | ||
| let dragStartWidth = 0; | ||
| const handleMouseMove = (e) => { | ||
| if (!isDragging()) return; | ||
| e.preventDefault(); | ||
| const deltaX = e.clientX - dragStartX; | ||
| const newWidth = Math.max(150, Math.min(800, dragStartWidth + deltaX)); | ||
| setLeftPanelWidth(newWidth); | ||
| }; | ||
| const handleMouseUp = () => { | ||
| setIsDragging(false); | ||
| document.body.style.cursor = ""; | ||
| document.body.style.userSelect = ""; | ||
| }; | ||
| onMount(() => { | ||
| document.addEventListener("mousemove", handleMouseMove); | ||
| document.addEventListener("mouseup", handleMouseUp); | ||
| }); | ||
| onCleanup(() => { | ||
| document.removeEventListener("mousemove", handleMouseMove); | ||
| document.removeEventListener("mouseup", handleMouseUp); | ||
| }); | ||
| /** | ||
| * TanStack Devtools plugin slots use height: 100% without min-height: 0 on flex | ||
| * ancestors, so percentage height often never constrains our tree. Observing the | ||
| * outer `plugin-container-*` host (not the immediate React mount wrapper) and | ||
| * setting an explicit pixel height makes inner overflow-y regions scroll without | ||
| * capping to the mount div’s collapsed height. | ||
| */ | ||
| createEffect(() => { | ||
| const el = shellRootEl(); | ||
| if (!el || typeof ResizeObserver === "undefined") return; | ||
| const pluginSlot = resolvePluginHost(el); | ||
| if (!pluginSlot) return; | ||
| const ro = new ResizeObserver((entries) => { | ||
| const entry = entries[0]; | ||
| if (!entry) return; | ||
| const target = entry.target; | ||
| const h = target.clientHeight > 0 ? target.clientHeight : readResizeObserverBlockSize(entry); | ||
| if (h > 0) setSlotHeightPx(Math.round(h)); | ||
| }); | ||
| ro.observe(pluginSlot); | ||
| onCleanup(() => ro.disconnect()); | ||
| }); | ||
| return ssr(_tmpl$2, ssrAttribute("class", escape(styles().shellRoot, true), false), ssrStyle({ ...slotHeightPx() !== void 0 ? { | ||
| height: `${slotHeightPx()}px`, | ||
| "max-height": `${slotHeightPx()}px` | ||
| } : {} }), escape(createComponent(MainPanel, { | ||
| get ["class"]() { | ||
| return styles().mainPanelShell; | ||
| }, | ||
| get children() { | ||
| return [createComponent(Header, { get children() { | ||
| return createComponent(HeaderLogo, { | ||
| flavor: { | ||
| light: "#84cc16", | ||
| dark: "#84cc16" | ||
| }, | ||
| children: "TanStack Pacer" | ||
| }); | ||
| } }), ssr(_tmpl$, ssrAttribute("class", escape(styles().mainContainer, true), false), ssrAttribute("class", escape(styles().leftPanel, true), false), ssrStyleProperty("width:", `${escape(leftPanelWidth(), true)}px`) + ssrStyleProperty(";min-width:", "150px") + ssrStyleProperty(";max-width:", "800px"), escape(createComponent(UtilList, { | ||
| selectedKey, | ||
| setSelectedKey, | ||
| utilState | ||
| })), `${escape(styles().dragHandle, true)} ${isDragging() ? "dragging" : ""}`, ssrAttribute("class", escape(styles().rightPanel, true), false), ssrStyleProperty("flex:", 1), ssrAttribute("class", escape(styles().panelHeader, true), false), escape(createComponent(DetailsPanel, { | ||
| selectedInstance, | ||
| utilState | ||
| })))]; | ||
| } | ||
| }))); | ||
| } | ||
| //#endregion | ||
| //#region src/components/index.tsx | ||
| function PacerDevtools(props) { | ||
| return createComponent(ThemeContextProvider, { | ||
| get theme() { | ||
| return props.theme; | ||
| }, | ||
| get children() { | ||
| return createComponent(PacerContextProvider, { get children() { | ||
| return createComponent(Shell, {}); | ||
| } }); | ||
| } | ||
| }); | ||
| } | ||
| //#endregion | ||
| export { PacerDevtools as default }; |
+0
-1
| import { ClassType } from "@tanstack/devtools-utils/solid"; | ||
| //#region src/core.d.ts | ||
@@ -4,0 +3,0 @@ interface PacerDevtoolsInit {} |
+1
-1
| "use client"; | ||
| import { constructCoreClass } from "@tanstack/devtools-utils/solid"; | ||
| //#region src/core.tsx | ||
| const coreClasses = constructCoreClass(() => import("./components-hqYwCNDx.js")); | ||
| const coreClasses = constructCoreClass(() => import("./components-BwHXH_1Y.js")); | ||
| const PacerDevtoolsCore$1 = coreClasses[0]; | ||
@@ -6,0 +6,0 @@ const PacerDevtoolsCoreNoOp = coreClasses[1]; |
| import { ClassType } from "@tanstack/devtools-utils/solid"; | ||
| //#region src/core.d.ts | ||
@@ -4,0 +3,0 @@ interface PacerDevtoolsInit {} |
| "use client"; | ||
| import { constructCoreClass } from "@tanstack/devtools-utils/solid"; | ||
| //#region src/core.tsx | ||
| const coreClasses = constructCoreClass(() => import("../components-hqYwCNDx.js")); | ||
| const coreClasses = constructCoreClass(() => import("../components-BwHXH_1Y.js")); | ||
| const PacerDevtoolsCore = coreClasses[0]; | ||
@@ -6,0 +6,0 @@ coreClasses[1]; |
| "use client"; | ||
| import { constructCoreClass } from "@tanstack/devtools-utils/solid"; | ||
| //#region src/core.tsx | ||
| const coreClasses = constructCoreClass(() => import("../components-DZv7GBzY.js")); | ||
| const coreClasses = constructCoreClass(() => import("../components-BXAd-LGS.js")); | ||
| const PacerDevtoolsCore = coreClasses[0]; | ||
@@ -6,0 +6,0 @@ coreClasses[1]; |
+1
-1
| "use client"; | ||
| import { constructCoreClass } from "@tanstack/devtools-utils/solid"; | ||
| //#region src/core.tsx | ||
| const coreClasses = constructCoreClass(() => import("./components-DZv7GBzY.js")); | ||
| const coreClasses = constructCoreClass(() => import("./components-BXAd-LGS.js")); | ||
| const PacerDevtoolsCore$1 = coreClasses[0]; | ||
@@ -6,0 +6,0 @@ const PacerDevtoolsCoreNoOp = coreClasses[1]; |
+8
-8
| { | ||
| "name": "@tanstack/pacer-devtools", | ||
| "version": "1.3.1", | ||
| "version": "1.4.0", | ||
| "description": "Devtools for Pacer.", | ||
@@ -74,15 +74,15 @@ "author": "Tanner Linsley", | ||
| "dependencies": { | ||
| "@tanstack/devtools-ui": "0.5.2", | ||
| "@tanstack/devtools-utils": "^0.5.0", | ||
| "@tanstack/solid-store": "^0.11.0", | ||
| "@tanstack/devtools-ui": "0.6.0", | ||
| "@tanstack/devtools-utils": "^0.6.0", | ||
| "@tanstack/solid-store": "^0.11.1", | ||
| "clsx": "^2.1.1", | ||
| "csstype": "^3.2.3", | ||
| "dayjs": "^1.11.20", | ||
| "dayjs": "^1.11.21", | ||
| "goober": "^2.1.19", | ||
| "solid-js": "^1.9.12" | ||
| "solid-js": "^1.9.14" | ||
| }, | ||
| "devDependencies": { | ||
| "rolldown-plugin-solid": "^0.2.1", | ||
| "tsdown": "^0.22.0", | ||
| "vite-plugin-solid": "^2.11.12" | ||
| "tsdown": "^0.22.14", | ||
| "vite-plugin-solid": "^2.11.14" | ||
| }, | ||
@@ -89,0 +89,0 @@ "scripts": { |
+17
-2
| <div align="center"> | ||
| <img src="./media/header_pacer.png" > | ||
| <picture> | ||
| <source | ||
| media="(prefers-color-scheme: dark)" | ||
| srcset="https://tanstack.com/api/readme/pacer.png?theme=dark" | ||
| /> | ||
| <source | ||
| media="(prefers-color-scheme: light)" | ||
| srcset="https://tanstack.com/api/readme/pacer.png" | ||
| /> | ||
| <img | ||
| src="https://tanstack.com/api/readme/pacer.png" | ||
| alt="TanStack Pacer" | ||
| width="900" | ||
| /> | ||
| </picture> | ||
| </div> | ||
@@ -32,4 +46,5 @@ | ||
| <div align="center"> | ||
| ### [Become a Sponsor!](https://github.com/sponsors/tannerlinsley/) | ||
| </div> | ||
@@ -36,0 +51,0 @@ |
| import * as goober from 'goober' | ||
| import { createEffect, createSignal } from 'solid-js' | ||
| import { useTheme } from '@tanstack/devtools-ui' | ||
| import { createTheme } from '@tanstack/devtools-ui' | ||
| import { tokens } from './tokens' | ||
@@ -379,3 +379,3 @@ | ||
| export function useStyles() { | ||
| const { theme } = useTheme() | ||
| const { theme } = createTheme() | ||
| const [styles, setStyles] = createSignal(stylesFactory(theme())) | ||
@@ -382,0 +382,0 @@ createEffect(() => { |
| import { createComponent, escape, ssr, ssrAttribute, ssrStyle, ssrStyleProperty } from "solid-js/web"; | ||
| import { Header, HeaderLogo, JsonTree, MainPanel, ThemeContextProvider, useTheme } from "@tanstack/devtools-ui"; | ||
| import { createStore, produce } from "solid-js/store"; | ||
| import { For, Show, createContext, createEffect, createMemo, createSignal, onCleanup, onMount, useContext } from "solid-js"; | ||
| import { getPacerDevtoolsInstance, pacerEventClient } from "@tanstack/pacer/event-client"; | ||
| import * as goober from "goober"; | ||
| import { shallow, useSelector } from "@tanstack/solid-store"; | ||
| import clsx from "clsx"; | ||
| import dayjs from "dayjs"; | ||
| import relativeTime from "dayjs/plugin/relativeTime.js"; | ||
| //#region src/PacerContextProvider.tsx | ||
| const initialPacerDevtoolsStore = { | ||
| asyncBatchers: [], | ||
| asyncDebouncers: [], | ||
| asyncQueuers: [], | ||
| asyncRateLimiters: [], | ||
| asyncThrottlers: [], | ||
| batchers: [], | ||
| debouncers: [], | ||
| queuers: [], | ||
| rateLimiters: [], | ||
| throttlers: [], | ||
| lastUpdatedByKey: {} | ||
| }; | ||
| const PacerDevtoolsContext = createContext([initialPacerDevtoolsStore, () => {}]); | ||
| /** | ||
| * Match TanStack Form devtools: subscribe with {@link pacerEventClient.on} per | ||
| * event suffix so the same `pacer:${suffix}` channel the library emits on is | ||
| * observed. `onAllPluginEvents` only sees `tanstack-devtools-global`, which is | ||
| * not always relayed the same way by the shell. | ||
| * | ||
| * `d-*` suffixes are used when the devtools panel pushes state back (see | ||
| * ActionButtons); those must update the panel store too. | ||
| */ | ||
| const PACER_DEVTOOLS_UTIL_EVENTS = [ | ||
| { | ||
| listKey: "asyncBatchers", | ||
| suffixes: ["AsyncBatcher", "d-AsyncBatcher"] | ||
| }, | ||
| { | ||
| listKey: "asyncDebouncers", | ||
| suffixes: ["AsyncDebouncer", "d-AsyncDebouncer"] | ||
| }, | ||
| { | ||
| listKey: "asyncQueuers", | ||
| suffixes: ["AsyncQueuer", "d-AsyncQueuer"] | ||
| }, | ||
| { | ||
| listKey: "asyncRateLimiters", | ||
| suffixes: ["AsyncRateLimiter", "d-AsyncRateLimiter"] | ||
| }, | ||
| { | ||
| listKey: "asyncThrottlers", | ||
| suffixes: ["AsyncThrottler", "d-AsyncThrottler"] | ||
| }, | ||
| { | ||
| listKey: "batchers", | ||
| suffixes: ["Batcher", "d-Batcher"] | ||
| }, | ||
| { | ||
| listKey: "debouncers", | ||
| suffixes: ["Debouncer", "d-Debouncer"] | ||
| }, | ||
| { | ||
| listKey: "queuers", | ||
| suffixes: ["Queuer", "d-Queuer"] | ||
| }, | ||
| { | ||
| listKey: "rateLimiters", | ||
| suffixes: ["RateLimiter", "d-RateLimiter"] | ||
| }, | ||
| { | ||
| listKey: "throttlers", | ||
| suffixes: ["Throttler", "d-Throttler"] | ||
| } | ||
| ]; | ||
| function PacerContextProvider(props) { | ||
| const [store, setStore] = createStore(initialPacerDevtoolsStore); | ||
| createEffect(() => { | ||
| const cleanups = []; | ||
| for (const { listKey, suffixes } of PACER_DEVTOOLS_UTIL_EVENTS) for (const suffix of suffixes) cleanups.push(pacerEventClient.on(suffix, (e) => { | ||
| const key = e.payload.key; | ||
| if (!key) return; | ||
| const instance = getPacerDevtoolsInstance(key); | ||
| if (!instance || typeof instance !== "object") return; | ||
| setStore(produce((draft) => { | ||
| const list = draft[listKey]; | ||
| const index = list.findIndex((item) => item.key === key); | ||
| const inst = instance; | ||
| if (index !== -1) list[index] = inst; | ||
| else list.push(inst); | ||
| draft.lastUpdatedByKey[key] = Date.now(); | ||
| })); | ||
| })); | ||
| onCleanup(() => { | ||
| for (const cleanup of cleanups) cleanup(); | ||
| }); | ||
| }); | ||
| return createComponent(PacerDevtoolsContext.Provider, { | ||
| value: [store, setStore], | ||
| get children() { | ||
| return props.children; | ||
| } | ||
| }); | ||
| } | ||
| const usePacerDevtoolsContext = () => { | ||
| return useContext(PacerDevtoolsContext); | ||
| }; | ||
| const usePacerDevtoolsState = () => { | ||
| const [state] = usePacerDevtoolsContext(); | ||
| return state; | ||
| }; | ||
| //#endregion | ||
| //#region src/styles/tokens.ts | ||
| const tokens = { | ||
| colors: { | ||
| inherit: "inherit", | ||
| current: "currentColor", | ||
| transparent: "transparent", | ||
| black: "#000000", | ||
| white: "#ffffff", | ||
| neutral: { | ||
| 50: "#f9fafb", | ||
| 100: "#f2f4f7", | ||
| 200: "#eaecf0", | ||
| 300: "#d0d5dd", | ||
| 400: "#98a2b3", | ||
| 500: "#667085", | ||
| 600: "#475467", | ||
| 700: "#344054", | ||
| 800: "#1d2939", | ||
| 900: "#101828" | ||
| }, | ||
| darkGray: { | ||
| 50: "#525c7a", | ||
| 100: "#49536e", | ||
| 200: "#414962", | ||
| 300: "#394056", | ||
| 400: "#313749", | ||
| 500: "#292e3d", | ||
| 600: "#212530", | ||
| 700: "#191c24", | ||
| 800: "#111318", | ||
| 900: "#0b0d10" | ||
| }, | ||
| gray: { | ||
| 50: "#f9fafb", | ||
| 100: "#f2f4f7", | ||
| 200: "#eaecf0", | ||
| 300: "#d0d5dd", | ||
| 400: "#98a2b3", | ||
| 500: "#667085", | ||
| 600: "#475467", | ||
| 700: "#344054", | ||
| 800: "#1d2939", | ||
| 900: "#101828" | ||
| }, | ||
| blue: { | ||
| 25: "#F5FAFF", | ||
| 50: "#EFF8FF", | ||
| 100: "#D1E9FF", | ||
| 200: "#B2DDFF", | ||
| 300: "#84CAFF", | ||
| 400: "#53B1FD", | ||
| 500: "#2E90FA", | ||
| 600: "#1570EF", | ||
| 700: "#175CD3", | ||
| 800: "#1849A9", | ||
| 900: "#194185" | ||
| }, | ||
| green: { | ||
| 25: "#F6FEF9", | ||
| 50: "#ECFDF3", | ||
| 100: "#D1FADF", | ||
| 200: "#A6F4C5", | ||
| 300: "#6CE9A6", | ||
| 400: "#32D583", | ||
| 500: "#12B76A", | ||
| 600: "#039855", | ||
| 700: "#027A48", | ||
| 800: "#05603A", | ||
| 900: "#054F31" | ||
| }, | ||
| red: { | ||
| 50: "#fef2f2", | ||
| 100: "#fee2e2", | ||
| 200: "#fecaca", | ||
| 300: "#fca5a5", | ||
| 400: "#f87171", | ||
| 500: "#ef4444", | ||
| 600: "#dc2626", | ||
| 700: "#b91c1c", | ||
| 800: "#991b1b", | ||
| 900: "#7f1d1d", | ||
| 950: "#450a0a" | ||
| }, | ||
| yellow: { | ||
| 25: "#FFFCF5", | ||
| 50: "#FFFAEB", | ||
| 100: "#FEF0C7", | ||
| 200: "#FEDF89", | ||
| 300: "#FEC84B", | ||
| 400: "#FDB022", | ||
| 500: "#F79009", | ||
| 600: "#DC6803", | ||
| 700: "#B54708", | ||
| 800: "#93370D", | ||
| 900: "#7A2E0E" | ||
| }, | ||
| purple: { | ||
| 25: "#FAFAFF", | ||
| 50: "#F4F3FF", | ||
| 100: "#EBE9FE", | ||
| 200: "#D9D6FE", | ||
| 300: "#BDB4FE", | ||
| 400: "#9B8AFB", | ||
| 500: "#7A5AF8", | ||
| 600: "#6938EF", | ||
| 700: "#5925DC", | ||
| 800: "#4A1FB8", | ||
| 900: "#3E1C96" | ||
| }, | ||
| teal: { | ||
| 25: "#F6FEFC", | ||
| 50: "#F0FDF9", | ||
| 100: "#CCFBEF", | ||
| 200: "#99F6E0", | ||
| 300: "#5FE9D0", | ||
| 400: "#2ED3B7", | ||
| 500: "#15B79E", | ||
| 600: "#0E9384", | ||
| 700: "#107569", | ||
| 800: "#125D56", | ||
| 900: "#134E48" | ||
| }, | ||
| pink: { | ||
| 25: "#fdf2f8", | ||
| 50: "#fce7f3", | ||
| 100: "#fbcfe8", | ||
| 200: "#f9a8d4", | ||
| 300: "#f472b6", | ||
| 400: "#ec4899", | ||
| 500: "#db2777", | ||
| 600: "#be185d", | ||
| 700: "#9d174d", | ||
| 800: "#831843", | ||
| 900: "#500724" | ||
| }, | ||
| cyan: { | ||
| 25: "#ecfeff", | ||
| 50: "#cffafe", | ||
| 100: "#a5f3fc", | ||
| 200: "#67e8f9", | ||
| 300: "#22d3ee", | ||
| 400: "#06b6d4", | ||
| 500: "#0891b2", | ||
| 600: "#0e7490", | ||
| 700: "#155e75", | ||
| 800: "#164e63", | ||
| 900: "#083344" | ||
| } | ||
| }, | ||
| alpha: { | ||
| 100: "ff", | ||
| 90: "e5", | ||
| 80: "cc", | ||
| 70: "b3", | ||
| 60: "99", | ||
| 50: "80", | ||
| 40: "66", | ||
| 30: "4d", | ||
| 20: "33", | ||
| 10: "1a", | ||
| 0: "00" | ||
| }, | ||
| font: { | ||
| size: { | ||
| "2xs": "calc(var(--tsrd-font-size) * 0.625)", | ||
| xs: "calc(var(--tsrd-font-size) * 0.75)", | ||
| sm: "calc(var(--tsrd-font-size) * 0.875)", | ||
| md: "var(--tsrd-font-size)", | ||
| lg: "calc(var(--tsrd-font-size) * 1.125)", | ||
| xl: "calc(var(--tsrd-font-size) * 1.25)", | ||
| "2xl": "calc(var(--tsrd-font-size) * 1.5)", | ||
| "3xl": "calc(var(--tsrd-font-size) * 1.875)", | ||
| "4xl": "calc(var(--tsrd-font-size) * 2.25)", | ||
| "5xl": "calc(var(--tsrd-font-size) * 3)", | ||
| "6xl": "calc(var(--tsrd-font-size) * 3.75)", | ||
| "7xl": "calc(var(--tsrd-font-size) * 4.5)", | ||
| "8xl": "calc(var(--tsrd-font-size) * 6)", | ||
| "9xl": "calc(var(--tsrd-font-size) * 8)" | ||
| }, | ||
| lineHeight: { | ||
| "3xs": "calc(var(--tsrd-font-size) * 0.75)", | ||
| "2xs": "calc(var(--tsrd-font-size) * 0.875)", | ||
| xs: "calc(var(--tsrd-font-size) * 1)", | ||
| sm: "calc(var(--tsrd-font-size) * 1.25)", | ||
| md: "calc(var(--tsrd-font-size) * 1.5)", | ||
| lg: "calc(var(--tsrd-font-size) * 1.75)", | ||
| xl: "calc(var(--tsrd-font-size) * 2)", | ||
| "2xl": "calc(var(--tsrd-font-size) * 2.25)", | ||
| "3xl": "calc(var(--tsrd-font-size) * 2.5)", | ||
| "4xl": "calc(var(--tsrd-font-size) * 2.75)", | ||
| "5xl": "calc(var(--tsrd-font-size) * 3)", | ||
| "6xl": "calc(var(--tsrd-font-size) * 3.25)", | ||
| "7xl": "calc(var(--tsrd-font-size) * 3.5)", | ||
| "8xl": "calc(var(--tsrd-font-size) * 3.75)", | ||
| "9xl": "calc(var(--tsrd-font-size) * 4)" | ||
| }, | ||
| weight: { | ||
| thin: "100", | ||
| extralight: "200", | ||
| light: "300", | ||
| normal: "400", | ||
| medium: "500", | ||
| semibold: "600", | ||
| bold: "700", | ||
| extrabold: "800", | ||
| black: "900" | ||
| }, | ||
| fontFamily: { | ||
| sans: "ui-sans-serif, Inter, system-ui, sans-serif, sans-serif", | ||
| mono: `ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace` | ||
| } | ||
| }, | ||
| breakpoints: { | ||
| xs: "320px", | ||
| sm: "640px", | ||
| md: "768px", | ||
| lg: "1024px", | ||
| xl: "1280px", | ||
| "2xl": "1536px" | ||
| }, | ||
| border: { radius: { | ||
| none: "0px", | ||
| xs: "calc(var(--tsrd-font-size) * 0.125)", | ||
| sm: "calc(var(--tsrd-font-size) * 0.25)", | ||
| md: "calc(var(--tsrd-font-size) * 0.375)", | ||
| lg: "calc(var(--tsrd-font-size) * 0.5)", | ||
| xl: "calc(var(--tsrd-font-size) * 0.75)", | ||
| "2xl": "calc(var(--tsrd-font-size) * 1)", | ||
| "3xl": "calc(var(--tsrd-font-size) * 1.5)", | ||
| full: "9999px" | ||
| } }, | ||
| size: { | ||
| 0: "0px", | ||
| .25: "calc(var(--tsrd-font-size) * 0.0625)", | ||
| .5: "calc(var(--tsrd-font-size) * 0.125)", | ||
| 1: "calc(var(--tsrd-font-size) * 0.25)", | ||
| 1.5: "calc(var(--tsrd-font-size) * 0.375)", | ||
| 2: "calc(var(--tsrd-font-size) * 0.5)", | ||
| 2.5: "calc(var(--tsrd-font-size) * 0.625)", | ||
| 3: "calc(var(--tsrd-font-size) * 0.75)", | ||
| 3.5: "calc(var(--tsrd-font-size) * 0.875)", | ||
| 4: "calc(var(--tsrd-font-size) * 1)", | ||
| 4.5: "calc(var(--tsrd-font-size) * 1.125)", | ||
| 5: "calc(var(--tsrd-font-size) * 1.25)", | ||
| 5.5: "calc(var(--tsrd-font-size) * 1.375)", | ||
| 6: "calc(var(--tsrd-font-size) * 1.5)", | ||
| 6.5: "calc(var(--tsrd-font-size) * 1.625)", | ||
| 7: "calc(var(--tsrd-font-size) * 1.75)", | ||
| 8: "calc(var(--tsrd-font-size) * 2)", | ||
| 9: "calc(var(--tsrd-font-size) * 2.25)", | ||
| 10: "calc(var(--tsrd-font-size) * 2.5)", | ||
| 11: "calc(var(--tsrd-font-size) * 2.75)", | ||
| 12: "calc(var(--tsrd-font-size) * 3)", | ||
| 14: "calc(var(--tsrd-font-size) * 3.5)", | ||
| 16: "calc(var(--tsrd-font-size) * 4)", | ||
| 20: "calc(var(--tsrd-font-size) * 5)", | ||
| 24: "calc(var(--tsrd-font-size) * 6)", | ||
| 28: "calc(var(--tsrd-font-size) * 7)", | ||
| 32: "calc(var(--tsrd-font-size) * 8)", | ||
| 36: "calc(var(--tsrd-font-size) * 9)", | ||
| 40: "calc(var(--tsrd-font-size) * 10)", | ||
| 44: "calc(var(--tsrd-font-size) * 11)", | ||
| 48: "calc(var(--tsrd-font-size) * 12)", | ||
| 52: "calc(var(--tsrd-font-size) * 13)", | ||
| 56: "calc(var(--tsrd-font-size) * 14)", | ||
| 60: "calc(var(--tsrd-font-size) * 15)", | ||
| 64: "calc(var(--tsrd-font-size) * 16)", | ||
| 72: "calc(var(--tsrd-font-size) * 18)", | ||
| 80: "calc(var(--tsrd-font-size) * 20)", | ||
| 96: "calc(var(--tsrd-font-size) * 24)" | ||
| }, | ||
| shadow: { | ||
| xs: (_ = "rgb(0 0 0 / 0.1)") => `0 1px 2px 0 rgb(0 0 0 / 0.05)`, | ||
| sm: (color = "rgb(0 0 0 / 0.1)") => `0 1px 3px 0 ${color}, 0 1px 2px -1px ${color}`, | ||
| md: (color = "rgb(0 0 0 / 0.1)") => `0 4px 6px -1px ${color}, 0 2px 4px -2px ${color}`, | ||
| lg: (color = "rgb(0 0 0 / 0.1)") => `0 10px 15px -3px ${color}, 0 4px 6px -4px ${color}`, | ||
| xl: (color = "rgb(0 0 0 / 0.1)") => `0 20px 25px -5px ${color}, 0 8px 10px -6px ${color}`, | ||
| "2xl": (color = "rgb(0 0 0 / 0.25)") => `0 25px 50px -12px ${color}`, | ||
| inner: (color = "rgb(0 0 0 / 0.05)") => `inset 0 2px 4px 0 ${color}`, | ||
| none: () => `none` | ||
| }, | ||
| zIndices: { | ||
| hide: -1, | ||
| auto: "auto", | ||
| base: 0, | ||
| docked: 10, | ||
| dropdown: 1e3, | ||
| sticky: 1100, | ||
| banner: 1200, | ||
| overlay: 1300, | ||
| modal: 1400, | ||
| popover: 1500, | ||
| skipLink: 1600, | ||
| toast: 1700, | ||
| tooltip: 1800 | ||
| } | ||
| }; | ||
| //#endregion | ||
| //#region src/styles/use-styles.ts | ||
| const stylesFactory = (theme) => { | ||
| const { colors, font, size, alpha, border } = tokens; | ||
| const { fontFamily, size: fontSize } = font; | ||
| const css = goober.css; | ||
| const t = (light, dark) => theme === "light" ? light : dark; | ||
| return { | ||
| /** | ||
| * Fills the devtools-utils portal div (height: 100%) and is a flex parent so MainPanel | ||
| * gets a real flex height budget (not just height: 100% without a flex chain). | ||
| */ | ||
| shellRoot: css` | ||
| display: flex; | ||
| flex-direction: column; | ||
| box-sizing: border-box; | ||
| width: 100%; | ||
| height: 100%; | ||
| min-height: 0; | ||
| overflow: hidden; | ||
| `, | ||
| /** Overrides devtools-ui MainPanel overflow-y: auto so inner columns own scrolling */ | ||
| mainPanelShell: css` | ||
| display: flex; | ||
| flex-direction: column; | ||
| flex: 1 1 0%; | ||
| min-height: 0; | ||
| overflow: hidden !important; | ||
| `, | ||
| mainContainer: css` | ||
| display: flex; | ||
| flex: 1 1 0%; | ||
| min-height: 0; | ||
| overflow: hidden; | ||
| padding: ${size[2]}; | ||
| padding-top: 0; | ||
| margin-top: ${size[2]}; | ||
| `, | ||
| dragHandle: css` | ||
| width: 8px; | ||
| background: ${t(colors.gray[300], colors.darkGray[600])}; | ||
| cursor: col-resize; | ||
| position: relative; | ||
| transition: all 0.2s ease; | ||
| user-select: none; | ||
| pointer-events: all; | ||
| margin: 0 ${size[1]}; | ||
| border-radius: 2px; | ||
| &:hover { | ||
| background: ${t(colors.blue[600], colors.blue[500])}; | ||
| margin: 0 ${size[1]}; | ||
| } | ||
| &.dragging { | ||
| background: ${t(colors.blue[700], colors.blue[600])}; | ||
| margin: 0 ${size[1]}; | ||
| } | ||
| &::after { | ||
| content: ''; | ||
| position: absolute; | ||
| top: 50%; | ||
| left: 50%; | ||
| transform: translate(-50%, -50%); | ||
| width: 2px; | ||
| height: 20px; | ||
| background: ${t(colors.gray[400], colors.darkGray[400])}; | ||
| border-radius: 1px; | ||
| pointer-events: none; | ||
| } | ||
| &:hover::after, | ||
| &.dragging::after { | ||
| background: ${t(colors.blue[500], colors.blue[300])}; | ||
| } | ||
| `, | ||
| leftPanel: css` | ||
| background: ${t(colors.gray[100], colors.darkGray[800])}; | ||
| border-radius: ${border.radius.lg}; | ||
| border: 1px solid ${t(colors.gray[200], colors.darkGray[700])}; | ||
| display: flex; | ||
| flex-direction: column; | ||
| overflow: hidden; | ||
| min-height: 0; | ||
| flex-shrink: 0; | ||
| `, | ||
| rightPanel: css` | ||
| background: ${t(colors.gray[100], colors.darkGray[800])}; | ||
| border-radius: ${border.radius.lg}; | ||
| border: 1px solid ${t(colors.gray[200], colors.darkGray[700])}; | ||
| display: flex; | ||
| flex-direction: column; | ||
| overflow: hidden; | ||
| min-height: 0; | ||
| flex: 1; | ||
| `, | ||
| panelHeader: css` | ||
| font-size: ${fontSize.md}; | ||
| font-weight: ${font.weight.bold}; | ||
| color: ${t(colors.blue[700], colors.blue[400])}; | ||
| padding: ${size[2]}; | ||
| border-bottom: 1px solid ${t(colors.gray[200], colors.darkGray[700])}; | ||
| background: ${t(colors.gray[100], colors.darkGray[800])}; | ||
| flex-shrink: 0; | ||
| `, | ||
| utilList: css` | ||
| flex: 1; | ||
| overflow-y: auto; | ||
| padding: ${size[1]}; | ||
| min-height: 0; | ||
| `, | ||
| utilGroup: css` | ||
| margin-bottom: ${size[2]}; | ||
| `, | ||
| utilGroupHeader: css` | ||
| font-size: ${fontSize.xs}; | ||
| font-weight: ${font.weight.semibold}; | ||
| color: ${t(colors.gray[600], colors.gray[400])}; | ||
| text-transform: uppercase; | ||
| letter-spacing: 0.05em; | ||
| margin-bottom: ${size[1]}; | ||
| padding: ${size[1]} ${size[2]}; | ||
| background: ${t(colors.gray[200], colors.darkGray[700])}; | ||
| border-radius: ${border.radius.md}; | ||
| `, | ||
| utilRow: css` | ||
| display: flex; | ||
| justify-content: space-between; | ||
| align-items: center; | ||
| padding: ${size[2]}; | ||
| margin-bottom: ${size[1]}; | ||
| background: ${t(colors.gray[200], colors.darkGray[700])}; | ||
| border-radius: ${border.radius.md}; | ||
| cursor: pointer; | ||
| transition: all 0.2s ease; | ||
| border: 1px solid transparent; | ||
| &:hover { | ||
| background: ${t(colors.gray[300], colors.darkGray[600])}; | ||
| border-color: ${t(colors.gray[400], colors.darkGray[500])}; | ||
| } | ||
| `, | ||
| utilRowSelected: css` | ||
| background: ${t(colors.blue[100], colors.blue[900] + alpha[20])}; | ||
| border-color: ${t(colors.blue[600], colors.blue[500])}; | ||
| box-shadow: 0 0 0 1px | ||
| ${t(colors.blue[600] + alpha[30], colors.blue[500] + alpha[30])}; | ||
| `, | ||
| utilKey: css` | ||
| font-family: ${fontFamily.mono}; | ||
| font-size: ${fontSize.xs}; | ||
| color: ${t(colors.gray[900], colors.gray[100])}; | ||
| flex: 1; | ||
| overflow: hidden; | ||
| text-overflow: ellipsis; | ||
| white-space: nowrap; | ||
| `, | ||
| utilStatus: css` | ||
| font-size: ${fontSize.xs}; | ||
| color: ${t(colors.gray[600], colors.gray[400])}; | ||
| text-transform: uppercase; | ||
| letter-spacing: 0.05em; | ||
| padding: ${size[1]} ${size[1]}; | ||
| background: ${t(colors.gray[300], colors.darkGray[600])}; | ||
| border-radius: ${border.radius.sm}; | ||
| margin-left: ${size[1]}; | ||
| `, | ||
| stateDetails: css` | ||
| flex: 1; | ||
| overflow-y: auto; | ||
| padding: ${size[2]}; | ||
| min-height: 0; | ||
| `, | ||
| stateHeader: css` | ||
| margin-bottom: ${size[2]}; | ||
| padding-bottom: ${size[2]}; | ||
| border-bottom: 1px solid ${t(colors.gray[200], colors.darkGray[700])}; | ||
| `, | ||
| stateTitle: css` | ||
| font-size: ${fontSize.md}; | ||
| font-weight: ${font.weight.bold}; | ||
| color: ${t(colors.blue[700], colors.blue[400])}; | ||
| margin-bottom: ${size[1]}; | ||
| `, | ||
| stateKey: css` | ||
| font-family: ${fontFamily.mono}; | ||
| font-size: ${fontSize.xs}; | ||
| color: ${t(colors.gray[600], colors.gray[400])}; | ||
| word-break: break-all; | ||
| `, | ||
| stateContent: css` | ||
| background: ${t(colors.gray[100], colors.darkGray[700])}; | ||
| border-radius: ${border.radius.md}; | ||
| padding: ${size[2]}; | ||
| border: 1px solid ${t(colors.gray[300], colors.darkGray[600])}; | ||
| `, | ||
| detailsGrid: css` | ||
| display: grid; | ||
| grid-template-columns: 1fr; | ||
| gap: ${size[2]}; | ||
| align-items: start; | ||
| `, | ||
| detailSection: css` | ||
| background: ${t(colors.white, colors.darkGray[700])}; | ||
| border: 1px solid ${t(colors.gray[300], colors.darkGray[600])}; | ||
| border-radius: ${border.radius.md}; | ||
| padding: ${size[2]}; | ||
| `, | ||
| detailSectionHeader: css` | ||
| font-size: ${fontSize.sm}; | ||
| font-weight: ${font.weight.bold}; | ||
| color: ${t(colors.gray[800], colors.gray[200])}; | ||
| margin-bottom: ${size[1]}; | ||
| text-transform: uppercase; | ||
| letter-spacing: 0.04em; | ||
| `, | ||
| actionsRow: css` | ||
| display: flex; | ||
| flex-wrap: wrap; | ||
| gap: ${size[2]}; | ||
| `, | ||
| actionButton: css` | ||
| display: inline-flex; | ||
| align-items: center; | ||
| gap: ${size[1]}; | ||
| padding: ${size[1]} ${size[2]}; | ||
| border-radius: ${border.radius.md}; | ||
| border: 1px solid ${t(colors.gray[300], colors.darkGray[500])}; | ||
| background: ${t(colors.gray[200], colors.darkGray[600])}; | ||
| color: ${t(colors.gray[900], colors.gray[100])}; | ||
| font-size: ${fontSize.xs}; | ||
| cursor: pointer; | ||
| user-select: none; | ||
| transition: | ||
| background 0.15s, | ||
| border-color 0.15s; | ||
| &:hover { | ||
| background: ${t(colors.gray[300], colors.darkGray[500])}; | ||
| border-color: ${t(colors.gray[400], colors.darkGray[400])}; | ||
| } | ||
| &:disabled { | ||
| opacity: 0.5; | ||
| cursor: not-allowed; | ||
| &:hover { | ||
| background: ${t(colors.gray[200], colors.darkGray[600])}; | ||
| border-color: ${t(colors.gray[300], colors.darkGray[500])}; | ||
| } | ||
| } | ||
| `, | ||
| actionDotBlue: css` | ||
| width: 6px; | ||
| height: 6px; | ||
| border-radius: 9999px; | ||
| background: ${colors.blue[400]}; | ||
| `, | ||
| actionDotGreen: css` | ||
| width: 6px; | ||
| height: 6px; | ||
| border-radius: 9999px; | ||
| background: ${colors.green[400]}; | ||
| `, | ||
| actionDotRed: css` | ||
| width: 6px; | ||
| height: 6px; | ||
| border-radius: 9999px; | ||
| background: ${colors.red[400]}; | ||
| `, | ||
| actionDotYellow: css` | ||
| width: 6px; | ||
| height: 6px; | ||
| border-radius: 9999px; | ||
| background: ${colors.yellow[400]}; | ||
| `, | ||
| actionDotOrange: css` | ||
| width: 6px; | ||
| height: 6px; | ||
| border-radius: 9999px; | ||
| background: ${colors.pink[400]}; | ||
| `, | ||
| actionDotPurple: css` | ||
| width: 6px; | ||
| height: 6px; | ||
| border-radius: 9999px; | ||
| background: ${colors.purple[400]}; | ||
| `, | ||
| infoGrid: css` | ||
| display: grid; | ||
| grid-template-columns: auto 1fr; | ||
| gap: ${size[1]}; | ||
| row-gap: ${size[1]}; | ||
| align-items: center; | ||
| `, | ||
| infoLabel: css` | ||
| color: ${t(colors.gray[600], colors.gray[400])}; | ||
| font-size: ${fontSize.xs}; | ||
| text-transform: uppercase; | ||
| letter-spacing: 0.05em; | ||
| `, | ||
| infoValueMono: css` | ||
| font-family: ${fontFamily.mono}; | ||
| font-size: ${fontSize.xs}; | ||
| color: ${t(colors.gray[900], colors.gray[100])}; | ||
| word-break: break-all; | ||
| `, | ||
| noSelection: css` | ||
| flex: 1; | ||
| display: flex; | ||
| align-items: center; | ||
| justify-content: center; | ||
| color: ${t(colors.gray[500], colors.gray[500])}; | ||
| font-style: italic; | ||
| text-align: center; | ||
| padding: ${size[4]}; | ||
| `, | ||
| sectionContainer: css` | ||
| display: flex; | ||
| flex-wrap: wrap; | ||
| gap: ${size[4]}; | ||
| `, | ||
| section: css` | ||
| background: ${t(colors.gray[100], colors.darkGray[800])}; | ||
| border-radius: ${border.radius.lg}; | ||
| box-shadow: ${tokens.shadow.md(t(colors.gray[400] + alpha[80], colors.black + alpha[80]))}; | ||
| padding: ${size[4]}; | ||
| margin-bottom: ${size[4]}; | ||
| border: 1px solid ${t(colors.gray[200], colors.darkGray[700])}; | ||
| min-width: 0; | ||
| max-width: 33%; | ||
| max-height: fit-content; | ||
| `, | ||
| sectionHeader: css` | ||
| font-size: ${fontSize.lg}; | ||
| font-weight: ${font.weight.bold}; | ||
| margin-bottom: ${size[2]}; | ||
| color: ${t(colors.blue[600], colors.blue[400])}; | ||
| letter-spacing: 0.01em; | ||
| display: flex; | ||
| align-items: center; | ||
| gap: ${size[2]}; | ||
| `, | ||
| sectionEmpty: css` | ||
| color: ${t(colors.gray[500], colors.gray[500])}; | ||
| font-size: ${fontSize.sm}; | ||
| font-style: italic; | ||
| margin: ${size[2]} 0; | ||
| `, | ||
| instanceList: css` | ||
| display: flex; | ||
| flex-direction: column; | ||
| gap: ${size[2]}; | ||
| background: ${t(colors.gray[200], colors.darkGray[700])}; | ||
| border: 1px solid ${t(colors.gray[300], colors.darkGray[600])}; | ||
| `, | ||
| instanceCard: css` | ||
| background: ${t(colors.gray[200], colors.darkGray[700])}; | ||
| border-radius: ${border.radius.md}; | ||
| padding: ${size[3]}; | ||
| border: 1px solid ${t(colors.gray[300], colors.darkGray[600])}; | ||
| font-size: ${fontSize.sm}; | ||
| color: ${t(colors.gray[900], colors.gray[100])}; | ||
| font-family: ${fontFamily.mono}; | ||
| overflow-x: auto; | ||
| transition: | ||
| box-shadow 0.3s, | ||
| background 0.3s; | ||
| ` | ||
| }; | ||
| }; | ||
| function useStyles() { | ||
| const { theme } = useTheme(); | ||
| const [styles, setStyles] = createSignal(stylesFactory(theme())); | ||
| createEffect(() => { | ||
| setStyles(stylesFactory(theme())); | ||
| }); | ||
| return styles; | ||
| } | ||
| //#endregion | ||
| //#region src/components/util-groups.ts | ||
| const UTIL_GROUPS = [ | ||
| { | ||
| key: "debouncers", | ||
| label: "Debouncers", | ||
| displayName: "Debouncer" | ||
| }, | ||
| { | ||
| key: "throttlers", | ||
| label: "Throttlers", | ||
| displayName: "Throttler" | ||
| }, | ||
| { | ||
| key: "batchers", | ||
| label: "Batchers", | ||
| displayName: "Batcher" | ||
| }, | ||
| { | ||
| key: "queuers", | ||
| label: "Queuers", | ||
| displayName: "Queuer" | ||
| }, | ||
| { | ||
| key: "rateLimiters", | ||
| label: "Rate Limiters", | ||
| displayName: "Rate Limiter" | ||
| }, | ||
| { | ||
| key: "asyncDebouncers", | ||
| label: "Async Debouncers", | ||
| displayName: "Async Debouncer" | ||
| }, | ||
| { | ||
| key: "asyncThrottlers", | ||
| label: "Async Throttlers", | ||
| displayName: "Async Throttler" | ||
| }, | ||
| { | ||
| key: "asyncBatchers", | ||
| label: "Async Batchers", | ||
| displayName: "Async Batcher" | ||
| }, | ||
| { | ||
| key: "asyncQueuers", | ||
| label: "Async Queuers", | ||
| displayName: "Async Queuer" | ||
| }, | ||
| { | ||
| key: "asyncRateLimiters", | ||
| label: "Async Rate Limiters", | ||
| displayName: "Async Rate Limiter" | ||
| } | ||
| ]; | ||
| //#endregion | ||
| //#region src/components/UtilList.tsx | ||
| var _tmpl$$4 = [ | ||
| "<div", | ||
| ">", | ||
| "</div>" | ||
| ], _tmpl$2$3 = [ | ||
| "<div", | ||
| "><div", | ||
| ">", | ||
| "</div>", | ||
| "</div>" | ||
| ], _tmpl$3$2 = [ | ||
| "<div", | ||
| "><div", | ||
| ">", | ||
| "</div><div", | ||
| ">", | ||
| "</div></div>" | ||
| ]; | ||
| function UtilList(props) { | ||
| const styles = useStyles(); | ||
| return ssr(_tmpl$$4, ssrAttribute("class", escape(styles().utilList, true), false), escape(createComponent(For, { | ||
| each: UTIL_GROUPS, | ||
| children: (group) => props.utilState()[group.key].length > 0 && ssr(_tmpl$2$3, ssrAttribute("class", escape(styles().utilGroup, true), false), ssrAttribute("class", escape(styles().utilGroupHeader, true), false), escape(group.label), escape(createComponent(For, { | ||
| get each() { | ||
| return props.utilState()[group.key]; | ||
| }, | ||
| children: (instance) => { | ||
| const status = (() => { | ||
| try { | ||
| const statusAccessor = useSelector(instance.store, (s) => s.status); | ||
| return () => statusAccessor() ?? "unknown"; | ||
| } catch { | ||
| return () => "unknown"; | ||
| } | ||
| })(); | ||
| return ssr(_tmpl$3$2, ssrAttribute("class", escape(clsx(styles().utilRow, props.selectedKey() === instance.key && styles().utilRowSelected), true), false), ssrAttribute("class", escape(styles().utilKey, true), false), escape(instance.key), ssrAttribute("class", escape(styles().utilStatus, true), false), escape(status())); | ||
| } | ||
| }))) | ||
| }))); | ||
| } | ||
| //#endregion | ||
| //#region src/utils/read-pacer-store-state.ts | ||
| /** TanStack Store (`@tanstack/store`) used by pacer utils. */ | ||
| function isPacerUtilTanStackStore(x) { | ||
| return typeof x === "object" && x !== null && "subscribe" in x && typeof x.subscribe === "function" && "get" in x && typeof x.get === "function"; | ||
| } | ||
| /** | ||
| * Core pacer utils expose TanStack `Store` (`get()` / `state`). React hooks may | ||
| * also expose a reactive `state` field. Use this for devtools UI that needs a | ||
| * plain snapshot (e.g. JsonTree). | ||
| */ | ||
| function getPacerUtilStoreState(instance) { | ||
| const store = instance.store; | ||
| if (store && typeof store.get === "function") return store.get(); | ||
| if (store && store.state !== void 0) return store.state; | ||
| return instance.state; | ||
| } | ||
| //#endregion | ||
| //#region src/components/ActionButtons.tsx | ||
| var _tmpl$$3 = ["<div", ">No actions available for this util</div>"], _tmpl$2$2 = [ | ||
| "<div", | ||
| ">", | ||
| "", | ||
| "", | ||
| "", | ||
| "", | ||
| "", | ||
| "</div>" | ||
| ], _tmpl$3$1 = [ | ||
| "<button", | ||
| "><span", | ||
| "></span>", | ||
| "</button>" | ||
| ], _tmpl$4 = [ | ||
| "<button", | ||
| "", | ||
| "><span", | ||
| "></span>Flush</button>" | ||
| ], _tmpl$5 = [ | ||
| "<button", | ||
| "", | ||
| "><span", | ||
| "></span>Cancel</button>" | ||
| ], _tmpl$6 = [ | ||
| "<button", | ||
| "><span", | ||
| "></span>Reset</button>" | ||
| ], _tmpl$7 = [ | ||
| "<button", | ||
| "", | ||
| "><span", | ||
| "></span>Clear</button>" | ||
| ]; | ||
| function ActionButtons(props) { | ||
| const styles = useStyles(); | ||
| const utilInstance = props.instance; | ||
| const store = utilInstance?.store; | ||
| const stateAccessor = isPacerUtilTanStackStore(store) ? useSelector(store, (s) => s !== null && s !== void 0 && typeof s === "object" ? s : {}, { compare: shallow }) : () => getPacerUtilStoreState(utilInstance); | ||
| const getState = () => stateAccessor(); | ||
| const hasPending = () => { | ||
| return "isPending" in getState(); | ||
| }; | ||
| const isPending = () => { | ||
| const u = getState(); | ||
| return "isPending" in u ? !!u.isPending : false; | ||
| }; | ||
| const isEmptyFlag = () => { | ||
| const u = getState(); | ||
| return "isEmpty" in u ? !!u.isEmpty : false; | ||
| }; | ||
| const hasFlush = typeof utilInstance.flush === "function"; | ||
| const hasCancel = typeof utilInstance.cancel === "function"; | ||
| const hasReset = typeof utilInstance.reset === "function"; | ||
| const hasClear = typeof utilInstance.clear === "function"; | ||
| const hasStart = typeof utilInstance.start === "function"; | ||
| const hasStop = typeof utilInstance.stop === "function"; | ||
| const hasStartStop = hasStart && hasStop; | ||
| const isRunning = () => { | ||
| const u = getState(); | ||
| return "isRunning" in u ? !!u.isRunning : true; | ||
| }; | ||
| if (!hasPending() && !hasFlush && !hasCancel && !hasReset && !hasClear) return ssr(_tmpl$$3, ssrAttribute("class", escape(styles().sectionEmpty, true), false)); | ||
| `${props.utilName}`; | ||
| const utilNameLower = props.utilName.toLowerCase(); | ||
| return ssr(_tmpl$2$2, ssrAttribute("class", escape(styles().actionsRow, true), false), hasPending() && ssr(_tmpl$3$1, ssrAttribute("class", escape(styles().actionButton, true), false), ssrAttribute("class", escape(styles().actionDotBlue, true), false), isPending() ? "Restore Pending" : "Trigger Pending"), hasFlush && ssr(_tmpl$4, ssrAttribute("class", escape(styles().actionButton, true), false), ssrAttribute("disabled", utilNameLower.includes("debouncer") && !isPending() || utilNameLower.includes("throttler") && !isPending() || utilNameLower.includes("batcher") && isEmptyFlag() || utilNameLower.includes("queuer") && isEmptyFlag(), true), ssrAttribute("class", escape(styles().actionDotGreen, true), false)), hasCancel && ssr(_tmpl$5, ssrAttribute("class", escape(styles().actionButton, true), false), ssrAttribute("disabled", utilNameLower.includes("debouncer") && !isPending() || utilNameLower.includes("throttler") && !isPending() || utilNameLower.includes("batcher") && isEmptyFlag() || utilNameLower.includes("queuer") && isEmptyFlag(), true), ssrAttribute("class", escape(styles().actionDotRed, true), false)), hasReset && ssr(_tmpl$6, ssrAttribute("class", escape(styles().actionButton, true), false), ssrAttribute("class", escape(styles().actionDotYellow, true), false)), hasClear && ssr(_tmpl$7, ssrAttribute("class", escape(styles().actionButton, true), false), ssrAttribute("disabled", utilNameLower.includes("batcher") && isEmptyFlag() || utilNameLower.includes("queuer") && isEmptyFlag(), true), ssrAttribute("class", escape(styles().actionDotOrange, true), false)), hasStartStop && ssr(_tmpl$3$1, ssrAttribute("class", escape(styles().actionButton, true), false), ssrAttribute("class", escape(styles().actionDotPurple, true), false), isRunning() ? "Stop" : "Start")); | ||
| } | ||
| //#endregion | ||
| //#region src/components/StateHeader.tsx | ||
| var _tmpl$$2 = [ | ||
| "<div", | ||
| "><div", | ||
| ">", | ||
| "</div><div style=\"", | ||
| "\"><div", | ||
| "><div", | ||
| ">Key</div><div", | ||
| ">", | ||
| "</div><div", | ||
| ">Last Updated</div><div", | ||
| ">", | ||
| " (", | ||
| ")</div></div><div", | ||
| " style=\"", | ||
| "\">", | ||
| "% reduction</div></div></div>" | ||
| ]; | ||
| dayjs.extend(relativeTime); | ||
| function reductionFromState(entry, state) { | ||
| if (!state) return 0; | ||
| const completedExecutions = entry.type.toLowerCase().includes("async") ? Number(state.settleCount) || 0 : Number(state.executionCount) || 0; | ||
| if (entry.type.toLowerCase().includes("batcher")) { | ||
| const totalItemsProcessed = Number(state.totalItemsProcessed) || 0; | ||
| if (totalItemsProcessed === 0) return 0; | ||
| return Math.round((totalItemsProcessed - completedExecutions) / totalItemsProcessed * 100); | ||
| } | ||
| let requestCount = 0; | ||
| if (state.maybeExecuteCount !== void 0) requestCount = Number(state.maybeExecuteCount) || 0; | ||
| else if (state.addItemCount !== void 0) requestCount = Number(state.addItemCount) || 0; | ||
| else return 0; | ||
| if (requestCount === 0) return 0; | ||
| const reduction = requestCount - completedExecutions; | ||
| return Math.round(reduction / requestCount * 100); | ||
| } | ||
| function StateHeaderInner(props) { | ||
| const key = props.entry.instance.key; | ||
| const updatedAt = () => props.utilState().lastUpdatedByKey[key] ?? Date.now(); | ||
| const getRelativeTime = () => { | ||
| const at = updatedAt(); | ||
| const diffMs = props.now() - at; | ||
| const diffSeconds = Math.floor(diffMs / 1e3); | ||
| if (diffSeconds < 60) return `${diffSeconds} second${diffSeconds !== 1 ? "s" : ""} ago`; | ||
| return dayjs(at).fromNow(); | ||
| }; | ||
| const store = props.entry.instance?.store; | ||
| const stateAccessor = isPacerUtilTanStackStore(store) ? useSelector(store, (s) => s !== null && s !== void 0 && typeof s === "object" ? s : {}, { compare: shallow }) : () => getPacerUtilStoreState(props.entry.instance); | ||
| const reductionPercentage = () => reductionFromState(props.entry, stateAccessor()); | ||
| return ssr(_tmpl$$2, ssrAttribute("class", escape(props.styles.stateHeader, true), false), ssrAttribute("class", escape(props.styles.stateTitle, true), false), escape(props.entry.type), ssrStyleProperty("display:", "flex") + ssrStyleProperty(";align-items:", "center") + ssrStyleProperty(";gap:", "16px"), ssrAttribute("class", escape(props.styles.infoGrid, true), false), ssrAttribute("class", escape(props.styles.infoLabel, true), false), ssrAttribute("class", escape(props.styles.infoValueMono, true), false), escape(key), ssrAttribute("class", escape(props.styles.infoLabel, true), false), ssrAttribute("class", escape(props.styles.infoValueMono, true), false), escape(new Date(updatedAt()).toLocaleTimeString()), escape(getRelativeTime()), ssrAttribute("class", escape(props.styles.infoValueMono, true), false), ssrStyleProperty("margin-left:", "auto") + ssrStyleProperty(";font-weight:", "bold"), escape(reductionPercentage())); | ||
| } | ||
| function StateHeader(props) { | ||
| const styles = useStyles(); | ||
| const [now, setNow] = createSignal(Date.now()); | ||
| onMount(() => { | ||
| const interval = setInterval(() => { | ||
| setNow(Date.now()); | ||
| }, 1e3); | ||
| onCleanup(() => { | ||
| clearInterval(interval); | ||
| }); | ||
| }); | ||
| return createComponent(Show, { | ||
| get when() { | ||
| return props.selectedInstance(); | ||
| }, | ||
| children: (entry) => createComponent(StateHeaderInner, { | ||
| get entry() { | ||
| return entry(); | ||
| }, | ||
| get styles() { | ||
| return styles(); | ||
| }, | ||
| now, | ||
| get utilState() { | ||
| return props.utilState; | ||
| } | ||
| }) | ||
| }); | ||
| } | ||
| //#endregion | ||
| //#region src/components/UtilStateJsonTree.tsx | ||
| /** | ||
| * Subscribes to the util's TanStack Store so the tree updates live (same | ||
| * idea as {@link UtilList} status). A plain snapshot would only refresh when the | ||
| * devtools shell re-renders, which does not happen on nested store updates. | ||
| */ | ||
| function UtilStateJsonTree(props) { | ||
| const store = props.instance?.store; | ||
| if (isPacerUtilTanStackStore(store)) { | ||
| const snapshot = useSelector(store, (s) => s); | ||
| return createComponent(JsonTree, { get value() { | ||
| return snapshot(); | ||
| } }); | ||
| } | ||
| return createComponent(JsonTree, { get value() { | ||
| return getPacerUtilStoreState(props.instance); | ||
| } }); | ||
| } | ||
| //#endregion | ||
| //#region src/components/DetailsPanel.tsx | ||
| var _tmpl$$1 = [ | ||
| "<div", | ||
| ">", | ||
| "</div>" | ||
| ], _tmpl$2$1 = ["<div", ">Select a util from the left panel to view its state</div>"], _tmpl$3 = [ | ||
| "<div", | ||
| "><div", | ||
| "><div", | ||
| ">Actions</div>", | ||
| "</div><div", | ||
| "><div", | ||
| ">State</div><div", | ||
| ">", | ||
| "</div></div><div", | ||
| "><div", | ||
| ">Options</div><div", | ||
| ">", | ||
| "</div></div></div>" | ||
| ]; | ||
| function DetailsPanel(props) { | ||
| const styles = useStyles(); | ||
| return ssr(_tmpl$$1, ssrAttribute("class", escape(styles().stateDetails, true), false), (() => { | ||
| const entry = props.selectedInstance(); | ||
| if (!entry) return ssr(_tmpl$2$1, ssrAttribute("class", escape(styles().noSelection, true), false)); | ||
| return escape([createComponent(StateHeader, { | ||
| get selectedInstance() { | ||
| return props.selectedInstance; | ||
| }, | ||
| get utilState() { | ||
| return props.utilState; | ||
| } | ||
| }), ssr(_tmpl$3, ssrAttribute("class", escape(styles().detailsGrid, true), false), ssrAttribute("class", escape(styles().detailSection, true), false), ssrAttribute("class", escape(styles().detailSectionHeader, true), false), escape(createComponent(ActionButtons, { | ||
| get instance() { | ||
| return entry.instance; | ||
| }, | ||
| get utilName() { | ||
| return entry.type; | ||
| } | ||
| })), ssrAttribute("class", escape(styles().detailSection, true), false), ssrAttribute("class", escape(styles().detailSectionHeader, true), false), ssrAttribute("class", escape(styles().stateContent, true), false), escape(createComponent(UtilStateJsonTree, { get instance() { | ||
| return entry.instance; | ||
| } })), ssrAttribute("class", escape(styles().detailSection, true), false), ssrAttribute("class", escape(styles().detailSectionHeader, true), false), ssrAttribute("class", escape(styles().stateContent, true), false), escape(createComponent(JsonTree, { get value() { | ||
| return entry.instance.options; | ||
| } })))]); | ||
| })()); | ||
| } | ||
| //#endregion | ||
| //#region src/components/Shell.tsx | ||
| var _tmpl$ = [ | ||
| "<div", | ||
| "><div", | ||
| " style=\"", | ||
| "\">", | ||
| "</div><div class=\"", | ||
| "\"></div><div", | ||
| " style=\"", | ||
| "\"><div", | ||
| ">Details</div>", | ||
| "</div></div>" | ||
| ], _tmpl$2 = [ | ||
| "<div", | ||
| " style=\"", | ||
| "\">", | ||
| "</div>" | ||
| ]; | ||
| function readResizeObserverBlockSize(entry) { | ||
| const [box] = entry.borderBoxSize; | ||
| if (box !== void 0) return box.blockSize; | ||
| return entry.contentRect.height; | ||
| } | ||
| /** Outer plugin slot from TanStack Devtools (not the inner React mount div, which can report a tiny height). */ | ||
| function resolvePluginHost(el) { | ||
| const byId = el.closest("[id^=\"plugin-container-\"]"); | ||
| if (byId) return byId; | ||
| return el.parentElement?.parentElement?.parentElement ?? el.parentElement?.parentElement ?? el.parentElement; | ||
| } | ||
| function Shell() { | ||
| const styles = useStyles(); | ||
| const state = usePacerDevtoolsState(); | ||
| const utilState = () => state; | ||
| const [selectedKey, setSelectedKey] = createSignal(null); | ||
| const [leftPanelWidth, setLeftPanelWidth] = createSignal(300); | ||
| const [isDragging, setIsDragging] = createSignal(false); | ||
| const [shellRootEl, setShellRootEl] = createSignal(null); | ||
| const [slotHeightPx, setSlotHeightPx] = createSignal(void 0); | ||
| const selectedInstance = createMemo(() => { | ||
| const key = selectedKey(); | ||
| if (!key) return null; | ||
| for (const group of UTIL_GROUPS) { | ||
| const instance = utilState()[group.key].find((inst) => inst.key === key); | ||
| if (instance) return { | ||
| instance, | ||
| type: group.displayName | ||
| }; | ||
| } | ||
| return null; | ||
| }); | ||
| let dragStartX = 0; | ||
| let dragStartWidth = 0; | ||
| const handleMouseMove = (e) => { | ||
| if (!isDragging()) return; | ||
| e.preventDefault(); | ||
| const deltaX = e.clientX - dragStartX; | ||
| setLeftPanelWidth(Math.max(150, Math.min(800, dragStartWidth + deltaX))); | ||
| }; | ||
| const handleMouseUp = () => { | ||
| setIsDragging(false); | ||
| document.body.style.cursor = ""; | ||
| document.body.style.userSelect = ""; | ||
| }; | ||
| onMount(() => { | ||
| document.addEventListener("mousemove", handleMouseMove); | ||
| document.addEventListener("mouseup", handleMouseUp); | ||
| }); | ||
| onCleanup(() => { | ||
| document.removeEventListener("mousemove", handleMouseMove); | ||
| document.removeEventListener("mouseup", handleMouseUp); | ||
| }); | ||
| /** | ||
| * TanStack Devtools plugin slots use height: 100% without min-height: 0 on flex | ||
| * ancestors, so percentage height often never constrains our tree. Observing the | ||
| * outer `plugin-container-*` host (not the immediate React mount wrapper) and | ||
| * setting an explicit pixel height makes inner overflow-y regions scroll without | ||
| * capping to the mount div’s collapsed height. | ||
| */ | ||
| createEffect(() => { | ||
| const el = shellRootEl(); | ||
| if (!el || typeof ResizeObserver === "undefined") return; | ||
| const pluginSlot = resolvePluginHost(el); | ||
| if (!pluginSlot) return; | ||
| const ro = new ResizeObserver((entries) => { | ||
| const entry = entries[0]; | ||
| if (!entry) return; | ||
| const target = entry.target; | ||
| const h = target.clientHeight > 0 ? target.clientHeight : readResizeObserverBlockSize(entry); | ||
| if (h > 0) setSlotHeightPx(Math.round(h)); | ||
| }); | ||
| ro.observe(pluginSlot); | ||
| onCleanup(() => ro.disconnect()); | ||
| }); | ||
| return ssr(_tmpl$2, ssrAttribute("class", escape(styles().shellRoot, true), false), ssrStyle({ ...slotHeightPx() !== void 0 ? { | ||
| height: `${slotHeightPx()}px`, | ||
| "max-height": `${slotHeightPx()}px` | ||
| } : {} }), escape(createComponent(MainPanel, { | ||
| get ["class"]() { | ||
| return styles().mainPanelShell; | ||
| }, | ||
| get children() { | ||
| return [createComponent(Header, { get children() { | ||
| return createComponent(HeaderLogo, { | ||
| flavor: { | ||
| light: "#84cc16", | ||
| dark: "#84cc16" | ||
| }, | ||
| children: "TanStack Pacer" | ||
| }); | ||
| } }), ssr(_tmpl$, ssrAttribute("class", escape(styles().mainContainer, true), false), ssrAttribute("class", escape(styles().leftPanel, true), false), ssrStyleProperty("width:", `${escape(leftPanelWidth(), true)}px`) + ssrStyleProperty(";min-width:", "150px") + ssrStyleProperty(";max-width:", "800px"), escape(createComponent(UtilList, { | ||
| selectedKey, | ||
| setSelectedKey, | ||
| utilState | ||
| })), `${escape(styles().dragHandle, true)} ${isDragging() ? "dragging" : ""}`, ssrAttribute("class", escape(styles().rightPanel, true), false), ssrStyleProperty("flex:", 1), ssrAttribute("class", escape(styles().panelHeader, true), false), escape(createComponent(DetailsPanel, { | ||
| selectedInstance, | ||
| utilState | ||
| })))]; | ||
| } | ||
| }))); | ||
| } | ||
| //#endregion | ||
| //#region src/components/index.tsx | ||
| function PacerDevtools(props) { | ||
| return createComponent(ThemeContextProvider, { | ||
| get theme() { | ||
| return props.theme; | ||
| }, | ||
| get children() { | ||
| return createComponent(PacerContextProvider, { get children() { | ||
| return createComponent(Shell, {}); | ||
| } }); | ||
| } | ||
| }); | ||
| } | ||
| //#endregion | ||
| export { PacerDevtools as default }; |
| import { className, createComponent, delegateEvents, effect, insert, memo, setStyleProperty, style, template, use } from "solid-js/web"; | ||
| import { Header, HeaderLogo, JsonTree, MainPanel, ThemeContextProvider, useTheme } from "@tanstack/devtools-ui"; | ||
| import { createStore, produce } from "solid-js/store"; | ||
| import { For, Show, createContext, createEffect, createMemo, createSignal, onCleanup, onMount, useContext } from "solid-js"; | ||
| import { getPacerDevtoolsInstance, pacerEventClient } from "@tanstack/pacer/event-client"; | ||
| import * as goober from "goober"; | ||
| import { shallow, useSelector } from "@tanstack/solid-store"; | ||
| import clsx from "clsx"; | ||
| import { emitChange } from "@tanstack/pacer"; | ||
| import dayjs from "dayjs"; | ||
| import relativeTime from "dayjs/plugin/relativeTime.js"; | ||
| //#region src/PacerContextProvider.tsx | ||
| const initialPacerDevtoolsStore = { | ||
| asyncBatchers: [], | ||
| asyncDebouncers: [], | ||
| asyncQueuers: [], | ||
| asyncRateLimiters: [], | ||
| asyncThrottlers: [], | ||
| batchers: [], | ||
| debouncers: [], | ||
| queuers: [], | ||
| rateLimiters: [], | ||
| throttlers: [], | ||
| lastUpdatedByKey: {} | ||
| }; | ||
| const PacerDevtoolsContext = createContext([initialPacerDevtoolsStore, () => {}]); | ||
| /** | ||
| * Match TanStack Form devtools: subscribe with {@link pacerEventClient.on} per | ||
| * event suffix so the same `pacer:${suffix}` channel the library emits on is | ||
| * observed. `onAllPluginEvents` only sees `tanstack-devtools-global`, which is | ||
| * not always relayed the same way by the shell. | ||
| * | ||
| * `d-*` suffixes are used when the devtools panel pushes state back (see | ||
| * ActionButtons); those must update the panel store too. | ||
| */ | ||
| const PACER_DEVTOOLS_UTIL_EVENTS = [ | ||
| { | ||
| listKey: "asyncBatchers", | ||
| suffixes: ["AsyncBatcher", "d-AsyncBatcher"] | ||
| }, | ||
| { | ||
| listKey: "asyncDebouncers", | ||
| suffixes: ["AsyncDebouncer", "d-AsyncDebouncer"] | ||
| }, | ||
| { | ||
| listKey: "asyncQueuers", | ||
| suffixes: ["AsyncQueuer", "d-AsyncQueuer"] | ||
| }, | ||
| { | ||
| listKey: "asyncRateLimiters", | ||
| suffixes: ["AsyncRateLimiter", "d-AsyncRateLimiter"] | ||
| }, | ||
| { | ||
| listKey: "asyncThrottlers", | ||
| suffixes: ["AsyncThrottler", "d-AsyncThrottler"] | ||
| }, | ||
| { | ||
| listKey: "batchers", | ||
| suffixes: ["Batcher", "d-Batcher"] | ||
| }, | ||
| { | ||
| listKey: "debouncers", | ||
| suffixes: ["Debouncer", "d-Debouncer"] | ||
| }, | ||
| { | ||
| listKey: "queuers", | ||
| suffixes: ["Queuer", "d-Queuer"] | ||
| }, | ||
| { | ||
| listKey: "rateLimiters", | ||
| suffixes: ["RateLimiter", "d-RateLimiter"] | ||
| }, | ||
| { | ||
| listKey: "throttlers", | ||
| suffixes: ["Throttler", "d-Throttler"] | ||
| } | ||
| ]; | ||
| function PacerContextProvider(props) { | ||
| const [store, setStore] = createStore(initialPacerDevtoolsStore); | ||
| createEffect(() => { | ||
| const cleanups = []; | ||
| for (const { listKey, suffixes } of PACER_DEVTOOLS_UTIL_EVENTS) for (const suffix of suffixes) cleanups.push(pacerEventClient.on(suffix, (e) => { | ||
| const key = e.payload.key; | ||
| if (!key) return; | ||
| const instance = getPacerDevtoolsInstance(key); | ||
| if (!instance || typeof instance !== "object") return; | ||
| setStore(produce((draft) => { | ||
| const list = draft[listKey]; | ||
| const index = list.findIndex((item) => item.key === key); | ||
| const inst = instance; | ||
| if (index !== -1) list[index] = inst; | ||
| else list.push(inst); | ||
| draft.lastUpdatedByKey[key] = Date.now(); | ||
| })); | ||
| })); | ||
| onCleanup(() => { | ||
| for (const cleanup of cleanups) cleanup(); | ||
| }); | ||
| }); | ||
| return createComponent(PacerDevtoolsContext.Provider, { | ||
| value: [store, setStore], | ||
| get children() { | ||
| return props.children; | ||
| } | ||
| }); | ||
| } | ||
| const usePacerDevtoolsContext = () => { | ||
| return useContext(PacerDevtoolsContext); | ||
| }; | ||
| const usePacerDevtoolsState = () => { | ||
| const [state] = usePacerDevtoolsContext(); | ||
| return state; | ||
| }; | ||
| //#endregion | ||
| //#region src/styles/tokens.ts | ||
| const tokens = { | ||
| colors: { | ||
| inherit: "inherit", | ||
| current: "currentColor", | ||
| transparent: "transparent", | ||
| black: "#000000", | ||
| white: "#ffffff", | ||
| neutral: { | ||
| 50: "#f9fafb", | ||
| 100: "#f2f4f7", | ||
| 200: "#eaecf0", | ||
| 300: "#d0d5dd", | ||
| 400: "#98a2b3", | ||
| 500: "#667085", | ||
| 600: "#475467", | ||
| 700: "#344054", | ||
| 800: "#1d2939", | ||
| 900: "#101828" | ||
| }, | ||
| darkGray: { | ||
| 50: "#525c7a", | ||
| 100: "#49536e", | ||
| 200: "#414962", | ||
| 300: "#394056", | ||
| 400: "#313749", | ||
| 500: "#292e3d", | ||
| 600: "#212530", | ||
| 700: "#191c24", | ||
| 800: "#111318", | ||
| 900: "#0b0d10" | ||
| }, | ||
| gray: { | ||
| 50: "#f9fafb", | ||
| 100: "#f2f4f7", | ||
| 200: "#eaecf0", | ||
| 300: "#d0d5dd", | ||
| 400: "#98a2b3", | ||
| 500: "#667085", | ||
| 600: "#475467", | ||
| 700: "#344054", | ||
| 800: "#1d2939", | ||
| 900: "#101828" | ||
| }, | ||
| blue: { | ||
| 25: "#F5FAFF", | ||
| 50: "#EFF8FF", | ||
| 100: "#D1E9FF", | ||
| 200: "#B2DDFF", | ||
| 300: "#84CAFF", | ||
| 400: "#53B1FD", | ||
| 500: "#2E90FA", | ||
| 600: "#1570EF", | ||
| 700: "#175CD3", | ||
| 800: "#1849A9", | ||
| 900: "#194185" | ||
| }, | ||
| green: { | ||
| 25: "#F6FEF9", | ||
| 50: "#ECFDF3", | ||
| 100: "#D1FADF", | ||
| 200: "#A6F4C5", | ||
| 300: "#6CE9A6", | ||
| 400: "#32D583", | ||
| 500: "#12B76A", | ||
| 600: "#039855", | ||
| 700: "#027A48", | ||
| 800: "#05603A", | ||
| 900: "#054F31" | ||
| }, | ||
| red: { | ||
| 50: "#fef2f2", | ||
| 100: "#fee2e2", | ||
| 200: "#fecaca", | ||
| 300: "#fca5a5", | ||
| 400: "#f87171", | ||
| 500: "#ef4444", | ||
| 600: "#dc2626", | ||
| 700: "#b91c1c", | ||
| 800: "#991b1b", | ||
| 900: "#7f1d1d", | ||
| 950: "#450a0a" | ||
| }, | ||
| yellow: { | ||
| 25: "#FFFCF5", | ||
| 50: "#FFFAEB", | ||
| 100: "#FEF0C7", | ||
| 200: "#FEDF89", | ||
| 300: "#FEC84B", | ||
| 400: "#FDB022", | ||
| 500: "#F79009", | ||
| 600: "#DC6803", | ||
| 700: "#B54708", | ||
| 800: "#93370D", | ||
| 900: "#7A2E0E" | ||
| }, | ||
| purple: { | ||
| 25: "#FAFAFF", | ||
| 50: "#F4F3FF", | ||
| 100: "#EBE9FE", | ||
| 200: "#D9D6FE", | ||
| 300: "#BDB4FE", | ||
| 400: "#9B8AFB", | ||
| 500: "#7A5AF8", | ||
| 600: "#6938EF", | ||
| 700: "#5925DC", | ||
| 800: "#4A1FB8", | ||
| 900: "#3E1C96" | ||
| }, | ||
| teal: { | ||
| 25: "#F6FEFC", | ||
| 50: "#F0FDF9", | ||
| 100: "#CCFBEF", | ||
| 200: "#99F6E0", | ||
| 300: "#5FE9D0", | ||
| 400: "#2ED3B7", | ||
| 500: "#15B79E", | ||
| 600: "#0E9384", | ||
| 700: "#107569", | ||
| 800: "#125D56", | ||
| 900: "#134E48" | ||
| }, | ||
| pink: { | ||
| 25: "#fdf2f8", | ||
| 50: "#fce7f3", | ||
| 100: "#fbcfe8", | ||
| 200: "#f9a8d4", | ||
| 300: "#f472b6", | ||
| 400: "#ec4899", | ||
| 500: "#db2777", | ||
| 600: "#be185d", | ||
| 700: "#9d174d", | ||
| 800: "#831843", | ||
| 900: "#500724" | ||
| }, | ||
| cyan: { | ||
| 25: "#ecfeff", | ||
| 50: "#cffafe", | ||
| 100: "#a5f3fc", | ||
| 200: "#67e8f9", | ||
| 300: "#22d3ee", | ||
| 400: "#06b6d4", | ||
| 500: "#0891b2", | ||
| 600: "#0e7490", | ||
| 700: "#155e75", | ||
| 800: "#164e63", | ||
| 900: "#083344" | ||
| } | ||
| }, | ||
| alpha: { | ||
| 100: "ff", | ||
| 90: "e5", | ||
| 80: "cc", | ||
| 70: "b3", | ||
| 60: "99", | ||
| 50: "80", | ||
| 40: "66", | ||
| 30: "4d", | ||
| 20: "33", | ||
| 10: "1a", | ||
| 0: "00" | ||
| }, | ||
| font: { | ||
| size: { | ||
| "2xs": "calc(var(--tsrd-font-size) * 0.625)", | ||
| xs: "calc(var(--tsrd-font-size) * 0.75)", | ||
| sm: "calc(var(--tsrd-font-size) * 0.875)", | ||
| md: "var(--tsrd-font-size)", | ||
| lg: "calc(var(--tsrd-font-size) * 1.125)", | ||
| xl: "calc(var(--tsrd-font-size) * 1.25)", | ||
| "2xl": "calc(var(--tsrd-font-size) * 1.5)", | ||
| "3xl": "calc(var(--tsrd-font-size) * 1.875)", | ||
| "4xl": "calc(var(--tsrd-font-size) * 2.25)", | ||
| "5xl": "calc(var(--tsrd-font-size) * 3)", | ||
| "6xl": "calc(var(--tsrd-font-size) * 3.75)", | ||
| "7xl": "calc(var(--tsrd-font-size) * 4.5)", | ||
| "8xl": "calc(var(--tsrd-font-size) * 6)", | ||
| "9xl": "calc(var(--tsrd-font-size) * 8)" | ||
| }, | ||
| lineHeight: { | ||
| "3xs": "calc(var(--tsrd-font-size) * 0.75)", | ||
| "2xs": "calc(var(--tsrd-font-size) * 0.875)", | ||
| xs: "calc(var(--tsrd-font-size) * 1)", | ||
| sm: "calc(var(--tsrd-font-size) * 1.25)", | ||
| md: "calc(var(--tsrd-font-size) * 1.5)", | ||
| lg: "calc(var(--tsrd-font-size) * 1.75)", | ||
| xl: "calc(var(--tsrd-font-size) * 2)", | ||
| "2xl": "calc(var(--tsrd-font-size) * 2.25)", | ||
| "3xl": "calc(var(--tsrd-font-size) * 2.5)", | ||
| "4xl": "calc(var(--tsrd-font-size) * 2.75)", | ||
| "5xl": "calc(var(--tsrd-font-size) * 3)", | ||
| "6xl": "calc(var(--tsrd-font-size) * 3.25)", | ||
| "7xl": "calc(var(--tsrd-font-size) * 3.5)", | ||
| "8xl": "calc(var(--tsrd-font-size) * 3.75)", | ||
| "9xl": "calc(var(--tsrd-font-size) * 4)" | ||
| }, | ||
| weight: { | ||
| thin: "100", | ||
| extralight: "200", | ||
| light: "300", | ||
| normal: "400", | ||
| medium: "500", | ||
| semibold: "600", | ||
| bold: "700", | ||
| extrabold: "800", | ||
| black: "900" | ||
| }, | ||
| fontFamily: { | ||
| sans: "ui-sans-serif, Inter, system-ui, sans-serif, sans-serif", | ||
| mono: `ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace` | ||
| } | ||
| }, | ||
| breakpoints: { | ||
| xs: "320px", | ||
| sm: "640px", | ||
| md: "768px", | ||
| lg: "1024px", | ||
| xl: "1280px", | ||
| "2xl": "1536px" | ||
| }, | ||
| border: { radius: { | ||
| none: "0px", | ||
| xs: "calc(var(--tsrd-font-size) * 0.125)", | ||
| sm: "calc(var(--tsrd-font-size) * 0.25)", | ||
| md: "calc(var(--tsrd-font-size) * 0.375)", | ||
| lg: "calc(var(--tsrd-font-size) * 0.5)", | ||
| xl: "calc(var(--tsrd-font-size) * 0.75)", | ||
| "2xl": "calc(var(--tsrd-font-size) * 1)", | ||
| "3xl": "calc(var(--tsrd-font-size) * 1.5)", | ||
| full: "9999px" | ||
| } }, | ||
| size: { | ||
| 0: "0px", | ||
| .25: "calc(var(--tsrd-font-size) * 0.0625)", | ||
| .5: "calc(var(--tsrd-font-size) * 0.125)", | ||
| 1: "calc(var(--tsrd-font-size) * 0.25)", | ||
| 1.5: "calc(var(--tsrd-font-size) * 0.375)", | ||
| 2: "calc(var(--tsrd-font-size) * 0.5)", | ||
| 2.5: "calc(var(--tsrd-font-size) * 0.625)", | ||
| 3: "calc(var(--tsrd-font-size) * 0.75)", | ||
| 3.5: "calc(var(--tsrd-font-size) * 0.875)", | ||
| 4: "calc(var(--tsrd-font-size) * 1)", | ||
| 4.5: "calc(var(--tsrd-font-size) * 1.125)", | ||
| 5: "calc(var(--tsrd-font-size) * 1.25)", | ||
| 5.5: "calc(var(--tsrd-font-size) * 1.375)", | ||
| 6: "calc(var(--tsrd-font-size) * 1.5)", | ||
| 6.5: "calc(var(--tsrd-font-size) * 1.625)", | ||
| 7: "calc(var(--tsrd-font-size) * 1.75)", | ||
| 8: "calc(var(--tsrd-font-size) * 2)", | ||
| 9: "calc(var(--tsrd-font-size) * 2.25)", | ||
| 10: "calc(var(--tsrd-font-size) * 2.5)", | ||
| 11: "calc(var(--tsrd-font-size) * 2.75)", | ||
| 12: "calc(var(--tsrd-font-size) * 3)", | ||
| 14: "calc(var(--tsrd-font-size) * 3.5)", | ||
| 16: "calc(var(--tsrd-font-size) * 4)", | ||
| 20: "calc(var(--tsrd-font-size) * 5)", | ||
| 24: "calc(var(--tsrd-font-size) * 6)", | ||
| 28: "calc(var(--tsrd-font-size) * 7)", | ||
| 32: "calc(var(--tsrd-font-size) * 8)", | ||
| 36: "calc(var(--tsrd-font-size) * 9)", | ||
| 40: "calc(var(--tsrd-font-size) * 10)", | ||
| 44: "calc(var(--tsrd-font-size) * 11)", | ||
| 48: "calc(var(--tsrd-font-size) * 12)", | ||
| 52: "calc(var(--tsrd-font-size) * 13)", | ||
| 56: "calc(var(--tsrd-font-size) * 14)", | ||
| 60: "calc(var(--tsrd-font-size) * 15)", | ||
| 64: "calc(var(--tsrd-font-size) * 16)", | ||
| 72: "calc(var(--tsrd-font-size) * 18)", | ||
| 80: "calc(var(--tsrd-font-size) * 20)", | ||
| 96: "calc(var(--tsrd-font-size) * 24)" | ||
| }, | ||
| shadow: { | ||
| xs: (_ = "rgb(0 0 0 / 0.1)") => `0 1px 2px 0 rgb(0 0 0 / 0.05)`, | ||
| sm: (color = "rgb(0 0 0 / 0.1)") => `0 1px 3px 0 ${color}, 0 1px 2px -1px ${color}`, | ||
| md: (color = "rgb(0 0 0 / 0.1)") => `0 4px 6px -1px ${color}, 0 2px 4px -2px ${color}`, | ||
| lg: (color = "rgb(0 0 0 / 0.1)") => `0 10px 15px -3px ${color}, 0 4px 6px -4px ${color}`, | ||
| xl: (color = "rgb(0 0 0 / 0.1)") => `0 20px 25px -5px ${color}, 0 8px 10px -6px ${color}`, | ||
| "2xl": (color = "rgb(0 0 0 / 0.25)") => `0 25px 50px -12px ${color}`, | ||
| inner: (color = "rgb(0 0 0 / 0.05)") => `inset 0 2px 4px 0 ${color}`, | ||
| none: () => `none` | ||
| }, | ||
| zIndices: { | ||
| hide: -1, | ||
| auto: "auto", | ||
| base: 0, | ||
| docked: 10, | ||
| dropdown: 1e3, | ||
| sticky: 1100, | ||
| banner: 1200, | ||
| overlay: 1300, | ||
| modal: 1400, | ||
| popover: 1500, | ||
| skipLink: 1600, | ||
| toast: 1700, | ||
| tooltip: 1800 | ||
| } | ||
| }; | ||
| //#endregion | ||
| //#region src/styles/use-styles.ts | ||
| const stylesFactory = (theme) => { | ||
| const { colors, font, size, alpha, border } = tokens; | ||
| const { fontFamily, size: fontSize } = font; | ||
| const css = goober.css; | ||
| const t = (light, dark) => theme === "light" ? light : dark; | ||
| return { | ||
| /** | ||
| * Fills the devtools-utils portal div (height: 100%) and is a flex parent so MainPanel | ||
| * gets a real flex height budget (not just height: 100% without a flex chain). | ||
| */ | ||
| shellRoot: css` | ||
| display: flex; | ||
| flex-direction: column; | ||
| box-sizing: border-box; | ||
| width: 100%; | ||
| height: 100%; | ||
| min-height: 0; | ||
| overflow: hidden; | ||
| `, | ||
| /** Overrides devtools-ui MainPanel overflow-y: auto so inner columns own scrolling */ | ||
| mainPanelShell: css` | ||
| display: flex; | ||
| flex-direction: column; | ||
| flex: 1 1 0%; | ||
| min-height: 0; | ||
| overflow: hidden !important; | ||
| `, | ||
| mainContainer: css` | ||
| display: flex; | ||
| flex: 1 1 0%; | ||
| min-height: 0; | ||
| overflow: hidden; | ||
| padding: ${size[2]}; | ||
| padding-top: 0; | ||
| margin-top: ${size[2]}; | ||
| `, | ||
| dragHandle: css` | ||
| width: 8px; | ||
| background: ${t(colors.gray[300], colors.darkGray[600])}; | ||
| cursor: col-resize; | ||
| position: relative; | ||
| transition: all 0.2s ease; | ||
| user-select: none; | ||
| pointer-events: all; | ||
| margin: 0 ${size[1]}; | ||
| border-radius: 2px; | ||
| &:hover { | ||
| background: ${t(colors.blue[600], colors.blue[500])}; | ||
| margin: 0 ${size[1]}; | ||
| } | ||
| &.dragging { | ||
| background: ${t(colors.blue[700], colors.blue[600])}; | ||
| margin: 0 ${size[1]}; | ||
| } | ||
| &::after { | ||
| content: ''; | ||
| position: absolute; | ||
| top: 50%; | ||
| left: 50%; | ||
| transform: translate(-50%, -50%); | ||
| width: 2px; | ||
| height: 20px; | ||
| background: ${t(colors.gray[400], colors.darkGray[400])}; | ||
| border-radius: 1px; | ||
| pointer-events: none; | ||
| } | ||
| &:hover::after, | ||
| &.dragging::after { | ||
| background: ${t(colors.blue[500], colors.blue[300])}; | ||
| } | ||
| `, | ||
| leftPanel: css` | ||
| background: ${t(colors.gray[100], colors.darkGray[800])}; | ||
| border-radius: ${border.radius.lg}; | ||
| border: 1px solid ${t(colors.gray[200], colors.darkGray[700])}; | ||
| display: flex; | ||
| flex-direction: column; | ||
| overflow: hidden; | ||
| min-height: 0; | ||
| flex-shrink: 0; | ||
| `, | ||
| rightPanel: css` | ||
| background: ${t(colors.gray[100], colors.darkGray[800])}; | ||
| border-radius: ${border.radius.lg}; | ||
| border: 1px solid ${t(colors.gray[200], colors.darkGray[700])}; | ||
| display: flex; | ||
| flex-direction: column; | ||
| overflow: hidden; | ||
| min-height: 0; | ||
| flex: 1; | ||
| `, | ||
| panelHeader: css` | ||
| font-size: ${fontSize.md}; | ||
| font-weight: ${font.weight.bold}; | ||
| color: ${t(colors.blue[700], colors.blue[400])}; | ||
| padding: ${size[2]}; | ||
| border-bottom: 1px solid ${t(colors.gray[200], colors.darkGray[700])}; | ||
| background: ${t(colors.gray[100], colors.darkGray[800])}; | ||
| flex-shrink: 0; | ||
| `, | ||
| utilList: css` | ||
| flex: 1; | ||
| overflow-y: auto; | ||
| padding: ${size[1]}; | ||
| min-height: 0; | ||
| `, | ||
| utilGroup: css` | ||
| margin-bottom: ${size[2]}; | ||
| `, | ||
| utilGroupHeader: css` | ||
| font-size: ${fontSize.xs}; | ||
| font-weight: ${font.weight.semibold}; | ||
| color: ${t(colors.gray[600], colors.gray[400])}; | ||
| text-transform: uppercase; | ||
| letter-spacing: 0.05em; | ||
| margin-bottom: ${size[1]}; | ||
| padding: ${size[1]} ${size[2]}; | ||
| background: ${t(colors.gray[200], colors.darkGray[700])}; | ||
| border-radius: ${border.radius.md}; | ||
| `, | ||
| utilRow: css` | ||
| display: flex; | ||
| justify-content: space-between; | ||
| align-items: center; | ||
| padding: ${size[2]}; | ||
| margin-bottom: ${size[1]}; | ||
| background: ${t(colors.gray[200], colors.darkGray[700])}; | ||
| border-radius: ${border.radius.md}; | ||
| cursor: pointer; | ||
| transition: all 0.2s ease; | ||
| border: 1px solid transparent; | ||
| &:hover { | ||
| background: ${t(colors.gray[300], colors.darkGray[600])}; | ||
| border-color: ${t(colors.gray[400], colors.darkGray[500])}; | ||
| } | ||
| `, | ||
| utilRowSelected: css` | ||
| background: ${t(colors.blue[100], colors.blue[900] + alpha[20])}; | ||
| border-color: ${t(colors.blue[600], colors.blue[500])}; | ||
| box-shadow: 0 0 0 1px | ||
| ${t(colors.blue[600] + alpha[30], colors.blue[500] + alpha[30])}; | ||
| `, | ||
| utilKey: css` | ||
| font-family: ${fontFamily.mono}; | ||
| font-size: ${fontSize.xs}; | ||
| color: ${t(colors.gray[900], colors.gray[100])}; | ||
| flex: 1; | ||
| overflow: hidden; | ||
| text-overflow: ellipsis; | ||
| white-space: nowrap; | ||
| `, | ||
| utilStatus: css` | ||
| font-size: ${fontSize.xs}; | ||
| color: ${t(colors.gray[600], colors.gray[400])}; | ||
| text-transform: uppercase; | ||
| letter-spacing: 0.05em; | ||
| padding: ${size[1]} ${size[1]}; | ||
| background: ${t(colors.gray[300], colors.darkGray[600])}; | ||
| border-radius: ${border.radius.sm}; | ||
| margin-left: ${size[1]}; | ||
| `, | ||
| stateDetails: css` | ||
| flex: 1; | ||
| overflow-y: auto; | ||
| padding: ${size[2]}; | ||
| min-height: 0; | ||
| `, | ||
| stateHeader: css` | ||
| margin-bottom: ${size[2]}; | ||
| padding-bottom: ${size[2]}; | ||
| border-bottom: 1px solid ${t(colors.gray[200], colors.darkGray[700])}; | ||
| `, | ||
| stateTitle: css` | ||
| font-size: ${fontSize.md}; | ||
| font-weight: ${font.weight.bold}; | ||
| color: ${t(colors.blue[700], colors.blue[400])}; | ||
| margin-bottom: ${size[1]}; | ||
| `, | ||
| stateKey: css` | ||
| font-family: ${fontFamily.mono}; | ||
| font-size: ${fontSize.xs}; | ||
| color: ${t(colors.gray[600], colors.gray[400])}; | ||
| word-break: break-all; | ||
| `, | ||
| stateContent: css` | ||
| background: ${t(colors.gray[100], colors.darkGray[700])}; | ||
| border-radius: ${border.radius.md}; | ||
| padding: ${size[2]}; | ||
| border: 1px solid ${t(colors.gray[300], colors.darkGray[600])}; | ||
| `, | ||
| detailsGrid: css` | ||
| display: grid; | ||
| grid-template-columns: 1fr; | ||
| gap: ${size[2]}; | ||
| align-items: start; | ||
| `, | ||
| detailSection: css` | ||
| background: ${t(colors.white, colors.darkGray[700])}; | ||
| border: 1px solid ${t(colors.gray[300], colors.darkGray[600])}; | ||
| border-radius: ${border.radius.md}; | ||
| padding: ${size[2]}; | ||
| `, | ||
| detailSectionHeader: css` | ||
| font-size: ${fontSize.sm}; | ||
| font-weight: ${font.weight.bold}; | ||
| color: ${t(colors.gray[800], colors.gray[200])}; | ||
| margin-bottom: ${size[1]}; | ||
| text-transform: uppercase; | ||
| letter-spacing: 0.04em; | ||
| `, | ||
| actionsRow: css` | ||
| display: flex; | ||
| flex-wrap: wrap; | ||
| gap: ${size[2]}; | ||
| `, | ||
| actionButton: css` | ||
| display: inline-flex; | ||
| align-items: center; | ||
| gap: ${size[1]}; | ||
| padding: ${size[1]} ${size[2]}; | ||
| border-radius: ${border.radius.md}; | ||
| border: 1px solid ${t(colors.gray[300], colors.darkGray[500])}; | ||
| background: ${t(colors.gray[200], colors.darkGray[600])}; | ||
| color: ${t(colors.gray[900], colors.gray[100])}; | ||
| font-size: ${fontSize.xs}; | ||
| cursor: pointer; | ||
| user-select: none; | ||
| transition: | ||
| background 0.15s, | ||
| border-color 0.15s; | ||
| &:hover { | ||
| background: ${t(colors.gray[300], colors.darkGray[500])}; | ||
| border-color: ${t(colors.gray[400], colors.darkGray[400])}; | ||
| } | ||
| &:disabled { | ||
| opacity: 0.5; | ||
| cursor: not-allowed; | ||
| &:hover { | ||
| background: ${t(colors.gray[200], colors.darkGray[600])}; | ||
| border-color: ${t(colors.gray[300], colors.darkGray[500])}; | ||
| } | ||
| } | ||
| `, | ||
| actionDotBlue: css` | ||
| width: 6px; | ||
| height: 6px; | ||
| border-radius: 9999px; | ||
| background: ${colors.blue[400]}; | ||
| `, | ||
| actionDotGreen: css` | ||
| width: 6px; | ||
| height: 6px; | ||
| border-radius: 9999px; | ||
| background: ${colors.green[400]}; | ||
| `, | ||
| actionDotRed: css` | ||
| width: 6px; | ||
| height: 6px; | ||
| border-radius: 9999px; | ||
| background: ${colors.red[400]}; | ||
| `, | ||
| actionDotYellow: css` | ||
| width: 6px; | ||
| height: 6px; | ||
| border-radius: 9999px; | ||
| background: ${colors.yellow[400]}; | ||
| `, | ||
| actionDotOrange: css` | ||
| width: 6px; | ||
| height: 6px; | ||
| border-radius: 9999px; | ||
| background: ${colors.pink[400]}; | ||
| `, | ||
| actionDotPurple: css` | ||
| width: 6px; | ||
| height: 6px; | ||
| border-radius: 9999px; | ||
| background: ${colors.purple[400]}; | ||
| `, | ||
| infoGrid: css` | ||
| display: grid; | ||
| grid-template-columns: auto 1fr; | ||
| gap: ${size[1]}; | ||
| row-gap: ${size[1]}; | ||
| align-items: center; | ||
| `, | ||
| infoLabel: css` | ||
| color: ${t(colors.gray[600], colors.gray[400])}; | ||
| font-size: ${fontSize.xs}; | ||
| text-transform: uppercase; | ||
| letter-spacing: 0.05em; | ||
| `, | ||
| infoValueMono: css` | ||
| font-family: ${fontFamily.mono}; | ||
| font-size: ${fontSize.xs}; | ||
| color: ${t(colors.gray[900], colors.gray[100])}; | ||
| word-break: break-all; | ||
| `, | ||
| noSelection: css` | ||
| flex: 1; | ||
| display: flex; | ||
| align-items: center; | ||
| justify-content: center; | ||
| color: ${t(colors.gray[500], colors.gray[500])}; | ||
| font-style: italic; | ||
| text-align: center; | ||
| padding: ${size[4]}; | ||
| `, | ||
| sectionContainer: css` | ||
| display: flex; | ||
| flex-wrap: wrap; | ||
| gap: ${size[4]}; | ||
| `, | ||
| section: css` | ||
| background: ${t(colors.gray[100], colors.darkGray[800])}; | ||
| border-radius: ${border.radius.lg}; | ||
| box-shadow: ${tokens.shadow.md(t(colors.gray[400] + alpha[80], colors.black + alpha[80]))}; | ||
| padding: ${size[4]}; | ||
| margin-bottom: ${size[4]}; | ||
| border: 1px solid ${t(colors.gray[200], colors.darkGray[700])}; | ||
| min-width: 0; | ||
| max-width: 33%; | ||
| max-height: fit-content; | ||
| `, | ||
| sectionHeader: css` | ||
| font-size: ${fontSize.lg}; | ||
| font-weight: ${font.weight.bold}; | ||
| margin-bottom: ${size[2]}; | ||
| color: ${t(colors.blue[600], colors.blue[400])}; | ||
| letter-spacing: 0.01em; | ||
| display: flex; | ||
| align-items: center; | ||
| gap: ${size[2]}; | ||
| `, | ||
| sectionEmpty: css` | ||
| color: ${t(colors.gray[500], colors.gray[500])}; | ||
| font-size: ${fontSize.sm}; | ||
| font-style: italic; | ||
| margin: ${size[2]} 0; | ||
| `, | ||
| instanceList: css` | ||
| display: flex; | ||
| flex-direction: column; | ||
| gap: ${size[2]}; | ||
| background: ${t(colors.gray[200], colors.darkGray[700])}; | ||
| border: 1px solid ${t(colors.gray[300], colors.darkGray[600])}; | ||
| `, | ||
| instanceCard: css` | ||
| background: ${t(colors.gray[200], colors.darkGray[700])}; | ||
| border-radius: ${border.radius.md}; | ||
| padding: ${size[3]}; | ||
| border: 1px solid ${t(colors.gray[300], colors.darkGray[600])}; | ||
| font-size: ${fontSize.sm}; | ||
| color: ${t(colors.gray[900], colors.gray[100])}; | ||
| font-family: ${fontFamily.mono}; | ||
| overflow-x: auto; | ||
| transition: | ||
| box-shadow 0.3s, | ||
| background 0.3s; | ||
| ` | ||
| }; | ||
| }; | ||
| function useStyles() { | ||
| const { theme } = useTheme(); | ||
| const [styles, setStyles] = createSignal(stylesFactory(theme())); | ||
| createEffect(() => { | ||
| setStyles(stylesFactory(theme())); | ||
| }); | ||
| return styles; | ||
| } | ||
| //#endregion | ||
| //#region src/components/util-groups.ts | ||
| const UTIL_GROUPS = [ | ||
| { | ||
| key: "debouncers", | ||
| label: "Debouncers", | ||
| displayName: "Debouncer" | ||
| }, | ||
| { | ||
| key: "throttlers", | ||
| label: "Throttlers", | ||
| displayName: "Throttler" | ||
| }, | ||
| { | ||
| key: "batchers", | ||
| label: "Batchers", | ||
| displayName: "Batcher" | ||
| }, | ||
| { | ||
| key: "queuers", | ||
| label: "Queuers", | ||
| displayName: "Queuer" | ||
| }, | ||
| { | ||
| key: "rateLimiters", | ||
| label: "Rate Limiters", | ||
| displayName: "Rate Limiter" | ||
| }, | ||
| { | ||
| key: "asyncDebouncers", | ||
| label: "Async Debouncers", | ||
| displayName: "Async Debouncer" | ||
| }, | ||
| { | ||
| key: "asyncThrottlers", | ||
| label: "Async Throttlers", | ||
| displayName: "Async Throttler" | ||
| }, | ||
| { | ||
| key: "asyncBatchers", | ||
| label: "Async Batchers", | ||
| displayName: "Async Batcher" | ||
| }, | ||
| { | ||
| key: "asyncQueuers", | ||
| label: "Async Queuers", | ||
| displayName: "Async Queuer" | ||
| }, | ||
| { | ||
| key: "asyncRateLimiters", | ||
| label: "Async Rate Limiters", | ||
| displayName: "Async Rate Limiter" | ||
| } | ||
| ]; | ||
| //#endregion | ||
| //#region src/components/UtilList.tsx | ||
| var _tmpl$$4 = /* @__PURE__ */ template(`<div>`), _tmpl$2$3 = /* @__PURE__ */ template(`<div><div>`), _tmpl$3$2 = /* @__PURE__ */ template(`<div><div></div><div>`); | ||
| function UtilList(props) { | ||
| const styles = useStyles(); | ||
| return (() => { | ||
| var _el$ = _tmpl$$4(); | ||
| insert(_el$, createComponent(For, { | ||
| each: UTIL_GROUPS, | ||
| children: (group) => memo(() => memo(() => props.utilState()[group.key].length > 0)() && (() => { | ||
| var _el$2 = _tmpl$2$3(), _el$3 = _el$2.firstChild; | ||
| insert(_el$3, () => group.label); | ||
| insert(_el$2, createComponent(For, { | ||
| get each() { | ||
| return props.utilState()[group.key]; | ||
| }, | ||
| children: (instance) => { | ||
| const status = (() => { | ||
| try { | ||
| const statusAccessor = useSelector(instance.store, (s) => s.status); | ||
| return () => statusAccessor() ?? "unknown"; | ||
| } catch { | ||
| return () => "unknown"; | ||
| } | ||
| })(); | ||
| return (() => { | ||
| var _el$4 = _tmpl$3$2(), _el$5 = _el$4.firstChild, _el$6 = _el$5.nextSibling; | ||
| _el$4.$$click = () => props.setSelectedKey(instance.key ?? null); | ||
| insert(_el$5, () => instance.key); | ||
| insert(_el$6, status); | ||
| effect((_p$) => { | ||
| var _v$3 = clsx(styles().utilRow, props.selectedKey() === instance.key && styles().utilRowSelected), _v$4 = styles().utilKey, _v$5 = styles().utilStatus; | ||
| _v$3 !== _p$.e && className(_el$4, _p$.e = _v$3); | ||
| _v$4 !== _p$.t && className(_el$5, _p$.t = _v$4); | ||
| _v$5 !== _p$.a && className(_el$6, _p$.a = _v$5); | ||
| return _p$; | ||
| }, { | ||
| e: void 0, | ||
| t: void 0, | ||
| a: void 0 | ||
| }); | ||
| return _el$4; | ||
| })(); | ||
| } | ||
| }), null); | ||
| effect((_p$) => { | ||
| var _v$ = styles().utilGroup, _v$2 = styles().utilGroupHeader; | ||
| _v$ !== _p$.e && className(_el$2, _p$.e = _v$); | ||
| _v$2 !== _p$.t && className(_el$3, _p$.t = _v$2); | ||
| return _p$; | ||
| }, { | ||
| e: void 0, | ||
| t: void 0 | ||
| }); | ||
| return _el$2; | ||
| })()) | ||
| })); | ||
| effect(() => className(_el$, styles().utilList)); | ||
| return _el$; | ||
| })(); | ||
| } | ||
| delegateEvents(["click"]); | ||
| //#endregion | ||
| //#region src/utils/read-pacer-store-state.ts | ||
| /** TanStack Store (`@tanstack/store`) used by pacer utils. */ | ||
| function isPacerUtilTanStackStore(x) { | ||
| return typeof x === "object" && x !== null && "subscribe" in x && typeof x.subscribe === "function" && "get" in x && typeof x.get === "function"; | ||
| } | ||
| /** | ||
| * Core pacer utils expose TanStack `Store` (`get()` / `state`). React hooks may | ||
| * also expose a reactive `state` field. Use this for devtools UI that needs a | ||
| * plain snapshot (e.g. JsonTree). | ||
| */ | ||
| function getPacerUtilStoreState(instance) { | ||
| const store = instance.store; | ||
| if (store && typeof store.get === "function") return store.get(); | ||
| if (store && store.state !== void 0) return store.state; | ||
| return instance.state; | ||
| } | ||
| //#endregion | ||
| //#region src/components/ActionButtons.tsx | ||
| var _tmpl$$3 = /* @__PURE__ */ template(`<div>No actions available for this util`), _tmpl$2$2 = /* @__PURE__ */ template(`<div>`), _tmpl$3$1 = /* @__PURE__ */ template(`<button><span>`), _tmpl$4 = /* @__PURE__ */ template(`<button><span></span>Flush`), _tmpl$5 = /* @__PURE__ */ template(`<button><span></span>Cancel`), _tmpl$6 = /* @__PURE__ */ template(`<button><span></span>Reset`), _tmpl$7 = /* @__PURE__ */ template(`<button><span></span>Clear`); | ||
| function ActionButtons(props) { | ||
| const styles = useStyles(); | ||
| const utilInstance = props.instance; | ||
| const store = utilInstance?.store; | ||
| const stateAccessor = isPacerUtilTanStackStore(store) ? useSelector(store, (s) => s !== null && s !== void 0 && typeof s === "object" ? s : {}, { compare: shallow }) : () => getPacerUtilStoreState(utilInstance); | ||
| const getState = () => stateAccessor(); | ||
| const hasPending = () => { | ||
| return "isPending" in getState(); | ||
| }; | ||
| const isPending = () => { | ||
| const u = getState(); | ||
| return "isPending" in u ? !!u.isPending : false; | ||
| }; | ||
| const isEmptyFlag = () => { | ||
| const u = getState(); | ||
| return "isEmpty" in u ? !!u.isEmpty : false; | ||
| }; | ||
| const hasFlush = typeof utilInstance.flush === "function"; | ||
| const hasCancel = typeof utilInstance.cancel === "function"; | ||
| const hasReset = typeof utilInstance.reset === "function"; | ||
| const hasClear = typeof utilInstance.clear === "function"; | ||
| const hasStart = typeof utilInstance.start === "function"; | ||
| const hasStop = typeof utilInstance.stop === "function"; | ||
| const hasStartStop = hasStart && hasStop; | ||
| const isRunning = () => { | ||
| const u = getState(); | ||
| return "isRunning" in u ? !!u.isRunning : true; | ||
| }; | ||
| if (!hasPending() && !hasFlush && !hasCancel && !hasReset && !hasClear) return (() => { | ||
| var _el$ = _tmpl$$3(); | ||
| effect(() => className(_el$, styles().sectionEmpty)); | ||
| return _el$; | ||
| })(); | ||
| const emitName = `d-${props.utilName}`; | ||
| const utilNameLower = props.utilName.toLowerCase(); | ||
| return (() => { | ||
| var _el$2 = _tmpl$2$2(); | ||
| insert(_el$2, (() => { | ||
| var _c$ = memo(() => !!hasPending()); | ||
| return () => _c$() && (() => { | ||
| var _el$3 = _tmpl$3$1(), _el$4 = _el$3.firstChild; | ||
| _el$3.$$mousedown = () => { | ||
| const next = !isPending(); | ||
| utilInstance.store.setState((prev) => ({ | ||
| ...prev, | ||
| isPending: next | ||
| })); | ||
| emitChange(emitName, utilInstance); | ||
| }; | ||
| insert(_el$3, () => isPending() ? "Restore Pending" : "Trigger Pending", null); | ||
| effect((_p$) => { | ||
| var _v$ = styles().actionButton, _v$2 = styles().actionDotBlue; | ||
| _v$ !== _p$.e && className(_el$3, _p$.e = _v$); | ||
| _v$2 !== _p$.t && className(_el$4, _p$.t = _v$2); | ||
| return _p$; | ||
| }, { | ||
| e: void 0, | ||
| t: void 0 | ||
| }); | ||
| return _el$3; | ||
| })(); | ||
| })(), null); | ||
| insert(_el$2, hasFlush && (() => { | ||
| var _el$5 = _tmpl$4(), _el$6 = _el$5.firstChild; | ||
| _el$5.$$mousedown = () => { | ||
| utilInstance.flush(); | ||
| emitChange(emitName, utilInstance); | ||
| }; | ||
| effect((_p$) => { | ||
| var _v$3 = styles().actionButton, _v$4 = utilNameLower.includes("debouncer") && !isPending() || utilNameLower.includes("throttler") && !isPending() || utilNameLower.includes("batcher") && isEmptyFlag() || utilNameLower.includes("queuer") && isEmptyFlag(), _v$5 = styles().actionDotGreen; | ||
| _v$3 !== _p$.e && className(_el$5, _p$.e = _v$3); | ||
| _v$4 !== _p$.t && (_el$5.disabled = _p$.t = _v$4); | ||
| _v$5 !== _p$.a && className(_el$6, _p$.a = _v$5); | ||
| return _p$; | ||
| }, { | ||
| e: void 0, | ||
| t: void 0, | ||
| a: void 0 | ||
| }); | ||
| return _el$5; | ||
| })(), null); | ||
| insert(_el$2, hasCancel && (() => { | ||
| var _el$7 = _tmpl$5(), _el$8 = _el$7.firstChild; | ||
| _el$7.$$mousedown = () => { | ||
| utilInstance.cancel(); | ||
| emitChange(emitName, utilInstance); | ||
| }; | ||
| effect((_p$) => { | ||
| var _v$6 = styles().actionButton, _v$7 = utilNameLower.includes("debouncer") && !isPending() || utilNameLower.includes("throttler") && !isPending() || utilNameLower.includes("batcher") && isEmptyFlag() || utilNameLower.includes("queuer") && isEmptyFlag(), _v$8 = styles().actionDotRed; | ||
| _v$6 !== _p$.e && className(_el$7, _p$.e = _v$6); | ||
| _v$7 !== _p$.t && (_el$7.disabled = _p$.t = _v$7); | ||
| _v$8 !== _p$.a && className(_el$8, _p$.a = _v$8); | ||
| return _p$; | ||
| }, { | ||
| e: void 0, | ||
| t: void 0, | ||
| a: void 0 | ||
| }); | ||
| return _el$7; | ||
| })(), null); | ||
| insert(_el$2, hasReset && (() => { | ||
| var _el$9 = _tmpl$6(), _el$0 = _el$9.firstChild; | ||
| _el$9.$$mousedown = () => { | ||
| utilInstance.reset(); | ||
| emitChange(emitName, utilInstance); | ||
| }; | ||
| effect((_p$) => { | ||
| var _v$9 = styles().actionButton, _v$0 = styles().actionDotYellow; | ||
| _v$9 !== _p$.e && className(_el$9, _p$.e = _v$9); | ||
| _v$0 !== _p$.t && className(_el$0, _p$.t = _v$0); | ||
| return _p$; | ||
| }, { | ||
| e: void 0, | ||
| t: void 0 | ||
| }); | ||
| return _el$9; | ||
| })(), null); | ||
| insert(_el$2, hasClear && (() => { | ||
| var _el$1 = _tmpl$7(), _el$10 = _el$1.firstChild; | ||
| _el$1.$$mousedown = () => { | ||
| utilInstance.clear(); | ||
| emitChange(emitName, utilInstance); | ||
| }; | ||
| effect((_p$) => { | ||
| var _v$1 = styles().actionButton, _v$10 = utilNameLower.includes("batcher") && isEmptyFlag() || utilNameLower.includes("queuer") && isEmptyFlag(), _v$11 = styles().actionDotOrange; | ||
| _v$1 !== _p$.e && className(_el$1, _p$.e = _v$1); | ||
| _v$10 !== _p$.t && (_el$1.disabled = _p$.t = _v$10); | ||
| _v$11 !== _p$.a && className(_el$10, _p$.a = _v$11); | ||
| return _p$; | ||
| }, { | ||
| e: void 0, | ||
| t: void 0, | ||
| a: void 0 | ||
| }); | ||
| return _el$1; | ||
| })(), null); | ||
| insert(_el$2, hasStartStop && (() => { | ||
| var _el$11 = _tmpl$3$1(), _el$12 = _el$11.firstChild; | ||
| _el$11.$$mousedown = () => { | ||
| if (isRunning()) utilInstance.stop(); | ||
| else utilInstance.start(); | ||
| emitChange(emitName, utilInstance); | ||
| }; | ||
| insert(_el$11, () => isRunning() ? "Stop" : "Start", null); | ||
| effect((_p$) => { | ||
| var _v$12 = styles().actionButton, _v$13 = styles().actionDotPurple; | ||
| _v$12 !== _p$.e && className(_el$11, _p$.e = _v$12); | ||
| _v$13 !== _p$.t && className(_el$12, _p$.t = _v$13); | ||
| return _p$; | ||
| }, { | ||
| e: void 0, | ||
| t: void 0 | ||
| }); | ||
| return _el$11; | ||
| })(), null); | ||
| effect(() => className(_el$2, styles().actionsRow)); | ||
| return _el$2; | ||
| })(); | ||
| } | ||
| delegateEvents(["mousedown"]); | ||
| //#endregion | ||
| //#region src/components/StateHeader.tsx | ||
| var _tmpl$$2 = /* @__PURE__ */ template(`<div><div></div><div style=display:flex;align-items:center;gap:16px><div><div>Key</div><div></div><div>Last Updated</div><div> (<!>)</div></div><div style=margin-left:auto;font-weight:bold>% reduction`); | ||
| dayjs.extend(relativeTime); | ||
| function reductionFromState(entry, state) { | ||
| if (!state) return 0; | ||
| const completedExecutions = entry.type.toLowerCase().includes("async") ? Number(state.settleCount) || 0 : Number(state.executionCount) || 0; | ||
| if (entry.type.toLowerCase().includes("batcher")) { | ||
| const totalItemsProcessed = Number(state.totalItemsProcessed) || 0; | ||
| if (totalItemsProcessed === 0) return 0; | ||
| return Math.round((totalItemsProcessed - completedExecutions) / totalItemsProcessed * 100); | ||
| } | ||
| let requestCount = 0; | ||
| if (state.maybeExecuteCount !== void 0) requestCount = Number(state.maybeExecuteCount) || 0; | ||
| else if (state.addItemCount !== void 0) requestCount = Number(state.addItemCount) || 0; | ||
| else return 0; | ||
| if (requestCount === 0) return 0; | ||
| const reduction = requestCount - completedExecutions; | ||
| return Math.round(reduction / requestCount * 100); | ||
| } | ||
| function StateHeaderInner(props) { | ||
| const key = props.entry.instance.key; | ||
| const updatedAt = () => props.utilState().lastUpdatedByKey[key] ?? Date.now(); | ||
| const getRelativeTime = () => { | ||
| const at = updatedAt(); | ||
| const diffMs = props.now() - at; | ||
| const diffSeconds = Math.floor(diffMs / 1e3); | ||
| if (diffSeconds < 60) return `${diffSeconds} second${diffSeconds !== 1 ? "s" : ""} ago`; | ||
| return dayjs(at).fromNow(); | ||
| }; | ||
| const store = props.entry.instance?.store; | ||
| const stateAccessor = isPacerUtilTanStackStore(store) ? useSelector(store, (s) => s !== null && s !== void 0 && typeof s === "object" ? s : {}, { compare: shallow }) : () => getPacerUtilStoreState(props.entry.instance); | ||
| const reductionPercentage = () => reductionFromState(props.entry, stateAccessor()); | ||
| return (() => { | ||
| var _el$ = _tmpl$$2(), _el$2 = _el$.firstChild, _el$4 = _el$2.nextSibling.firstChild, _el$5 = _el$4.firstChild, _el$6 = _el$5.nextSibling, _el$7 = _el$6.nextSibling, _el$8 = _el$7.nextSibling, _el$9 = _el$8.firstChild, _el$1 = _el$9.nextSibling; | ||
| _el$1.nextSibling; | ||
| var _el$10 = _el$4.nextSibling, _el$11 = _el$10.firstChild; | ||
| insert(_el$2, () => props.entry.type); | ||
| insert(_el$6, key); | ||
| insert(_el$8, () => new Date(updatedAt()).toLocaleTimeString(), _el$9); | ||
| insert(_el$8, getRelativeTime, _el$1); | ||
| insert(_el$10, reductionPercentage, _el$11); | ||
| effect((_p$) => { | ||
| var _v$ = props.styles.stateHeader, _v$2 = props.styles.stateTitle, _v$3 = props.styles.infoGrid, _v$4 = props.styles.infoLabel, _v$5 = props.styles.infoValueMono, _v$6 = props.styles.infoLabel, _v$7 = props.styles.infoValueMono, _v$8 = props.styles.infoValueMono; | ||
| _v$ !== _p$.e && className(_el$, _p$.e = _v$); | ||
| _v$2 !== _p$.t && className(_el$2, _p$.t = _v$2); | ||
| _v$3 !== _p$.a && className(_el$4, _p$.a = _v$3); | ||
| _v$4 !== _p$.o && className(_el$5, _p$.o = _v$4); | ||
| _v$5 !== _p$.i && className(_el$6, _p$.i = _v$5); | ||
| _v$6 !== _p$.n && className(_el$7, _p$.n = _v$6); | ||
| _v$7 !== _p$.s && className(_el$8, _p$.s = _v$7); | ||
| _v$8 !== _p$.h && className(_el$10, _p$.h = _v$8); | ||
| return _p$; | ||
| }, { | ||
| e: void 0, | ||
| t: void 0, | ||
| a: void 0, | ||
| o: void 0, | ||
| i: void 0, | ||
| n: void 0, | ||
| s: void 0, | ||
| h: void 0 | ||
| }); | ||
| return _el$; | ||
| })(); | ||
| } | ||
| function StateHeader(props) { | ||
| const styles = useStyles(); | ||
| const [now, setNow] = createSignal(Date.now()); | ||
| onMount(() => { | ||
| const interval = setInterval(() => { | ||
| setNow(Date.now()); | ||
| }, 1e3); | ||
| onCleanup(() => { | ||
| clearInterval(interval); | ||
| }); | ||
| }); | ||
| return createComponent(Show, { | ||
| get when() { | ||
| return props.selectedInstance(); | ||
| }, | ||
| children: (entry) => createComponent(StateHeaderInner, { | ||
| get entry() { | ||
| return entry(); | ||
| }, | ||
| get styles() { | ||
| return styles(); | ||
| }, | ||
| now, | ||
| get utilState() { | ||
| return props.utilState; | ||
| } | ||
| }) | ||
| }); | ||
| } | ||
| //#endregion | ||
| //#region src/components/UtilStateJsonTree.tsx | ||
| /** | ||
| * Subscribes to the util's TanStack Store so the tree updates live (same | ||
| * idea as {@link UtilList} status). A plain snapshot would only refresh when the | ||
| * devtools shell re-renders, which does not happen on nested store updates. | ||
| */ | ||
| function UtilStateJsonTree(props) { | ||
| const store = props.instance?.store; | ||
| if (isPacerUtilTanStackStore(store)) { | ||
| const snapshot = useSelector(store, (s) => s); | ||
| return createComponent(JsonTree, { get value() { | ||
| return snapshot(); | ||
| } }); | ||
| } | ||
| return createComponent(JsonTree, { get value() { | ||
| return getPacerUtilStoreState(props.instance); | ||
| } }); | ||
| } | ||
| //#endregion | ||
| //#region src/components/DetailsPanel.tsx | ||
| var _tmpl$$1 = /* @__PURE__ */ template(`<div>`), _tmpl$2$1 = /* @__PURE__ */ template(`<div>Select a util from the left panel to view its state`), _tmpl$3 = /* @__PURE__ */ template(`<div><div><div>Actions</div></div><div><div>State</div><div></div></div><div><div>Options</div><div>`); | ||
| function DetailsPanel(props) { | ||
| const styles = useStyles(); | ||
| return (() => { | ||
| var _el$ = _tmpl$$1(); | ||
| insert(_el$, () => { | ||
| const entry = props.selectedInstance(); | ||
| if (!entry) return (() => { | ||
| var _el$2 = _tmpl$2$1(); | ||
| effect(() => className(_el$2, styles().noSelection)); | ||
| return _el$2; | ||
| })(); | ||
| return [createComponent(StateHeader, { | ||
| get selectedInstance() { | ||
| return props.selectedInstance; | ||
| }, | ||
| get utilState() { | ||
| return props.utilState; | ||
| } | ||
| }), (() => { | ||
| var _el$3 = _tmpl$3(), _el$4 = _el$3.firstChild, _el$5 = _el$4.firstChild, _el$6 = _el$4.nextSibling, _el$7 = _el$6.firstChild, _el$8 = _el$7.nextSibling, _el$9 = _el$6.nextSibling, _el$0 = _el$9.firstChild, _el$1 = _el$0.nextSibling; | ||
| insert(_el$4, createComponent(ActionButtons, { | ||
| get instance() { | ||
| return entry.instance; | ||
| }, | ||
| get utilName() { | ||
| return entry.type; | ||
| } | ||
| }), null); | ||
| insert(_el$8, createComponent(UtilStateJsonTree, { get instance() { | ||
| return entry.instance; | ||
| } })); | ||
| insert(_el$1, createComponent(JsonTree, { get value() { | ||
| return entry.instance.options; | ||
| } })); | ||
| effect((_p$) => { | ||
| var _v$ = styles().detailsGrid, _v$2 = styles().detailSection, _v$3 = styles().detailSectionHeader, _v$4 = styles().detailSection, _v$5 = styles().detailSectionHeader, _v$6 = styles().stateContent, _v$7 = styles().detailSection, _v$8 = styles().detailSectionHeader, _v$9 = styles().stateContent; | ||
| _v$ !== _p$.e && className(_el$3, _p$.e = _v$); | ||
| _v$2 !== _p$.t && className(_el$4, _p$.t = _v$2); | ||
| _v$3 !== _p$.a && className(_el$5, _p$.a = _v$3); | ||
| _v$4 !== _p$.o && className(_el$6, _p$.o = _v$4); | ||
| _v$5 !== _p$.i && className(_el$7, _p$.i = _v$5); | ||
| _v$6 !== _p$.n && className(_el$8, _p$.n = _v$6); | ||
| _v$7 !== _p$.s && className(_el$9, _p$.s = _v$7); | ||
| _v$8 !== _p$.h && className(_el$0, _p$.h = _v$8); | ||
| _v$9 !== _p$.r && className(_el$1, _p$.r = _v$9); | ||
| return _p$; | ||
| }, { | ||
| e: void 0, | ||
| t: void 0, | ||
| a: void 0, | ||
| o: void 0, | ||
| i: void 0, | ||
| n: void 0, | ||
| s: void 0, | ||
| h: void 0, | ||
| r: void 0 | ||
| }); | ||
| return _el$3; | ||
| })()]; | ||
| }); | ||
| effect(() => className(_el$, styles().stateDetails)); | ||
| return _el$; | ||
| })(); | ||
| } | ||
| //#endregion | ||
| //#region src/components/Shell.tsx | ||
| var _tmpl$ = /* @__PURE__ */ template(`<div><div style=min-width:150px;max-width:800px></div><div></div><div style=flex:1><div>Details`), _tmpl$2 = /* @__PURE__ */ template(`<div>`); | ||
| function readResizeObserverBlockSize(entry) { | ||
| const [box] = entry.borderBoxSize; | ||
| if (box !== void 0) return box.blockSize; | ||
| return entry.contentRect.height; | ||
| } | ||
| /** Outer plugin slot from TanStack Devtools (not the inner React mount div, which can report a tiny height). */ | ||
| function resolvePluginHost(el) { | ||
| const byId = el.closest("[id^=\"plugin-container-\"]"); | ||
| if (byId) return byId; | ||
| return el.parentElement?.parentElement?.parentElement ?? el.parentElement?.parentElement ?? el.parentElement; | ||
| } | ||
| function Shell() { | ||
| const styles = useStyles(); | ||
| const state = usePacerDevtoolsState(); | ||
| const utilState = () => state; | ||
| const [selectedKey, setSelectedKey] = createSignal(null); | ||
| const [leftPanelWidth, setLeftPanelWidth] = createSignal(300); | ||
| const [isDragging, setIsDragging] = createSignal(false); | ||
| const [shellRootEl, setShellRootEl] = createSignal(null); | ||
| const [slotHeightPx, setSlotHeightPx] = createSignal(void 0); | ||
| const selectedInstance = createMemo(() => { | ||
| const key = selectedKey(); | ||
| if (!key) return null; | ||
| for (const group of UTIL_GROUPS) { | ||
| const instance = utilState()[group.key].find((inst) => inst.key === key); | ||
| if (instance) return { | ||
| instance, | ||
| type: group.displayName | ||
| }; | ||
| } | ||
| return null; | ||
| }); | ||
| let dragStartX = 0; | ||
| let dragStartWidth = 0; | ||
| const handleMouseDown = (e) => { | ||
| e.preventDefault(); | ||
| e.stopPropagation(); | ||
| setIsDragging(true); | ||
| document.body.style.cursor = "col-resize"; | ||
| document.body.style.userSelect = "none"; | ||
| dragStartX = e.clientX; | ||
| dragStartWidth = leftPanelWidth(); | ||
| }; | ||
| const handleMouseMove = (e) => { | ||
| if (!isDragging()) return; | ||
| e.preventDefault(); | ||
| const deltaX = e.clientX - dragStartX; | ||
| setLeftPanelWidth(Math.max(150, Math.min(800, dragStartWidth + deltaX))); | ||
| }; | ||
| const handleMouseUp = () => { | ||
| setIsDragging(false); | ||
| document.body.style.cursor = ""; | ||
| document.body.style.userSelect = ""; | ||
| }; | ||
| onMount(() => { | ||
| document.addEventListener("mousemove", handleMouseMove); | ||
| document.addEventListener("mouseup", handleMouseUp); | ||
| }); | ||
| onCleanup(() => { | ||
| document.removeEventListener("mousemove", handleMouseMove); | ||
| document.removeEventListener("mouseup", handleMouseUp); | ||
| }); | ||
| /** | ||
| * TanStack Devtools plugin slots use height: 100% without min-height: 0 on flex | ||
| * ancestors, so percentage height often never constrains our tree. Observing the | ||
| * outer `plugin-container-*` host (not the immediate React mount wrapper) and | ||
| * setting an explicit pixel height makes inner overflow-y regions scroll without | ||
| * capping to the mount div’s collapsed height. | ||
| */ | ||
| createEffect(() => { | ||
| const el = shellRootEl(); | ||
| if (!el || typeof ResizeObserver === "undefined") return; | ||
| const pluginSlot = resolvePluginHost(el); | ||
| if (!pluginSlot) return; | ||
| const ro = new ResizeObserver((entries) => { | ||
| const entry = entries[0]; | ||
| if (!entry) return; | ||
| const target = entry.target; | ||
| const h = target.clientHeight > 0 ? target.clientHeight : readResizeObserverBlockSize(entry); | ||
| if (h > 0) setSlotHeightPx(Math.round(h)); | ||
| }); | ||
| ro.observe(pluginSlot); | ||
| onCleanup(() => ro.disconnect()); | ||
| }); | ||
| return (() => { | ||
| var _el$ = _tmpl$2(); | ||
| use(setShellRootEl, _el$); | ||
| insert(_el$, createComponent(MainPanel, { | ||
| get ["class"]() { | ||
| return styles().mainPanelShell; | ||
| }, | ||
| get children() { | ||
| return [createComponent(Header, { get children() { | ||
| return createComponent(HeaderLogo, { | ||
| flavor: { | ||
| light: "#84cc16", | ||
| dark: "#84cc16" | ||
| }, | ||
| children: "TanStack Pacer" | ||
| }); | ||
| } }), (() => { | ||
| var _el$2 = _tmpl$(), _el$3 = _el$2.firstChild, _el$4 = _el$3.nextSibling, _el$5 = _el$4.nextSibling, _el$6 = _el$5.firstChild; | ||
| insert(_el$3, createComponent(UtilList, { | ||
| selectedKey, | ||
| setSelectedKey, | ||
| utilState | ||
| })); | ||
| _el$4.$$mousedown = handleMouseDown; | ||
| insert(_el$5, createComponent(DetailsPanel, { | ||
| selectedInstance, | ||
| utilState | ||
| }), null); | ||
| effect((_p$) => { | ||
| var _v$ = styles().mainContainer, _v$2 = styles().leftPanel, _v$3 = `${leftPanelWidth()}px`, _v$4 = `${styles().dragHandle} ${isDragging() ? "dragging" : ""}`, _v$5 = styles().rightPanel, _v$6 = styles().panelHeader; | ||
| _v$ !== _p$.e && className(_el$2, _p$.e = _v$); | ||
| _v$2 !== _p$.t && className(_el$3, _p$.t = _v$2); | ||
| _v$3 !== _p$.a && setStyleProperty(_el$3, "width", _p$.a = _v$3); | ||
| _v$4 !== _p$.o && className(_el$4, _p$.o = _v$4); | ||
| _v$5 !== _p$.i && className(_el$5, _p$.i = _v$5); | ||
| _v$6 !== _p$.n && className(_el$6, _p$.n = _v$6); | ||
| return _p$; | ||
| }, { | ||
| e: void 0, | ||
| t: void 0, | ||
| a: void 0, | ||
| o: void 0, | ||
| i: void 0, | ||
| n: void 0 | ||
| }); | ||
| return _el$2; | ||
| })()]; | ||
| } | ||
| })); | ||
| effect((_p$) => { | ||
| var _v$7 = styles().shellRoot, _v$8 = { ...slotHeightPx() !== void 0 ? { | ||
| height: `${slotHeightPx()}px`, | ||
| "max-height": `${slotHeightPx()}px` | ||
| } : {} }; | ||
| _v$7 !== _p$.e && className(_el$, _p$.e = _v$7); | ||
| _p$.t = style(_el$, _v$8, _p$.t); | ||
| return _p$; | ||
| }, { | ||
| e: void 0, | ||
| t: void 0 | ||
| }); | ||
| return _el$; | ||
| })(); | ||
| } | ||
| delegateEvents(["mousedown"]); | ||
| //#endregion | ||
| //#region src/components/index.tsx | ||
| function PacerDevtools(props) { | ||
| return createComponent(ThemeContextProvider, { | ||
| get theme() { | ||
| return props.theme; | ||
| }, | ||
| get children() { | ||
| return createComponent(PacerContextProvider, { get children() { | ||
| return createComponent(Shell, {}); | ||
| } }); | ||
| } | ||
| }); | ||
| } | ||
| //#endregion | ||
| export { PacerDevtools as default }; |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
145263
0.33%4298
0.51%181
9.04%+ Added
+ Added
- Removed
- Removed
Updated
Updated
Updated