Files
headroom/frontend/node_modules/sveltekit-superforms/dist/client/SuperDebug.svelte
Santhosh Janardhanan de2d83092e feat: Reinitialize frontend with SvelteKit and TypeScript
- Delete old Vite+Svelte frontend
- Initialize new SvelteKit project with TypeScript
- Configure Tailwind CSS v4 + DaisyUI
- Implement JWT authentication with auto-refresh
- Create login page with form validation (Zod)
- Add protected route guards
- Update Docker configuration for single-stage build
- Add E2E tests with Playwright (6/11 passing)
- Fix Svelte 5 reactivity with $state() runes

Known issues:
- 5 E2E tests failing (timing/async issues)
- Token refresh implementation needs debugging
- Validation error display timing
2026-02-17 16:19:59 -05:00

722 lines
18 KiB
Svelte

<svelte:options runes={false} />
<script>
import { browser } from '$app/environment';
import { page } from '$app/stores';
import { readable, get } from 'svelte/store';
import { clipboardCopy } from './clipboardCopy.js';
let styleInit = false;
/**
* @typedef {unknown | Promise<unknown>} EncodeableData
* @typedef {import('svelte/store').Readable<EncodeableData>} EncodeableDataStore
*
* @typedef {EncodeableData | EncodeableDataStore} DebugData
*/
/**
* Data to be displayed as pretty JSON.
*
* @type {DebugData}
*/
export let data;
/**
* Controls when the component should be displayed.
*
* Default: `true`.
*/
export let display = true;
/**
* Controls when to show the HTTP status code of the current page (reflecs the status code of the last request).
*
* Default is `true`.
*/
export let status = true;
/**
* Optional label to identify the component easily.
*/
export let label = '';
/**
* Controls the maximum length of a string field of the data prop.
*
* Default is `120` characters. Set to `0` to disable trimming.
*/
export let stringTruncate = 120;
/**
* Reference to the pre element that contains the data.
*
* @type {HTMLPreElement | undefined}
*/
export let ref = undefined;
/**
* Controls if the data prop should be treated as a promise (skips promise detection when true).
*
* Default is `false`.
* @deprecated Promises are auto-detected from 1.3.0.
*/
export let promise = false;
/**
* Controls if the data prop should be treated as a plain object (skips promise and store detection when true, prevails over promise prop).
*
* Default is `false`.
*/
export let raw = false;
/**
* Enables the display of fields of the data prop that are functions.
*
* Default is `false`.
*/
export let functions = false;
/**
* Theme, which can also be customized with CSS variables:
*
* ```txt
* --sd-bg-color
* --sd-label-color
* --sd-promise-loading-color
* --sd-promise-rejected-color
* --sd-code-default
* --sd-info
* --sd-success
* --sd-redirect
* --sd-error
* --sd-code-key
* --sd-code-string
* --sd-code-date
* --sd-code-boolean
* --sd-code-number
* --sd-code-bigint
* --sd-code-null
* --sd-code-nan
* --sd-code-undefined
* --sd-code-function
* --sd-code-symbol
* --sd-code-error
* --sd-sb-width
* --sd-sb-height
* --sd-sb-track-color
* --sd-sb-track-color-focus
* --sd-sb-thumb-color
* --sd-sb-thumb-color-focus
* ```
*
* @type {"default" | "vscode"}
*/
export let theme = 'default';
///// Collapse behavior ///////////////////////////////////////////
/**
* Will show a collapse bar at the bottom of the component, that can be used to hide and show the output. Default is `false`.
* When toggled, the state is saved in session storage for all SuperDebug components on the page.
*/
export let collapsible = false;
/**
* Initial state for the collapsed component. Use together with the `collapsible` prop.
* On subsequent page loads, the session storage will determine the state of all SuperDebug components on the page.
*/
export let collapsed = false;
if (browser && collapsible) setCollapse();
/**
* @param {boolean|undefined} status
*/
function setCollapse(status = undefined) {
let data;
const route = $page.route.id ?? '';
try {
if (sessionStorage.SuperDebug) {
data = JSON.parse(sessionStorage.SuperDebug);
}
data = { collapsed: data && data.collapsed ? data.collapsed : {} };
data.collapsed[route] = status === undefined ? (data.collapsed[route] ?? collapsed) : status;
} catch {
data = {
collapsed: {
[route]: collapsed
}
};
}
if (status !== undefined) {
sessionStorage.SuperDebug = JSON.stringify(data);
}
collapsed = data.collapsed[route];
}
/**
* @type {ReturnType<typeof setTimeout> | undefined}
*/
let copied;
/**
* @param {Event} e
*/
async function copyContent(e) {
if (!e.target) return;
const parent = /** @type {HTMLElement} */ (e.target).closest('.super-debug');
if (!parent) return;
const codeEl = /** @type {HTMLPreElement} */ (parent.querySelector('.super-debug--code'));
if (!codeEl) return;
clearTimeout(copied);
await clipboardCopy(codeEl.innerText);
copied = setTimeout(() => (copied = undefined), 900);
}
/**
* @param {File} file
*/
function fileToJSON(file) {
return {
name: file.name,
size: file.size,
type: file.type,
lastModified: new Date(file.lastModified)
};
}
///////////////////////////////////////////////////////////////////
/**
* @param {unknown} json
* @returns {string}
*/
function syntaxHighlight(json) {
switch (typeof json) {
case 'function': {
return `<span class="function">[function ${json.name ?? 'unnamed'}]</span>`;
}
case 'symbol': {
return `<span class="symbol">${json.toString()}</span>`;
}
}
const encodedString = JSON.stringify(
json,
function (key, value) {
if (value === undefined) {
return '#}#undefined';
}
if (typeof this === 'object' && this[key] instanceof Date) {
// @ts-expect-error Date checking works with isNaN
return '#}D#' + (isNaN(this[key]) ? 'Invalid Date' : value);
}
if (typeof value === 'number') {
if (value == Number.POSITIVE_INFINITY) return '#}#Inf';
if (value == Number.NEGATIVE_INFINITY) return '#}#-Inf';
if (isNaN(value)) return '#}#NaN';
}
if (typeof value === 'bigint') {
return '#}BI#' + value;
}
if (typeof value === 'function' && functions) {
return '#}F#' + `[function ${value.name}]`;
}
if (value instanceof Error) {
return '#}E#' + `${value.name}: ${value.message || value.cause || '(No error message)'}`;
}
if (value instanceof Set) {
return Array.from(value);
}
if (value instanceof Map) {
return Array.from(value.entries());
}
if (
typeof this === 'object' &&
typeof this[key] == 'object' &&
this[key] &&
'toExponential' in this[key]
) {
return '#}DE#' + this[key].toString();
}
if (browser && typeof this === 'object' && this[key] instanceof File) {
return fileToJSON(this[key]);
}
if (browser && typeof this === 'object' && this[key] instanceof FileList) {
/** @type FileList */
const list = this[key];
const output = [];
for (let i = 0; i < list.length; i++) {
const file = list.item(i);
if (file) output.push(fileToJSON(file));
}
return output;
}
return value;
},
2
)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
return encodedString.replace(
/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?)/g,
function (match) {
let cls = 'number';
if (/^"/.test(match)) {
if (/:$/.test(match)) {
cls = 'key';
match = match.slice(1, -2) + ':';
} else {
cls = 'string';
match =
stringTruncate > 0 && match.length > stringTruncate
? match.slice(0, stringTruncate / 2) +
`[..${match.length - stringTruncate}/${match.length}..]` +
match.slice(-stringTruncate / 2)
: match;
if (match == '"#}#undefined"') {
cls = 'undefined';
match = 'undefined';
} else if (match.startsWith('"#}D#')) {
cls = 'date';
match = match.slice(5, -1);
} else if (match == '"#}#NaN"') {
cls = 'nan';
match = 'NaN';
} else if (match == '"#}#Inf"') {
cls = 'nan';
match = 'Infinity';
} else if (match == '"#}#-Inf"') {
cls = 'nan';
match = '-Infinity';
} else if (match.startsWith('"#}BI#')) {
cls = 'bigint';
match = match.slice(6, -1) + 'n';
} else if (match.startsWith('"#}F#')) {
cls = 'function';
match = match.slice(5, -1);
} else if (match.startsWith('"#}E#')) {
cls = 'error';
match = match.slice(5, -1);
} else if (match.startsWith('"#}DE#')) {
cls = 'number';
match = match.slice(6, -1);
}
}
} else if (/true|false/.test(match)) {
cls = 'boolean';
} else if (/null/.test(match)) {
cls = 'null';
}
return '<span class="' + cls + '">' + match + '</span>';
}
);
}
/**
* @param {EncodeableData} data
* @param {boolean} raw
* @param {boolean} promise
* @returns {data is Promise<unknown>}
*/
function assertPromise(data, raw, promise) {
if (raw) {
return false;
}
return (
promise ||
(typeof data === 'object' &&
data !== null &&
'then' in data &&
typeof data['then'] === 'function')
);
}
/**
* @param {DebugData} data
* @param {boolean} raw
* @returns {data is EncodeableDataStore}
*/
function assertStore(data, raw) {
if (raw) {
return false;
}
return (
typeof data === 'object' &&
data !== null &&
'subscribe' in data &&
typeof data['subscribe'] === 'function'
);
}
$: themeStyle =
theme === 'vscode'
? `
--sd-vscode-bg-color: #1f1f1f;
--sd-vscode-label-color: #cccccc;
--sd-vscode-code-default: #8c8a89;
--sd-vscode-code-key: #9cdcfe;
--sd-vscode-code-string: #ce9171;
--sd-vscode-code-number: #b5c180;
--sd-vscode-code-boolean: #4a9cd6;
--sd-vscode-code-null: #4a9cd6;
--sd-vscode-code-undefined: #4a9cd6;
--sd-vscode-code-nan: #4a9cd6;
--sd-vscode-code-symbol: #4de0c5;
--sd-vscode-sb-thumb-color: #35373a;
--sd-vscode-sb-thumb-color-focus: #4b4d50;
`
: undefined;
/** @type {import('svelte/store').Readable<EncodeableData>} */
$: debugData = assertStore(data, raw) ? data : readable(data);
</script>
{#if !styleInit}
<style>
.super-debug--absolute {
position: absolute;
}
.super-debug--top-0 {
top: 0;
}
.super-debug--inset-x-0 {
left: 0px;
right: 0px;
}
.super-debug--hidden {
height: 0;
overflow: hidden;
}
.super-debug--hidden:not(.super-debug--with-label) {
height: 1.5em;
}
.super-debug--rotated {
transform: rotate(180deg);
}
.super-debug {
--_sd-bg-color: var(--sd-bg-color, var(--sd-vscode-bg-color, rgb(30, 41, 59)));
position: relative;
background-color: var(--_sd-bg-color);
border-radius: 0.5rem;
overflow: hidden;
}
.super-debug--pre {
overflow-x: auto;
}
.super-debug--collapse {
display: block;
width: 100%;
color: rgba(255, 255, 255, 0.25);
background-color: rgba(255, 255, 255, 0.15);
padding: 5px 0;
display: flex;
justify-content: center;
border-color: transparent;
margin: 0;
padding: 3px 0;
}
.super-debug--collapse:focus {
color: #fafafa;
background-color: rgba(255, 255, 255, 0.25);
}
.super-debug--collapse:is(:hover) {
color: rgba(255, 255, 255, 0.35);
background-color: rgba(255, 255, 255, 0.25);
}
.super-debug--status {
display: flex;
padding: 1em;
padding-bottom: 0;
justify-content: space-between;
font-family:
Inconsolata, Monaco, Consolas, 'Lucida Console', 'Courier New', Courier, monospace;
}
.super-debug--right-status {
display: flex;
gap: 0.55em;
}
.super-debug--copy {
margin: 0;
padding: 0;
padding-top: 2px;
background-color: transparent;
border: 0;
color: #666;
cursor: pointer;
}
.super-debug--copy:hover {
background-color: transparent;
color: #666;
}
.super-debug--copy:focus {
background-color: transparent;
color: #666;
}
.super-debug--label {
color: var(--sd-label-color, var(--sd-vscode-label-color, white));
}
.super-debug--promise-loading {
color: var(--sd-promise-loading-color, var(--sd-vscode-promise-loading-color, #999));
}
.super-debug--promise-rejected {
color: var(--sd-promise-rejected-color, var(--sd-vscode-promise-rejected-color, #ff475d));
}
.super-debug pre {
color: var(--sd-code-default, var(--sd-vscode-code-default, #999));
background-color: var(--_sd-bg-color);
font-size: 1em;
margin-bottom: 0;
padding: 1em 0 1em 1em;
}
.super-debug--info {
color: var(--sd-info, var(--sd-vscode-info, rgb(85, 85, 255)));
}
.super-debug--success {
color: var(--sd-success, var(--sd-vscode-success, #2cd212));
}
.super-debug--redirect {
color: var(--sd-redirect, var(--sd-vscode-redirect, #03cae5));
}
.super-debug--error {
color: var(--sd-error, var(--sd-vscode-error, #ff475d));
}
.super-debug--code .key {
color: var(--sd-code-key, var(--sd-vscode-code-key, #eab308));
}
.super-debug--code .string {
color: var(--sd-code-string, var(--sd-vscode-code-string, #6ec687));
}
.super-debug--code .date {
color: var(--sd-code-date, var(--sd-vscode-code-date, #f06962));
}
.super-debug--code .boolean {
color: var(--sd-code-boolean, var(--sd-vscode-code-boolean, #79b8ff));
}
.super-debug--code .number {
color: var(--sd-code-number, var(--sd-vscode-code-number, #af77e9));
}
.super-debug--code .bigint {
color: var(--sd-code-bigint, var(--sd-vscode-code-bigint, #af77e9));
}
.super-debug--code .null {
color: var(--sd-code-null, var(--sd-vscode-code-null, #238afe));
}
.super-debug--code .nan {
color: var(--sd-code-nan, var(--sd-vscode-code-nan, #af77e9));
}
.super-debug--code .undefined {
color: var(--sd-code-undefined, var(--sd-vscode-code-undefined, #238afe));
}
.super-debug--code .function {
color: var(--sd-code-function, var(--sd-vscode-code-function, #f06962));
}
.super-debug--code .symbol {
color: var(--sd-code-symbol, var(--sd-vscode-code-symbol, #4de0c5));
}
.super-debug--code .error {
color: var(--sd-code-error, var(--sd-vscode-code-error, #ff475d));
}
.super-debug pre::-webkit-scrollbar {
width: var(--sd-sb-width, var(--sd-vscode-sb-width, 1rem));
height: var(--sd-sb-height, var(--sd-vscode-sb-height, 1rem));
}
.super-debug pre::-webkit-scrollbar-track {
border-radius: 12px;
background-color: var(
--sd-sb-track-color,
var(--sd-vscode-sb-track-color, hsl(0, 0%, 40%, 0.2))
);
}
.super-debug:is(:focus-within, :hover) pre::-webkit-scrollbar-track {
border-radius: 12px;
background-color: var(
--sd-sb-track-color-focus,
var(--sd-vscode-sb-track-color-focus, hsl(0, 0%, 50%, 0.2))
);
}
.super-debug pre::-webkit-scrollbar-thumb {
border-radius: 12px;
background-color: var(
--sd-sb-thumb-color,
var(--sd-vscode-sb-thumb-color, hsl(217, 50%, 50%, 0.5))
);
}
.super-debug:is(:focus-within, :hover) pre::-webkit-scrollbar-thumb {
border-radius: 12px;
background-color: var(
--sd-sb-thumb-color-focus,
var(--sd-vscode-sb-thumb-color-focus, hsl(217, 50%, 50%))
);
}
</style>
<!-- eslint-disable-next-line @typescript-eslint/no-unused-vars -->
{@const init = styleInit = true}
{/if}
<!-- eslint-disable svelte/no-at-html-tags -->
{#if display}
<div
class="super-debug"
class:super-debug--collapsible={collapsible}
style={themeStyle}
dir="ltr"
>
<div
class="super-debug--status {label === ''
? 'super-debug--absolute super-debug--inset-x-0 super-debug--top-0'
: ''}"
>
<div class="super-debug--label">{label}</div>
<div class="super-debug--right-status">
<button type="button" class="super-debug--copy" on:click={copyContent}>
{#if !copied}
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"
><g
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
><path
d="M7 9.667A2.667 2.667 0 0 1 9.667 7h8.666A2.667 2.667 0 0 1 21 9.667v8.666A2.667 2.667 0 0 1 18.333 21H9.667A2.667 2.667 0 0 1 7 18.333z"
/><path
d="M4.012 16.737A2.005 2.005 0 0 1 3 15V5c0-1.1.9-2 2-2h10c.75 0 1.158.385 1.5 1"
/></g
></svg
>
{:else}
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"
><g
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
><path d="M15 12v6m-3-3h6" /><rect
width="14"
height="14"
x="8"
y="8"
rx="2"
ry="2"
/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" /></g
></svg
>
{/if}
</button>
{#if status}
<div
class:super-debug--info={$page.status < 200}
class:super-debug--success={$page.status >= 200 && $page.status < 300}
class:super-debug--redirect={$page.status >= 300 && $page.status < 400}
class:super-debug--error={$page.status >= 400}
>
{$page.status}
</div>
{/if}
</div>
</div>
<pre
class="super-debug--pre"
class:super-debug--with-label={label}
class:super-debug--hidden={collapsed}
bind:this={ref}><code class="super-debug--code"
><slot
>{#if assertPromise($debugData, raw, promise)}{#await $debugData}<div
class="super-debug--promise-loading">Loading data...</div>{:then result}{@html syntaxHighlight(
assertStore(result, raw) ? get(result) : result
)}{:catch error}<span class="super-debug--promise-rejected">Rejected:</span
> {@html syntaxHighlight(error)}{/await}{:else}{@html syntaxHighlight(
$debugData
)}{/if}</slot
></code
></pre>
{#if collapsible}
<button
type="button"
on:click|preventDefault={() => setCollapse(!collapsed)}
class="super-debug--collapse"
aria-label="Collapse"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
class:super-debug--rotated={collapsed}
><path
fill="currentColor"
d="M4.08 11.92L12 4l7.92 7.92l-1.42 1.41l-5.5-5.5V22h-2V7.83l-5.5 5.5l-1.42-1.41M12 4h10V2H2v2h10Z"
/></svg
>
</button>
{/if}
</div>
{/if}
<!--
@component
SuperDebug is a debugging component that gives you colorized and nicely formatted output for any data structure, usually $form.
Other use cases includes debugging plain objects, promises, stores and more.
More info: https://superforms.rocks/super-debug
**Short example:**
```svelte
<script>
import SuperDebug from 'sveltekit-superforms';
import { superForm } from 'sveltekit-superforms';
export let data;
const { errors, form, enhance } = superForm(data.form);
</script>
<SuperDebug data={$form} label="My form data" />
```
-->