WindowATM
A desktop-style window: a title bar with an icon and a title, minimize / maximize / close on the right, dragged by its bar and raised to the front when you press it - several on one page. It is a non-modal <dialog>, so opening, closing and the close event are the browser's, the × is a <form method="dialog"> button that works without script, and resizing is native CSS. Windows, macOS, Linux and retro chrome; df$.shadcn.win creates and arranges windows from script.
On this page (10)
§Window
Drag the title bar to move it (it never leaves the desktop for good), double-click it to maximize, − rolls it up to its bar, □ fills the desktop, × closes it - a native <form method='dialog'>. The corner handle resizes (CSS resize). Focus the title bar and use the arrow keys to move it by keyboard.
§Several windows
Press any window - its bar, its content - and it comes to the front; the others dim their title (data-active marks the front one). Stacking is shared across the page.
§Chrome
data-chrome: windows (the default - wide controls, a red × on hover), mac (traffic lights on the left, glyphs on hover, centred title), linux (GNOME round buttons, centred title) and retro (a bevelled 9x window). Same markup, same behaviour.
§Driven from script
df$.shadcn.win.create() builds a window (title, icon, content, position, size, chrome) in the first desktop; cascade() and tile() arrange the open ones; close() takes an element, id or selector. The log reads window-focus, window-move and close.
§Window states
The State API per window: default (open, normal size - also reopens a closed one), maximized, minimized (rolled up to the bar) and closed. The × and close() land in closed too - the state follows the native dialog.
§A desktop app: an editor with its toolbar
The editor window carries its app toolbar under the title bar (.window-toolbar): a ghost menubar - File, Edit, Window, Help - and tool buttons, all driving the windows through df$.shadcn.win (Window → Tile / Cascade / Maximize, Help → About opens a retro window). The editor opens on the left, the Preview on the right; the editor is a textarea over a Shiki-highlighted copy (loaded from esm.sh, like these docs), re-highlighted as you type; the status bar follows the caret and the Preview window renders the page live.
§States
Named states via the shared State API, driven per window through the bound api:
default- open at its normal size; reopens a closed window,{ x, y }moves itmaximized- fills the desktopminimized- rolled up to its title barclosed- the dialog is closed; also entered when the × orclose()closes it
The first example carries data-state-demo; bun run screenshots drives it via el.api.setState(name) and captures screenshots/{mode}/window-{state}.png.
Machine contract - verified against window.schema.json by bun run verify:
| State | Type | Values | Default | Description |
|---|---|---|---|---|
maximized | boolean | true, false | false | Fills the desktop. |
minimized | boolean | true, false | false | Rolled up to its title bar. |
closed | boolean | true, false | false | The dialog is closed - also by the × or close(). |
§API
Generated from window.ts and the shared State API - the descriptions are their JSDoc, the types are checked by the compiler.
States
type WindowState = 'default' | 'maximized' | 'minimized' | 'closed' - setState(name, config) takes the config of the state it names.
| State | Description | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|
default | Open at its normal size (opens a closed window).
| |||||||||
maximized | Fills the desktop.
| |||||||||
minimized | Rolled up to its title bar.
| |||||||||
closed | Closed - also after the close button or close(); it reopens at its normal size. No config. |
Every element
| Member | Description | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|
el.api.setState<S extends WindowState>(name: S, config?: WindowStateConfigs[S]): unknown | Enter a state: the DOM work runs (also when it is the current state), the store records it.
Returns | |||||||||
el.api.getState(): { name: WindowState; config: WindowStateConfigs[WindowState]; model?: ElementModel } | The state the element shows now - read back from the DOM, so it includes what the user changed. Returns | |||||||||
el.api.render(state?: { name: WindowState; config: WindowStateConfigs[WindowState]; model?: ElementModel }): string | The element's markup in a state - the authored markup with that state applied; a pure function of the state.
Returns | |||||||||
el.api.settled(): Promise<void> | Wait for the last state's DOM work (async states: a diagram rendering, a chart mounting). Returns | |||||||||
el.store: Store<{ name: WindowState; config: WindowStateConfigs[WindowState] }> | A defuss-store store of the element's state - subscribe to follow every change (also the user's), set it to drive the component. |
Registry
| Member | Description | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
df$.shadcn.windowApi.setState<S extends WindowState>(el: HTMLElement, name: S, config?: WindowStateConfigs[S]): unknown | Enter a state: the DOM work runs (also when it is the current state), the store records it.
Returns | ||||||||||||
df$.shadcn.windowApi.getState(el: HTMLElement): { name: WindowState; config: WindowStateConfigs[WindowState]; model?: ElementModel } | The state the element shows now - read back from the DOM, so it includes what the user changed.
Returns | ||||||||||||
df$.shadcn.windowApi.render(state: { name: WindowState; config: WindowStateConfigs[WindowState]; model?: ElementModel }): string | The element's markup in a state - the authored markup with that state applied; a pure function of the state.
Returns | ||||||||||||
df$.shadcn.windowApi.store(el: HTMLElement): Store<{ name: WindowState; config: WindowStateConfigs[WindowState] }> | The element's store (bindComponent made it).
Returns | ||||||||||||
df$.shadcn.windowApi.commit<S extends WindowState>(el: HTMLElement, name: S, config?: WindowStateConfigs[S]): void | Record a state the element reached on its own (no DOM work) - for a component's own handlers.
| ||||||||||||
df$.shadcn.windowStates: WindowState[] | The declared states, 'default' first: default, maximized, minimized, closed. |
df$.shadcn.win
| Member | Description | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
create(options: WindowCreateOptions = {}): HTMLDialogElement | Builds a window element from options - the shape the skill documents.
Returns | ||||||||||||
open(target: string | HTMLElement, config: { x?: number; y?: number } = {}): HTMLDialogElement | null | Open a window (closed, minimized or not shown yet) - config is the state's config.
Returns | ||||||||||||
close(target: string | HTMLElement): HTMLDialogElement | null | Close it (the closed state).
Returns | ||||||||||||
focus(target: string | HTMLElement): HTMLDialogElement | null | Bring it to the front (the active window).
Returns | ||||||||||||
move(target: string | HTMLElement, x: number, y: number): WindowPosition | null | Move it to x, y (px, inside its desktop).
Returns | ||||||||||||
resize(target: string | HTMLElement, width: number | string, height?: number | string): HTMLDialogElement | null | Size it: width (and height) as px numbers or CSS lengths.
Returns | ||||||||||||
maximize(target: string | HTMLElement): HTMLDialogElement | null | Fill the desktop.
Returns | ||||||||||||
minimize(target: string | HTMLElement): HTMLDialogElement | null | Minimize it to the taskbar.
Returns | ||||||||||||
restore(target: string | HTMLElement): HTMLDialogElement | null | Back to its normal size and place.
Returns | ||||||||||||
toggleMaximize(target: string | HTMLElement): HTMLDialogElement | null | Maximize it, or restore it when it is maximized.
Returns | ||||||||||||
active(): HTMLDialogElement | undefined | The window in front. Returns | ||||||||||||
list(scope?: HTMLElement | string, all: boolean = false): HTMLDialogElement[] | Windows (open unless `all`) inside `scope` (default: the page).
Returns | ||||||||||||
cascade(scope?: HTMLElement | string, step: number = 28): void | Steps the open windows diagonally from the top-left, front-most last.
| ||||||||||||
tile(scope?: HTMLElement | string): void | Lays the open windows side by side in a grid that fills their desktop.
|
Events
| Event | Description | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|
window-focus | Fires when a window comes to the front - its title.
| |||||||||
window-move | Fires after a window was dragged (or moved with the keyboard) - its position.
|
Types
| Type | Description | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
WindowCreateOptions | What create() takes - the shape the skill documents.
| ||||||||||||||||||||||||||||||||||||||||||||||||
WindowFocusDetail | What window-focus carries.
| ||||||||||||||||||||||||||||||||||||||||||||||||
WindowPosition | A window's place inside its desktop, px.
|
§CSS view file
/* -- Window component -------------------------------------------- A desktop-style window: a non-modal <dialog> with a title bar (icon, title, minimize / maximize / close), moved by dragging the bar, raised to the front on click, resized natively. Chrome styles after Windows, macOS, Linux (GNOME) and a retro 9x look. The controls are a <form method="dialog">, so the × closes the window without script. */@layer components { /* the area windows live in: positioned, clipped - a desktop */ .window-desktop { position: relative; overflow: hidden; min-height: 18rem; border-radius: var(--radius-lg); background: radial-gradient(circle at 1px 1px, color-mix(in oklch, var(--foreground) 12%, transparent) 1px, transparent 0) 0 0 / 1.25rem 1.25rem, var(--muted); isolation: isolate; } .window { /* the UA dialog defaults (centered, padded, bordered) - replaced */ --_titlebar: 2.25rem; --_ctl: 2.75rem; position: absolute; inset: auto; left: var(--window-x, 2rem); top: var(--window-y, 2rem); box-sizing: border-box; width: var(--window-w, 24rem); height: var(--window-h, auto); max-width: none; max-height: none; min-width: 12rem; margin: 0; padding: 0; display: none; flex-direction: column; border: 1px solid var(--border); border-radius: var(--radius-lg); background: var(--card); color: var(--card-foreground); font-family: var(--font-sans); font-size: 0.875rem; box-shadow: var(--shadow-lg); overflow: hidden; transition: box-shadow 150ms; &[open] { display: flex; } /* native resize handle, bottom-right */ &[data-resizable] { resize: both; min-height: calc(var(--_titlebar) + 4rem); } /* -- Title bar ---------------------------------------------------- */ & > .window-titlebar { display: flex; align-items: center; gap: 0.5rem; flex: none; height: var(--_titlebar); padding-inline-start: 0.75rem; background: var(--muted); border-bottom: 1px solid var(--border); cursor: default; user-select: none; touch-action: none; &:focus-visible { outline: 2px solid var(--ring); outline-offset: -2px; } } & .window-icon { flex: none; width: 1rem; height: 1rem; object-fit: contain; color: var(--muted-foreground); } & .window-title { flex: 1; min-width: 0; margin: 0; overflow: hidden; font: inherit; font-size: 0.8125rem; font-weight: 500; white-space: nowrap; text-overflow: ellipsis; } /* -- Controls: CSS-drawn glyphs --------------------------------------- */ & .window-controls { display: flex; align-self: stretch; flex: none; margin: 0; } & .window-controls > button { position: relative; box-sizing: border-box; width: var(--_ctl); height: 100%; margin: 0; padding: 0; border: 0; background: transparent; color: var(--foreground); cursor: default; transition: background-color 120ms, color 120ms; &::before, &::after { content: ""; position: absolute; inset: 50% auto auto 50%; translate: -50% -50%; box-sizing: border-box; } &:hover { background: color-mix(in oklch, var(--foreground) 10%, transparent); } &:focus-visible { outline: 2px solid var(--ring); outline-offset: -2px; } } /* minimize: a bar */ & .window-minimize::before { width: 0.625rem; height: 1px; background: currentColor; } /* maximize: a box; restore (while maximized): two boxes */ & .window-maximize::before { width: 0.625rem; height: 0.625rem; border: 1px solid currentColor; border-radius: 1px; } &[data-maximized] .window-maximize { &::before { translate: -40% -60%; width: 0.5rem; height: 0.5rem; } &::after { translate: -65% -35%; width: 0.5rem; height: 0.5rem; border: 1px solid currentColor; border-radius: 1px; background: var(--muted); } } /* close: an × */ & .window-close { &::before, &::after { width: 0.75rem; height: 1px; background: currentColor; rotate: 45deg; } &::after { rotate: -45deg; } } /* Windows chrome: the × turns red with a white glyph. Stated through .window-controls > so it outranks the shared control hover (a light tint) - otherwise the white glyph landed on light grey. The other chromes style their own close button. */ &:not([data-chrome="mac"], [data-chrome="linux"], [data-chrome="retro"]) .window-controls > .window-close:hover { background: oklch(0.58 0.21 27); color: oklch(0.99 0 0); } /* -- Toolbar: an app's menubar / buttons under the title bar -------- */ & > .window-toolbar { display: flex; align-items: center; gap: 0.25rem; flex: none; min-height: 2.25rem; padding: 0.125rem 0.375rem; border-bottom: 1px solid var(--border); background: var(--background); overflow-x: auto; } /* -- Body / status bar -------------------------------------------- */ & > .window-body { flex: 1; min-height: 0; padding: 1rem; overflow: auto; overscroll-behavior: contain; } & > .window-body[data-flush] { padding: 0; } & > .window-statusbar { display: flex; align-items: center; gap: 1rem; flex: none; padding: 0.25rem 0.75rem; border-top: 1px solid var(--border); background: var(--muted); color: var(--muted-foreground); font-size: 0.75rem; } /* -- Active / inactive ---------------------------------------------- */ &:not([data-active]) { box-shadow: var(--shadow-sm); & > .window-titlebar { color: var(--muted-foreground); } } /* -- Maximized: fill the desktop; minimized: rolled up to the bar -- */ &[data-maximized] { inset: 0; width: auto; height: auto; border-radius: 0; border-width: 0; resize: none; box-shadow: none; } &[data-minimized] { height: auto; min-height: 0; resize: none; & > :not(.window-titlebar) { display: none; } & > .window-titlebar { border-bottom: 0; } } &[data-dragging] { transition: none; } /* -- Chrome: macOS - traffic lights on the left, centred title ------- */ &[data-chrome="mac"] { --_ctl: 1.25rem; & > .window-titlebar { padding-inline: 0.75rem; } & .window-controls { order: -1; align-self: center; gap: 0.5rem; margin-inline-end: 0.25rem; } & .window-controls > button { width: 0.75rem; height: 0.75rem; border-radius: 999px; color: oklch(0.3 0.05 30 / 0); &:hover { background: var(--_light); } &::before, &::after { scale: 0.7; } } /* the glyphs appear when the pointer is over the lights */ & .window-controls:hover > button { color: oklch(0.3 0.05 30 / 0.8); } & .window-close { order: 1; --_light: oklch(0.68 0.2 25); } & .window-minimize { order: 2; --_light: oklch(0.82 0.16 85); } & .window-maximize { order: 3; --_light: oklch(0.74 0.17 145); } & .window-controls > button { background: var(--_light); } & .window-close:hover { color: oklch(0.3 0.05 30 / 0.8); } & .window-title { text-align: center; } /* the icon sits beside the centred title */ & .window-icon { order: 1; } & .window-title { order: 0; } &[data-maximized] .window-maximize::after { background: var(--_light); } &:not([data-active]) .window-controls:not(:hover) > button { background: color-mix(in oklch, var(--muted-foreground) 35%, transparent); } } /* -- Chrome: Linux (GNOME) - round grey buttons on the right --------- */ &[data-chrome="linux"] { --_ctl: 1.5rem; & > .window-titlebar { padding-inline-end: 0.5rem; } & .window-title { text-align: center; font-weight: 600; } & .window-controls { align-self: center; gap: 0.5rem; } & .window-controls > button { width: var(--_ctl); height: var(--_ctl); border-radius: 999px; background: color-mix(in oklch, var(--foreground) 9%, transparent); &::before, &::after { scale: 0.8; } &:hover { background: color-mix(in oklch, var(--foreground) 16%, transparent); } } & .window-close:hover { background: color-mix(in oklch, var(--foreground) 16%, transparent); color: var(--foreground); } &[data-maximized] .window-maximize::after { background: transparent; } } /* -- Chrome: retro - a bevelled 9x window with a gradient title ------ */ &[data-chrome="retro"] { --_face: oklch(0.8 0 0); --_hi: oklch(1 0 0); --_lo: oklch(0.5 0 0); --_ctl: 1.125rem; padding: 3px; border: 0; border-radius: 0; background: var(--_face); color: oklch(0.15 0 0); box-shadow: inset -1px -1px 0 oklch(0.1 0 0), inset 1px 1px 0 var(--_face), inset -2px -2px 0 var(--_lo), inset 2px 2px 0 var(--_hi); & > .window-titlebar { height: 1.375rem; padding-inline: 0.25rem 0.125rem; gap: 0.25rem; border: 0; background: linear-gradient(90deg, oklch(0.3 0.16 264), oklch(0.62 0.12 240)); color: oklch(1 0 0); } & .window-icon { color: inherit; } & .window-title { font-weight: 700; font-size: 0.75rem; } & .window-controls { align-self: center; gap: 2px; } & .window-controls > button { width: calc(var(--_ctl) + 2px); height: var(--_ctl); background: var(--_face); color: oklch(0.1 0 0); box-shadow: inset -1px -1px 0 oklch(0.1 0 0), inset 1px 1px 0 var(--_hi), inset -2px -2px 0 var(--_lo); &:hover { background: var(--_face); color: oklch(0.1 0 0); } &:active { box-shadow: inset 1px 1px 0 oklch(0.1 0 0), inset -1px -1px 0 var(--_hi), inset 2px 2px 0 var(--_lo); } &::before, &::after { scale: 0.75; } } & .window-close { margin-inline-start: 2px; } & .window-minimize::before { height: 2px; translate: -50% 150%; } & .window-maximize::before { border-top-width: 2px; } & .window-close::before, & .window-close::after { height: 2px; } &[data-maximized] .window-maximize::after { background: var(--_face); border-top-width: 2px; } & > .window-body { margin-top: 2px; background: oklch(1 0 0); box-shadow: inset 1px 1px 0 var(--_lo), inset -1px -1px 0 var(--_hi), inset 2px 2px 0 oklch(0.1 0 0); } & > .window-toolbar { margin-top: 2px; border: 0; background: var(--_face); color: oklch(0.15 0 0); } & > .window-statusbar { margin-top: 2px; border: 0; background: var(--_face); color: oklch(0.15 0 0); box-shadow: inset 1px 1px 0 var(--_lo), inset -1px -1px 0 var(--_hi); } &:not([data-active]) > .window-titlebar { background: linear-gradient(90deg, oklch(0.55 0 0), oklch(0.72 0 0)); color: oklch(0.85 0 0); } &[data-maximized] { padding: 3px; } } } /* -- Accessibility -------------------------------------------- */ @media (prefers-reduced-motion: reduce) { .window, .window .window-controls > button { transition: none; } } @media (prefers-contrast: more) { .window { border-color: var(--foreground); } .window > .window-titlebar { border-bottom-color: var(--foreground); } .window:not([data-active]) > .window-titlebar { color: var(--foreground); } } @media (forced-colors: active) { .window { border: 1px solid CanvasText; } .window[data-active] > .window-titlebar { background: Highlight; color: HighlightText; forced-color-adjust: none; } .window .window-controls > button::before, .window .window-controls > button::after { border-color: ButtonText; } }}§JS view file
// -- Window -----------------------------------------------------// A desktop-style window on a non-modal <dialog>: the browser owns open /// close (the × is a <form method="dialog"> submit, so it works without// script) and the close event; this runtime adds what it cannot do - moving// the window by its title bar (pointer capture, arrow keys), raising the// clicked window to the front, maximize / minimize - plus the named-state// API (AGENTS.md "State API") and the imperative df$.shadcn.win.// Shared preamble (AGENTS.md "State API"); the implementation lives in core.js —// build.ts rewrites this import into a df$.shadcn.shared binding in dist/.import { defussGlobals, defussQuery, componentState, bindComponent } from '../../../shared/state-api.js';const df$ = defussGlobals();const dfDollar = defussQuery();// VERIFIED: (verify's component types ratchet - tsc -p tsconfig.components.json) every type// this file's API docs state - arguments, return values, event details - holds// against its code: a wrong one is a new type error and fails the build./** A window's place inside its desktop, px. */interface WindowPosition { /** from the desktop's left edge */ x: number; /** from the desktop's top edge */ y: number;}/** What create() takes - the shape the skill documents. */interface WindowCreateOptions { /** the title bar text (default 'Untitled') */ title?: string; /** a Lucide icon name for the title bar */ icon?: string; /** the body: a node, or text */ content?: Node | string; /** the body as markup (used when content is not a node) */ html?: string; /** a status bar line */ statusbar?: string; /** the window's id */ id?: string; /** left edge: px, or a CSS length (default: cascaded from the windows before it) */ x?: number | string; /** top edge: px, or a CSS length */ y?: number | string; /** width: px, or a CSS length */ width?: number | string; /** height: px, or a CSS length */ height?: number | string; /** the look of the title bar */ chrome?: 'windows' | 'mac' | 'linux' | 'retro'; /** the native resize handle (default true) */ resizable?: boolean; /** the desktop to open in: element, id or selector (default: the .window-desktop, else the body) */ parent?: HTMLElement | string; /** open in front, active (default true) */ focus?: boolean; /** a body without padding (an app inside) */ flush?: boolean;}/** What window-focus carries. */interface WindowFocusDetail { /** the title of the window now in front */ title: string;}const windowStates = ['default', 'maximized', 'minimized', 'closed'];/** setState() configs per state. */export interface WindowStateConfigs { /** Open at its normal size (opens a closed window). */ default: { /** move it: the left edge, px inside its desktop (with y) */ x?: number; /** move it: the top edge, px inside its desktop (with x) */ y?: number; }; /** Fills the desktop. */ maximized: { /** reported by getState() until it closes: the left edge set last, px */ x?: number; /** reported by getState() until it closes: the top edge set last, px */ y?: number; }; /** Rolled up to its title bar. */ minimized: { /** reported by getState() until it closes: the left edge set last, px */ x?: number; /** reported by getState() until it closes: the top edge set last, px */ y?: number; }; /** Closed - also after the close button or close(); it reopens at its normal size. */ closed: {};}/** Stacking order is shared by every window on the page - a counter, not state. */let topZ = 10;/** How much of a window must stay inside its desktop, so it can be grabbed back. */const KEEP = 48;const resolve = (target) => typeof target === 'string' ? dfDollar('#' + CSS.escape(target)).get(0) ?? dfDollar(target).get(0) : target;const titleOf = (w) => dfDollar(w).find('.window-title').get(0)?.textContent?.trim() ?? '';/** The window's position inside its desktop, in px. */const posOf = (w): WindowPosition => ({ x: w.offsetLeft, y: w.offsetTop });/** * Moves a window, keeping it reachable: at least KEEP px of GRABBABLE title * bar stay inside the desktop - the controls don't count, so a window pushed * to an edge never shows only its buttons. */function moveTo(w, x, y) { const parent = w.offsetParent; const bar = dfDollar(w).find('.window-titlebar').get(0); if (parent) { const ctl = dfDollar(w).find('.window-controls').get(0)?.offsetWidth ?? 0; const maxX = parent.clientWidth - KEEP - ctl; const minX = KEEP + ctl - w.offsetWidth; const maxY = parent.clientHeight - (bar?.offsetHeight ?? KEEP); x = Math.min(Math.max(x, minX), maxX); y = Math.min(Math.max(y, 0), Math.max(maxY, 0)); } w.style.setProperty('--window-x', `${Math.round(x)}px`); w.style.setProperty('--window-y', `${Math.round(y)}px`); return { x: Math.round(x), y: Math.round(y) };}/** Raises a window above every other and marks it the active one. */function raise(w) { if (!w.open) return; if (w.hasAttribute('data-active') && Number(w.style.zIndex) === topZ) return; topZ += 1; w.style.zIndex = String(topZ); dfDollar('.window[data-active]').toArray().forEach((o) => { if (o !== w) o.removeAttribute('data-active'); }); w.setAttribute('data-active', ''); // Fires when a window comes to the front - its title. w.dispatchEvent(new CustomEvent<WindowFocusDetail>('window-focus', { bubbles: true, detail: { title: titleOf(w) } }));}/** * show() runs the dialog focusing steps - it moves focus into the window * (onto the focusable title bar), which paints a focus ring on a window the * user never touched and pulls focus away from whatever opened it. Opening * leaves focus where it was; pressing into the window focuses as usual. */function showQuietly(w) { const prev = document.activeElement as HTMLElement | null; w.show(); if (prev && prev !== document.body && prev.isConnected && prev.focus) prev.focus({ preventScroll: true }); else if (w.contains(document.activeElement)) (document.activeElement as HTMLElement).blur();}/** Hands "active" to the front-most open window left (after one closed). */function activateTopmost() { const open = Array.from(dfDollar('.window[open]').toArray()); if (!open.length) return; const top = open.reduce((a, b) => (Number(b.style.zIndex || 0) > Number(a.style.zIndex || 0) ? b : a)); raise(top);}/** Maximize / minimize drop an inline size (from a native resize) and put it back after. */function stashSize(w) { if (w._stash) return; w._stash = { width: w.style.width, height: w.style.height }; w.style.width = ''; w.style.height = '';}function restoreSize(w) { if (!w._stash) return; w.style.width = w._stash.width; w.style.height = w._stash.height; w._stash = null;}/** * The markup of a state, for render(): the attributes a state writes, applied * to a detached copy of the authored markup ('default' IS the authored * markup). The live element gets the same markup from triggerStateChange - * the e2e render round trip proves they agree. */function applyMarkup(el, stateName) { const open = stateName !== 'closed'; dfDollar(el).attr('open', open ? '' : null); dfDollar(el).attr('data-maximized', stateName === 'maximized' ? '' : null); dfDollar(el).attr('data-minimized', stateName === 'minimized' ? '' : null); dfDollar(el).find('.window-maximize').attr('aria-label', stateName === 'maximized' ? 'Restore' : 'Maximize'); dfDollar(el).find('.window-minimize').attr('aria-label', stateName === 'minimized' ? 'Restore' : 'Minimize');}/** * UI side of setState. 'default' = open, normal size (opens a closed * window; `{ x, y }` moves it); 'maximized' fills the desktop; 'minimized' * rolls it up to its title bar; 'closed' closes the dialog. */function triggerStateChange(w, stateName, config) { const maxBtn = dfDollar(w).find('.window-maximize').get(0); if (stateName !== 'closed' && !w.open) showQuietly(w); switch (stateName) { case 'default': w.removeAttribute('data-maximized'); w.removeAttribute('data-minimized'); restoreSize(w); if (config.x !== undefined && config.y !== undefined) moveTo(w, Number(config.x), Number(config.y)); raise(w); break; case 'maximized': w.removeAttribute('data-minimized'); stashSize(w); w.setAttribute('data-maximized', ''); raise(w); break; case 'minimized': w.removeAttribute('data-maximized'); stashSize(w); w.setAttribute('data-minimized', ''); break; case 'closed': if (w.open) w.close(); // the close event hands "active" on // closed means closed: the size modes are not kept for the next open w.removeAttribute('data-maximized'); w.removeAttribute('data-minimized'); break; } if (maxBtn) maxBtn.setAttribute('aria-label', stateName === 'maximized' ? 'Restore' : 'Maximize'); dfDollar(w).find('.window-minimize').get(0)?.setAttribute('aria-label', stateName === 'minimized' ? 'Restore' : 'Minimize');}/** Registry-level API; pass the .window element explicitly. Unknown names throw. */export const windowApi = componentState<HTMLDialogElement>({ component: 'window', states: windowStates, apply: (w, state) => { w.dataset.stateName = state.name; triggerStateChange(w, state.name, state.config); }, read: (w, state) => { // the dialog is the truth: closed the moment it closes, before the // queued close event updates data-state-name const name = !w.open ? 'closed' : w.dataset.stateName || 'default'; return { name, config: name === 'closed' ? {} : state.config }; }, markup: (el, state) => applyMarkup(el, state.name),});df$.windowApi = windowApi;df$.windowStates = windowStates;/** Title-bar dragging: pointer capture, so a fast drag never loses the window. */function bindDrag(w, bar) { bar.addEventListener('pointerdown', (e) => { if (e.button !== 0 || e.target.closest('button, a, input, select, textarea')) return; raise(w); if (w.hasAttribute('data-maximized')) return; e.preventDefault(); const start = posOf(w); const sx = e.clientX, sy = e.clientY; bar.setPointerCapture(e.pointerId); w.setAttribute('data-dragging', ''); const onMove = (m) => moveTo(w, start.x + m.clientX - sx, start.y + m.clientY - sy); const onUp = () => { bar.removeEventListener('pointermove', onMove); bar.removeEventListener('pointerup', onUp); bar.removeEventListener('pointercancel', onUp); w.removeAttribute('data-dragging'); // Fires after a window was dragged (or moved with the keyboard) - its position. w.dispatchEvent(new CustomEvent<WindowPosition>('window-move', { bubbles: true, detail: posOf(w) })); }; bar.addEventListener('pointermove', onMove); bar.addEventListener('pointerup', onUp); bar.addEventListener('pointercancel', onUp); }); // double-click the bar: maximize / restore, like every desktop bar.addEventListener('dblclick', (e) => { if (e.target.closest('button')) return; windowApi.setState(w, w.hasAttribute('data-maximized') ? 'default' : 'maximized', {}); }); // keyboard: the bar is focusable; arrows move (Shift = bigger steps) bar.addEventListener('keydown', (e) => { const step = e.shiftKey ? 64 : 16; const d = { ArrowLeft: [-step, 0], ArrowRight: [step, 0], ArrowUp: [0, -step], ArrowDown: [0, step] }[e.key]; if (!d || w.hasAttribute('data-maximized') || e.target !== bar) return; e.preventDefault(); const p = posOf(w); moveTo(w, p.x + d[0], p.y + d[1]); w.dispatchEvent(new CustomEvent<WindowPosition>('window-move', { bubbles: true, detail: posOf(w) })); });}function init() { dfDollar<HTMLDialogElement>('dialog.window:not([data-init])').toArray().forEach((w) => { w.dataset.init = ''; const bar = dfDollar(w).find(':scope > .window-titlebar').get(0); if (bar) { if (!bar.hasAttribute('tabindex')) bar.tabIndex = 0; bindDrag(w, bar); } if (!w.hasAttribute('aria-labelledby') && !w.hasAttribute('aria-label')) { const title = dfDollar(w).find('.window-title').get(0); if (title) { if (!title.id) title.id = `window-title-${Math.random().toString(36).slice(2, 8)}`; w.setAttribute('aria-labelledby', title.id); } } // any press inside a window brings it to the front (capture: before // the content handles it); so does focus arriving by keyboard w.addEventListener('pointerdown', () => raise(w), true); w.addEventListener('focusin', () => raise(w)); dfDollar(w).find('.window-maximize').get(0)?.addEventListener('click', () => windowApi.setState(w, w.hasAttribute('data-maximized') ? 'default' : 'maximized', {})); // the × closes through the API too: the native form submit already // does it, but a page that cancels submits (a sandbox, a SPA) must not // strand the window open dfDollar(w).find('.window-close').get(0)?.addEventListener('click', (e) => { e.preventDefault(); windowApi.setState(w, 'closed', {}); }); dfDollar(w).find('.window-minimize').get(0)?.addEventListener('click', () => windowApi.setState(w, w.hasAttribute('data-minimized') ? 'default' : 'minimized', {})); // however it closed - the × (form method="dialog"), Escape-less // close(), the API - the state follows the dialog w.addEventListener('close', () => { // the event is queued: a window reopened meanwhile stays open if (w.open) return; w.dataset.stateName = 'closed'; w.removeAttribute('data-active'); w.removeAttribute('data-maximized'); w.removeAttribute('data-minimized'); activateTopmost(); }); // el.store + el.api (AGENTS.md "State through stores") bindComponent(w, windowApi); const initial = !w.open ? 'closed' : w.hasAttribute('data-maximized') ? 'maximized' : w.hasAttribute('data-minimized') ? 'minimized' : 'default'; w.dataset.stateName = initial; // the buttons name what they do in the state the window starts in (an // authored maximized window's button restores it) dfDollar(w).find('.window-maximize').attr('aria-label', initial === 'maximized' ? 'Restore' : 'Maximize'); dfDollar(w).find('.window-minimize').attr('aria-label', initial === 'minimized' ? 'Restore' : 'Minimize'); if (w.open) { w.style.zIndex = String(++topZ); // the last authored open window starts active dfDollar('.window[data-active]').toArray().forEach((o) => o.removeAttribute('data-active')); w.setAttribute('data-active', ''); } });}/** * Builds a window element from options - the shape the skill documents. * @param options - title, body, place, size, look and where it opens * @returns the new window (a <dialog class="window">), open unless focus is false */function create(options: WindowCreateOptions = {}): HTMLDialogElement { const { title = 'Untitled', icon, content, html, statusbar, id, x, y, width, height, chrome, resizable = true, parent, focus = true, flush = false, } = options; const host = resolve(parent) ?? dfDollar('.window-desktop').get(0) ?? document.body; const w = document.createElement('dialog'); w.className = 'window'; if (id) w.id = id; if (chrome) w.dataset.chrome = chrome; if (resizable) w.setAttribute('data-resizable', ''); const count = dfDollar(host).find(':scope > .window').toArray().length; w.style.setProperty('--window-x', typeof x === 'number' ? `${x}px` : x ?? `${24 + (count % 8) * 28}px`); w.style.setProperty('--window-y', typeof y === 'number' ? `${y}px` : y ?? `${24 + (count % 8) * 28}px`); if (width !== undefined) w.style.setProperty('--window-w', typeof width === 'number' ? `${width}px` : width); if (height !== undefined) w.style.setProperty('--window-h', typeof height === 'number' ? `${height}px` : height); const bar = document.createElement('header'); bar.className = 'window-titlebar'; if (icon) { const i = document.createElement('i'); i.className = 'window-icon'; i.setAttribute('data-lucide', icon); bar.append(i); } const h = document.createElement('h2'); h.className = 'window-title'; h.textContent = title; const controls = document.createElement('form'); controls.method = 'dialog'; controls.className = 'window-controls'; for (const [cls, label, type] of [['window-minimize', 'Minimize', 'button'], ['window-maximize', 'Maximize', 'button'], ['window-close', 'Close', 'submit']] as const) { const b = document.createElement('button'); b.type = type; b.className = cls; b.setAttribute('aria-label', label); controls.append(b); } bar.append(h, controls); const body = document.createElement('div'); body.className = 'window-body'; if (flush) body.setAttribute('data-flush', ''); if (content instanceof Node) body.append(content); else if (typeof html === 'string') body.append(document.createRange().createContextualFragment(html)); else if (content !== undefined) body.textContent = String(content); w.append(bar, body); if (statusbar !== undefined) { const s = document.createElement('footer'); s.className = 'window-statusbar'; s.textContent = String(statusbar); w.append(s); } host.append(w); init(); showQuietly(w); if (focus) windowApi.setState(w, 'default', {}); if (icon) globalThis.lucide?.createIcons?.(); return w;}/** * Windows (open unless `all`) inside `scope` (default: the page). * @param scope - a desktop element, its id or a selector (default: the page) * @param all - true: closed windows too * @returns the windows, in document order */const list = (scope?: HTMLElement | string, all: boolean = false): HTMLDialogElement[] => dfDollar(resolve(scope) ?? document).find<HTMLDialogElement>(all ? '.window' : '.window[open]').toArray();/** * Steps the open windows diagonally from the top-left, front-most last. * @param scope - a desktop element, its id or a selector (default: the page) * @param step - px between two windows (default 28) */function cascade(scope?: HTMLElement | string, step: number = 28): void { list(scope) .sort((a, b) => Number(a.style.zIndex || 0) - Number(b.style.zIndex || 0)) .forEach((w, i) => { windowApi.setState(w, 'default', {}); moveTo(w, 16 + i * step, 16 + i * step); });}/** * Lays the open windows side by side in a grid that fills their desktop. * @param scope - a desktop element, its id or a selector (default: the page) */function tile(scope?: HTMLElement | string): void { const wins = list(scope); if (!wins.length) return; const cols = Math.ceil(Math.sqrt(wins.length)); const rows = Math.ceil(wins.length / cols); wins.forEach((w, i) => { windowApi.setState(w, 'default', {}); const p = w.offsetParent; if (!p) return; const cw = p.clientWidth / cols, ch = p.clientHeight / rows; w.style.width = ''; w.style.height = ''; w.style.setProperty('--window-w', `${Math.floor(cw)}px`); w.style.setProperty('--window-h', `${Math.floor(ch)}px`); moveTo(w, (i % cols) * cw, Math.floor(i / cols) * ch); });}// the imperative API: df$.shadcn.win.* - every method takes an element,// an id or a selector/** df$.shadcn.win - create, arrange and drive windows. */export const windowActions = { create, /** * Open a window (closed, minimized or not shown yet) - config is the state's config. * @param target - the .window element, its id or a selector * @param config - where it opens: { x, y } px * @returns the window, null when the target matches none */ open: (target: string | HTMLElement, config: { x?: number; y?: number } = {}): HTMLDialogElement | null => { const w = resolve(target); if (w) windowApi.setState(w, 'default', config); return w; }, /** * Close it (the closed state). * @param target - the .window element, its id or a selector * @returns the window, null when the target matches none */ close: (target: string | HTMLElement): HTMLDialogElement | null => { const w = resolve(target); if (w) windowApi.setState(w, 'closed', {}); return w; }, /** * Bring it to the front (the active window). * @param target - the .window element, its id or a selector * @returns the window, null when the target matches none */ focus: (target: string | HTMLElement): HTMLDialogElement | null => { const w = resolve(target); if (w?.open) raise(w); return w; }, /** * Move it to x, y (px, inside its desktop). * @param target - the .window element, its id or a selector * @param x - the left edge, px * @param y - the top edge, px * @returns where it landed (kept reachable inside its desktop), null when the target matches none */ move: (target: string | HTMLElement, x: number, y: number): WindowPosition | null => { const w = resolve(target); return w ? moveTo(w, x, y) : null; }, /** * Size it: width (and height) as px numbers or CSS lengths. * @param target - the .window element, its id or a selector * @param width - px, or a CSS length * @param height - px, or a CSS length; omitted, the height stays * @returns the window, null when the target matches none */ resize: (target: string | HTMLElement, width: number | string, height?: number | string): HTMLDialogElement | null => { const w = resolve(target); if (!w) return null; w.style.width = ''; w.style.height = ''; w.style.setProperty('--window-w', typeof width === 'number' ? `${width}px` : width); if (height !== undefined) w.style.setProperty('--window-h', typeof height === 'number' ? `${height}px` : height); return w; }, /** * Fill the desktop. * @param target - the .window element, its id or a selector * @returns the window, null when the target matches none */ maximize: (target: string | HTMLElement): HTMLDialogElement | null => { const w = resolve(target); if (w) windowApi.setState(w, 'maximized', {}); return w; }, /** * Minimize it to the taskbar. * @param target - the .window element, its id or a selector * @returns the window, null when the target matches none */ minimize: (target: string | HTMLElement): HTMLDialogElement | null => { const w = resolve(target); if (w) windowApi.setState(w, 'minimized', {}); return w; }, /** * Back to its normal size and place. * @param target - the .window element, its id or a selector * @returns the window, null when the target matches none */ restore: (target: string | HTMLElement): HTMLDialogElement | null => { const w = resolve(target); if (w) windowApi.setState(w, 'default', {}); return w; }, /** * Maximize it, or restore it when it is maximized. * @param target - the .window element, its id or a selector * @returns the window, null when the target matches none */ toggleMaximize: (target: string | HTMLElement): HTMLDialogElement | null => { const w = resolve(target); if (w) windowApi.setState(w, w.hasAttribute('data-maximized') ? 'default' : 'maximized', {}); return w; }, /** * The window in front. * @returns the active open window, undefined when none is open */ active: (): HTMLDialogElement | undefined => dfDollar<HTMLDialogElement>('.window[open][data-active]').get(0), list, cascade, tile,};df$.win = windowActions;init();new MutationObserver(init).observe(document, { childList: true, subtree: true });Comments, ideas or improvements? Edit this page's source on GitHub