onfroy2's picture
Build a website where content exists as physical objects on an interactive tabletop:
4d9d3b3 verified
class TabletopObject extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
const type = this.getAttribute('type');
const title = this.getAttribute('title');
const color = this.getAttribute('color') || 'blue';
const image = this.getAttribute('image');
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
position: absolute;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
outline: none;
}
:host(:hover) {
transform: translateY(-8px) rotateZ(2deg);
filter: drop-shadow(0 20px 40px rgba(0, 0, 0, 0.3));
z-index: 100;
}
:host(:focus-visible) {
outline: 3px solid #3b82f6;
outline-offset: 2px;
}
.object-container {
width: 100%;
height: 100%;
position: relative;
}
/* Magazine styles */
.magazine {
background: linear-gradient(135deg, ${this.getColorShades(color)[0]} 0%, ${this.getColorShades(color)[1]} 100%);
border-radius: 4px;
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);
position: relative;
overflow: hidden;
}
.magazine::before {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
transform: translateX(-100%);
}
.magazine:hover::before {
animation: shimmer 1.5s infinite;
}
@keyframes shimmer {
100% { transform: translateX(100%); }
}
.magazine-title {
writing-mode: vertical-rl;
text-orientation: mixed;
color: white;
font-weight: bold;
font-size: 14px;
padding: 12px 8px;
text-align: center;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
/* Resource/file styles */
.file-folder {
background: linear-gradient(135deg, #fbbf24 0%, #f59e0b 100%);
border-radius: 8px;
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);
position: relative;
}
.file-folder::after {
content: '';
position: absolute;
top: 0;
right: 12px;
width: 30px;
height: 10px;
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
border-radius: 0 0 8px 8px;
}
.file-label {
position: absolute;
bottom: 8px;
left: 8px;
right: 8px;
background: rgba(0, 0, 0, 0.7);
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
}
/* Art frame styles */
.art-frame {
background: linear-gradient(135deg, #d97706 0%, #92400e 100%);
padding: 12px;
border-radius: 4px;
box-shadow:
inset 0 0 20px rgba(0, 0, 0, 0.3),
0 10px 30px rgba(0, 0, 0, 0.2);
}
.art-canvas {
background: #f3f4f6;
border-radius: 2px;
overflow: hidden;
position: relative;
}
.art-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.art-title {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: linear-gradient(to top, rgba(0, 0, 0, 0.8), transparent);
color: white;
padding: 12px 8px 8px;
font-size: 12px;
font-weight: bold;
}
/* Demo device styles */
.device {
background: #1f2937;
border-radius: 20px;
padding: 20px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
position: relative;
}
.device-screen {
background: #000;
border-radius: 10px;
overflow: hidden;
}
.device-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.device-title {
position: absolute;
bottom: -32px;
left: 50%;
transform: translateX(-50%);
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(8px);
padding: 6px 16px;
border-radius: 20px;
font-size: 14px;
font-weight: 500;
white-space: nowrap;
}
</style>
<div class="object-container">
${this.renderObject(type, title, color, image)}
</div>
`;
this.setupInteractions();
}
getColorShades(color) {
const colorMap = {
blue: ['#3b82f6', '#1d4ed8'],
purple: ['#8b5cf6', '#6d28d9'],
green: ['#10b981', '#047857'],
red: ['#ef4444', '#dc2626'],
orange: ['#f97316', '#ea580c'],
pink: ['#ec4899', '#db2777'],
yellow: ['#f59e0b', '#d97706']
};
return colorMap[color] || colorMap.blue;
}
renderObject(type, title, color, image) {
switch(type) {
case 'magazine':
return `
<div class="magazine w-32 h-44">
<div class="magazine-title">${title}</div>
</div>
`;
case 'resource':
return `
<div class="file-folder w-24 h-32">
<div class="file-label">${title}</div>
</div>
`;
case 'art':
return `
<div class="art-frame">
<div class="art-canvas w-48 h-32">
<img src="${image}" alt="${title}" class="art-image">
<div class="art-title">${title}</div>
</div>
</div>
`;
case 'demo':
return `
<div class="device w-64 h-40">
<div class="device-screen">
<img src="${image}" alt="${title}" class="device-image">
</div>
<div class="device-title">${title}</div>
</div>
`;
default:
return '<div>Unknown object type</div>';
}
}
setupInteractions() {
this.addEventListener('click', (e) => {
this.dispatchEvent(new CustomEvent('object-click', {
detail: {
id: this.getAttribute('object-id'),
type: this.getAttribute('type'),
title: this.getAttribute('title')
},
bubbles: true
}));
});
// Adding hover preview
this.addEventListener('mouseenter', () => {
this.dispatchEvent(new CustomEvent('object-hover', {
detail: {
id: this.getAttribute('object-id'),
type: this.getAttribute('type'),
title: this.getAttribute('title')
},
bubbles: true
}));
});
this.addEventListener('mouseleave', () => {
this.dispatchEvent(new CustomEvent('object-unhover', {
detail: {
id: this.getAttribute('object-id')
},
bubbles: true
}));
});
}
}
customElements.define('tabletop-object', TabletopObject);