This is a simple floating window class made by Gemini.
## Prompts
```plaintext
In HTML and Javascript, some attractive looking openable, draggable, possibly
resizeable, closeable windows. Overflow options can be hidden, scroll. Also a
resize to contents button. With an X in the corner to close. As a Javascript
class that we subclass to define the contents and configuration for. And
necessary CSS for styling.
yes please. and an example html file with clickable elements that open various
windows.
Snapping to the siees works. But could the windows remember their unsnapped
size to return to when unsnapping from an an edge.
I don't like the snapping animation.
```
## CSS
```css
:root {
--win-bg: #ffffff;
--win-border: #bdc3c7;
--win-header: #ecf0f1;
--win-shadow: 0 10px 30px rgba(0,0,0,0.3);
--taskbar-bg: #2c3e50;
--accent: #3498db;
}
body {
margin: 0;
height: 100vh;
overflow: hidden; /* Prevent body scroll while dragging */
}
/* The Window Container */
.win-instance {
position: absolute;
min-width: 200px;
min-height: 150px;
background: var(--win-bg);
border: 1px solid var(--win-border);
box-shadow: var(--win-shadow);
display: flex;
flex-direction: column;
border-radius: 8px;
overflow: hidden;
z-index: 10;
xxtransition: width 0.15s ease-out, height 0.15s ease-out; /* Smooth snapping */
}
/* Header/Title Bar */
.win-header {
padding: 10px 15px;
background: var(--win-header);
cursor: move;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid var(--win-border);
user-select: none;
}
.win-title {
font-weight: bold;
font-size: 14px;
color: #333;
}
/* Window Buttons */
.win-controls button {
margin-left: 4px;
cursor: pointer;
border: none;
background: #d5dbdb;
border-radius: 4px;
padding: 4px 10px;
font-size: 12px;
transition: background 0.2s;
}
.win-controls button:hover { background: #bdc3c7; }
.win-close:hover { background: #e74c3c !important; color: white; }
/* Content Area */
.win-content {
flex-grow: 1;
padding: 15px;
position: relative;
background: white;
}
/* Resize Handle */
.resizer {
width: 16px;
height: 16px;
position: absolute;
right: 0;
bottom: 0;
cursor: nwse-resize;
background: linear-gradient(135deg, transparent 50%, #bdc3c7 50%);
}
/* Taskbar */
#taskbar {
position: fixed;
bottom: 0; left: 0; right: 0;
height: 45px;
background: var(--taskbar-bg);
display: flex;
padding: 0 10px;
gap: 8px;
align-items: center;
z-index: 10000;
}
.task-item {
background: #34495e;
color: white;
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
border-bottom: 2px solid transparent;
}
.task-item:hover { background: #456789; }
.task-item.active { border-bottom: 2px solid var(--accent); }
/* States */
.win-minimized { display: none !important; }
```
## Javascript
```js
class FloatingWindow {
constructor(title, options = {}) {
this.title = title;
this.width = options.width || '400px';
this.height = options.height || '300px';
this.overflow = options.overflow || 'auto';
this.id = 'win-' + Math.random().toString(36).substr(2, 9);
// State Tracking
this.isSnapped = false;
this.previousState = { width: this.width, height: this.height, top: '100px', left: '100px' };
this.init();
}
init() {
this.el = document.createElement('div');
this.el.className = 'win-instance';
this.el.id = this.id;
this.el.style.width = this.width;
this.el.style.height = this.height;
this.el.style.left = '100px';
this.el.style.top = '100px';
this.el.innerHTML = `
`;
this.contentArea = this.el.querySelector('.win-content');
document.body.appendChild(this.el);
this.createTaskbarItem();
this.setupEvents();
this.bringToFront();
this.render(); // Subclass content
}
createTaskbarItem() {
const bar = document.getElementById('taskbar');
if (!bar) return;
const btn = document.createElement('div');
btn.className = 'task-item';
btn.id = 'task-' + this.id;
btn.innerText = this.title;
btn.onclick = () => this.toggleMinimize();
bar.appendChild(btn);
}
bringToFront() {
document.querySelectorAll('.win-instance').forEach(w => w.style.zIndex = 10);
document.querySelectorAll('.task-item').forEach(t => t.classList.remove('active'));
this.el.style.zIndex = 100;
const tItem = document.getElementById('task-' + this.id);
if (tItem) tItem.classList.add('active');
}
toggleMinimize() {
if (this.el.classList.contains('win-minimized')) {
this.el.classList.remove('win-minimized');
this.bringToFront();
} else {
this.el.classList.add('win-minimized');
}
}
setupEvents() {
const header = this.el.querySelector('.win-header');
// Header Buttons
this.el.querySelector('.win-close').onclick = () => {
this.el.remove();
const tItem = document.getElementById('task-' + this.id);
if (tItem) tItem.remove();
};
this.el.querySelector('.win-min').onclick = () => this.toggleMinimize();
this.el.querySelector('.win-fit').onclick = () => {
this.isSnapped = false;
this.el.style.width = 'auto';
this.el.style.height = 'auto';
};
// Mouse Interaction
this.el.onmousedown = () => this.bringToFront();
header.onmousedown = (e) => this.startDrag(e);
this.el.querySelector('.resizer').onmousedown = (e) => this.startResize(e);
}
startDrag(e) {
let pos1 = 0, pos2 = 0, pos3 = e.clientX, pos4 = e.clientY;
document.onmousemove = (moveEvent) => {
// 1. If snapped, restore before moving further
if (this.isSnapped) {
this.restoreFromSnap(moveEvent.clientX);
// Recalculate positions after restore to avoid jumping
pos3 = moveEvent.clientX;
pos4 = moveEvent.clientY;
}
pos1 = pos3 - moveEvent.clientX;
pos2 = pos4 - moveEvent.clientY;
pos3 = moveEvent.clientX;
pos4 = moveEvent.clientY;
let newTop = this.el.offsetTop - pos2;
let newLeft = this.el.offsetLeft - pos1;
// 2. Check for Snap Zones
if (moveEvent.clientX < 10) {
this.applySnap(0, 0, '50%', 'calc(100% - 45px)');
} else if (moveEvent.clientX > window.innerWidth - 10) {
this.applySnap('50%', 0, '50%', 'calc(100% - 45px)');
} else if (moveEvent.clientY < 10) {
this.applySnap(0, 0, '100%', 'calc(100% - 45px)');
} else {
this.el.style.top = newTop + "px";
this.el.style.left = newLeft + "px";
}
};
document.onmouseup = () => document.onmousemove = null;
}
applySnap(left, top, w, h) {
if (!this.isSnapped) {
this.previousState = {
width: this.el.style.width,
height: this.el.style.height,
top: this.el.style.top,
left: this.el.style.left
};
this.isSnapped = true;
}
this.el.style.left = left;
this.el.style.top = top;
this.el.style.width = w;
this.el.style.height = h;
}
restoreFromSnap(mouseX) {
this.isSnapped = false;
this.el.style.width = this.previousState.width;
this.el.style.height = this.previousState.height;
// Center window under cursor horizontally during pull-away
const w = parseInt(this.el.style.width) || 400;
this.el.style.left = (mouseX - (w / 2)) + 'px';
}
startResize(e) {
e.preventDefault();
e.stopPropagation();
this.isSnapped = false; // Resizing breaks snap
document.onmousemove = (ev) => {
this.el.style.width = (ev.clientX - this.el.offsetLeft) + 'px';
this.el.style.height = (ev.clientY - this.el.offsetTop) + 'px';
};
document.onmouseup = () => document.onmousemove = null;
}
render() {} // To be overridden
}
```
## HTML Example Usage
Assuming CSS is in `win1.css` and Javascript in `win1.js`.
See the example [here](win1.html).
```html
JS Window System
Dashboard
```