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
This commit is contained in:
2026-02-17 16:19:59 -05:00
parent 54df6018f5
commit de2d83092e
28274 changed files with 3816354 additions and 90 deletions

View File

@@ -0,0 +1,157 @@
import Event from '../../event/Event.js';
import * as PropertySymbol from '../../PropertySymbol.js';
import BrowserWindow from '../../window/BrowserWindow.js';
import Document from '../document/Document.js';
import HTMLElement from '../html-element/HTMLElement.js';
import CrossOriginBrowserWindow from '../../window/CrossOriginBrowserWindow.js';
import DOMTokenList from '../../dom/DOMTokenList.js';
import Attr from '../attr/Attr.js';
/**
* HTML Iframe Element.
*
* Reference:
* https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameElement.
*/
export default class HTMLIFrameElement extends HTMLElement {
#private;
cloneNode: (deep?: boolean) => HTMLIFrameElement;
[PropertySymbol.sandbox]: DOMTokenList | null;
get onload(): ((event: Event) => void) | null;
set onload(value: ((event: Event) => void) | null);
get onerror(): ((event: Event) => void) | null;
set onerror(value: ((event: Event) => void) | null);
/**
* Returns source.
*
* @returns Source.
*/
get src(): string;
/**
* Sets source.
*
* @param src Source.
*/
set src(src: string);
/**
* Returns allow.
*
* @returns Allow.
*/
get allow(): string;
/**
* Sets allow.
*
* @param allow Allow.
*/
set allow(allow: string);
/**
* Returns height.
*
* @returns Height.
*/
get height(): string;
/**
* Sets height.
*
* @param height Height.
*/
set height(height: string);
/**
* Returns width.
*
* @returns Width.
*/
get width(): string;
/**
* Sets width.
*
* @param width Width.
*/
set width(width: string);
/**
* Returns name.
*
* @returns Name.
*/
get name(): string;
/**
* Sets name.
*
* @param name Name.
*/
set name(name: string);
/**
* Returns sandbox.
*
* @returns Sandbox.
*/
get sandbox(): DOMTokenList;
/**
* Sets sandbox.
*/
set sandbox(sandbox: string);
/**
* Returns srcdoc.
*
* @returns Srcdoc.
*/
get srcdoc(): string;
/**
* Sets srcdoc.
*
* @param srcdoc Srcdoc.
*/
set srcdoc(srcdoc: string);
/**
* Returns referrer policy.
*/
get referrerPolicy(): string;
/**
* Sets referrer policy.
*
* @param referrerPolicy Referrer policy.
*/
set referrerPolicy(referrerPolicy: string);
/**
* Returns content document.
*
* @returns Content document.
*/
get contentDocument(): Document | null;
/**
* Returns content window.
*
* @returns Content window.
*/
get contentWindow(): BrowserWindow | CrossOriginBrowserWindow | null;
/**
* @override
*/
get tabIndex(): number;
/**
* @override
*/
set tabIndex(tabIndex: number);
/**
* @override
*/
[PropertySymbol.connectedToDocument](): void;
/**
* Called when disconnected from document.
* @param e
*/
[PropertySymbol.disconnectedFromDocument](): void;
/**
* @override
*/
[PropertySymbol.cloneNode](deep?: boolean): HTMLIFrameElement;
/**
* @override
*/
[PropertySymbol.onSetAttribute](attribute: Attr, replacedAttribute: Attr | null): void;
/**
* @override
*/
[PropertySymbol.onRemoveAttribute](removedAttribute: Attr): void;
}
//# sourceMappingURL=HTMLIFrameElement.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"HTMLIFrameElement.d.ts","sourceRoot":"","sources":["../../../src/nodes/html-iframe-element/HTMLIFrameElement.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,370 @@
import Event from '../../event/Event.js';
import * as PropertySymbol from '../../PropertySymbol.js';
import HTMLElement from '../html-element/HTMLElement.js';
import CrossOriginBrowserWindow from '../../window/CrossOriginBrowserWindow.js';
import DOMTokenList from '../../dom/DOMTokenList.js';
import BrowserFrameFactory from '../../browser/utilities/BrowserFrameFactory.js';
import BrowserFrameURL from '../../browser/utilities/BrowserFrameURL.js';
import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum.js';
import WindowBrowserContext from '../../window/WindowBrowserContext.js';
import ElementEventAttributeUtility from '../element/ElementEventAttributeUtility.js';
const SANDBOX_FLAGS = [
'allow-downloads',
'allow-forms',
'allow-modals',
'allow-orientation-lock',
'allow-pointer-lock',
'allow-popups',
'allow-popups-to-escape-sandbox',
'allow-presentation',
'allow-same-origin',
'allow-scripts',
'allow-top-navigation',
'allow-top-navigation-by-user-activation',
'allow-top-navigation-to-custom-protocols'
];
/**
* HTML Iframe Element.
*
* Reference:
* https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameElement.
*/
export default class HTMLIFrameElement extends HTMLElement {
// Internal properties
[PropertySymbol.sandbox] = null;
// Private properties
#contentWindowContainer = {
window: null
};
#iframe;
#loadedSrcdoc = null;
// Events
/* eslint-disable jsdoc/require-jsdoc */
get onload() {
return ElementEventAttributeUtility.getEventListener(this, 'onload');
}
set onload(value) {
this[PropertySymbol.propertyEventListeners].set('onload', value);
}
get onerror() {
return ElementEventAttributeUtility.getEventListener(this, 'onerror');
}
set onerror(value) {
this[PropertySymbol.propertyEventListeners].set('onerror', value);
}
/* eslint-enable jsdoc/require-jsdoc */
/**
* Returns source.
*
* @returns Source.
*/
get src() {
if (!this.hasAttribute('src')) {
return '';
}
try {
return new URL(this.getAttribute('src'), this[PropertySymbol.ownerDocument].location.href)
.href;
}
catch (e) {
return this.getAttribute('src');
}
}
/**
* Sets source.
*
* @param src Source.
*/
set src(src) {
this.setAttribute('src', src);
}
/**
* Returns allow.
*
* @returns Allow.
*/
get allow() {
return this.getAttribute('allow') || '';
}
/**
* Sets allow.
*
* @param allow Allow.
*/
set allow(allow) {
this.setAttribute('allow', allow);
}
/**
* Returns height.
*
* @returns Height.
*/
get height() {
return this.getAttribute('height') || '';
}
/**
* Sets height.
*
* @param height Height.
*/
set height(height) {
this.setAttribute('height', height);
}
/**
* Returns width.
*
* @returns Width.
*/
get width() {
return this.getAttribute('width') || '';
}
/**
* Sets width.
*
* @param width Width.
*/
set width(width) {
this.setAttribute('width', width);
}
/**
* Returns name.
*
* @returns Name.
*/
get name() {
return this.getAttribute('name') || '';
}
/**
* Sets name.
*
* @param name Name.
*/
set name(name) {
this.setAttribute('name', name);
}
/**
* Returns sandbox.
*
* @returns Sandbox.
*/
get sandbox() {
if (!this[PropertySymbol.sandbox]) {
this[PropertySymbol.sandbox] = new DOMTokenList(PropertySymbol.illegalConstructor, this, 'sandbox');
}
return this[PropertySymbol.sandbox];
}
/**
* Sets sandbox.
*/
set sandbox(sandbox) {
this.setAttribute('sandbox', sandbox);
}
/**
* Returns srcdoc.
*
* @returns Srcdoc.
*/
get srcdoc() {
return this.getAttribute('srcdoc') || '';
}
/**
* Sets srcdoc.
*
* @param srcdoc Srcdoc.
*/
set srcdoc(srcdoc) {
this.setAttribute('srcdoc', srcdoc);
}
/**
* Returns referrer policy.
*/
get referrerPolicy() {
return this.getAttribute('referrerpolicy') || '';
}
/**
* Sets referrer policy.
*
* @param referrerPolicy Referrer policy.
*/
set referrerPolicy(referrerPolicy) {
this.setAttribute('referrerpolicy', referrerPolicy);
}
/**
* Returns content document.
*
* @returns Content document.
*/
get contentDocument() {
return this.#contentWindowContainer.window?.document ?? null;
}
/**
* Returns content window.
*
* @returns Content window.
*/
get contentWindow() {
return this.#contentWindowContainer.window;
}
/**
* @override
*/
get tabIndex() {
const tabIndex = this.getAttribute('tabindex');
if (tabIndex !== null) {
const parsed = Number(tabIndex);
return isNaN(parsed) ? 0 : parsed;
}
return 0;
}
/**
* @override
*/
set tabIndex(tabIndex) {
super.tabIndex = tabIndex;
}
/**
* @override
*/
[PropertySymbol.connectedToDocument]() {
super[PropertySymbol.connectedToDocument]();
this.#loadPage();
}
/**
* Called when disconnected from document.
* @param e
*/
[PropertySymbol.disconnectedFromDocument]() {
super[PropertySymbol.disconnectedFromDocument]();
this.#unloadPage();
}
/**
* @override
*/
[PropertySymbol.cloneNode](deep = false) {
return super[PropertySymbol.cloneNode](deep);
}
/**
* @override
*/
[PropertySymbol.onSetAttribute](attribute, replacedAttribute) {
super[PropertySymbol.onSetAttribute](attribute, replacedAttribute);
if (attribute[PropertySymbol.name] === 'srcdoc') {
this.#loadPage();
}
if (attribute[PropertySymbol.name] === 'src' &&
attribute[PropertySymbol.value] &&
!this.hasAttribute('srcdoc') &&
attribute[PropertySymbol.value] !== replacedAttribute?.[PropertySymbol.value]) {
this.#loadPage();
}
if (attribute[PropertySymbol.name] === 'sandbox') {
this.#validateSandboxFlags();
}
}
/**
* @override
*/
[PropertySymbol.onRemoveAttribute](removedAttribute) {
super[PropertySymbol.onRemoveAttribute](removedAttribute);
if (removedAttribute[PropertySymbol.name] === 'srcdoc' ||
removedAttribute[PropertySymbol.name] === 'src') {
this.#loadPage();
}
}
/**
*
* @param tokens
* @param vconsole
*/
#validateSandboxFlags() {
const window = this[PropertySymbol.window];
const invalidFlags = [];
for (const token of this.sandbox) {
if (!SANDBOX_FLAGS.includes(token)) {
invalidFlags.push(token);
}
}
if (invalidFlags.length === 1) {
window.console.error(`Error while parsing the 'sandbox' attribute: '${invalidFlags[0]}' is an invalid sandbox flag.`);
}
else if (invalidFlags.length > 1) {
window.console.error(`Error while parsing the 'sandbox' attribute: '${invalidFlags.join(`', '`)}' are invalid sandbox flags.`);
}
}
/**
* Loads an iframe page.
*/
#loadPage() {
if (!this[PropertySymbol.isConnected]) {
this.#unloadPage();
return;
}
const srcdoc = this.getAttribute('srcdoc');
const window = this[PropertySymbol.window];
const browserFrame = new WindowBrowserContext(window).getBrowserFrame();
if (!browserFrame) {
return;
}
if (srcdoc !== null) {
if (this.#loadedSrcdoc === srcdoc) {
return;
}
this.#unloadPage();
this.#iframe = BrowserFrameFactory.createChildFrame(browserFrame);
this.#iframe.url = 'about:srcdoc';
this.#contentWindowContainer.window = this.#iframe.window;
this.#iframe.window[PropertySymbol.top] = browserFrame.window.top;
this.#iframe.window[PropertySymbol.parent] = browserFrame.window;
this.#iframe.window.document.open();
this.#iframe.window.document.write(srcdoc);
this.#loadedSrcdoc = srcdoc;
this[PropertySymbol.window].requestAnimationFrame(() => this.dispatchEvent(new Event('load')));
return;
}
if (this.#loadedSrcdoc !== null) {
this.#unloadPage();
}
const originURL = browserFrame.window.location;
const targetURL = BrowserFrameURL.getRelativeURL(browserFrame, this.src);
if (this.#iframe && this.#iframe.window.location.href === targetURL.href) {
return;
}
if (browserFrame.page.context.browser.settings.disableIframePageLoading) {
const error = new window.DOMException(`Failed to load iframe page "${targetURL.href}". Iframe page loading is disabled.`, DOMExceptionNameEnum.notSupportedError);
browserFrame.page?.console.error(error);
this.dispatchEvent(new Event('error'));
return;
}
// Iframes has a special rule for CORS and doesn't allow access between frames when the origin is different.
const isSameOrigin = originURL.origin === targetURL.origin || targetURL.origin === 'null';
const parentWindow = isSameOrigin ? window : new CrossOriginBrowserWindow(window);
this.#iframe = this.#iframe ?? BrowserFrameFactory.createChildFrame(browserFrame);
this.#iframe.window[PropertySymbol.top] = parentWindow;
this.#iframe.window[PropertySymbol.parent] = parentWindow;
this.#iframe
.goto(targetURL.href, {
referrer: originURL.origin,
referrerPolicy: this.referrerPolicy
})
.then(() => this.dispatchEvent(new Event('load')))
.catch((error) => {
browserFrame.page?.console.error(error);
this.dispatchEvent(new Event('error'));
});
this.#contentWindowContainer.window = isSameOrigin
? this.#iframe.window
: new CrossOriginBrowserWindow(this.#iframe.window, window);
}
/**
* Unloads an iframe page.
*/
#unloadPage() {
if (this.#iframe) {
BrowserFrameFactory.destroyFrame(this.#iframe);
this.#iframe = null;
}
this.#contentWindowContainer.window = null;
this.#loadedSrcdoc = null;
}
}
//# sourceMappingURL=HTMLIFrameElement.js.map

File diff suppressed because one or more lines are too long