onfroy2 commited on
Commit
4d9d3b3
Β·
verified Β·
1 Parent(s): 4d04582

Build a website where content exists as physical objects on an interactive tabletop:

Browse files

**Metaphor**: Digital workspace where blog posts are magazines, resources are file folders, and art are framed pieces on a gallery wall.

**Interactions**:
1. Pan/zoom across infinite table surface
2. Hover objects for preview (gentle lift + shadow)
3. Drag to rearrange with slight physics
4. Click to "pick up" object and open in magazine/comic view
5. Magazine view: Comic panel layout with page navigation
6. Return to tabletop closes magazine back to its position

**Content types**:
- Blog posts β†’ Magazines (cover, spine, pages)
- Resources β†’ Filing cabinet with categorized cards
- Art projects β†’ Gallery wall with frames
- Project demos β†’ Device mockups

**Key features**:
- Multiple table views (chronological, categorical, project groups)
- Search via "magnifying glass" tool
- Personal annotations on objects (sticky notes, doodles)
- Day/night mode affecting lighting
- Mobile/touch optimized with gesture support

**Tech requirements**:
- Prioritize accessibility (keyboard nav, screen reader fallback)
- Offline reading capability for opened magazines
- Fast initial load (lazy-load objects as they enter viewport)
- CMS integration for easy content updates

**Success metrics**:
- Users naturally "play" with the interface
- Content discovery feels organic, not forced
- Emotional connection through tactile interaction
- Low cognitive load despite rich visual environment

Files changed (6) hide show
  1. README.md +8 -5
  2. components/magazine-viewer.js +443 -0
  3. components/tabletop-object.js +278 -0
  4. index.html +100 -19
  5. script.js +643 -0
  6. style.css +194 -19
README.md CHANGED
@@ -1,10 +1,13 @@
1
  ---
2
- title: Tabletop Gallery Explorer
3
- emoji: 🌍
4
- colorFrom: purple
5
- colorTo: blue
6
  sdk: static
7
  pinned: false
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
1
  ---
2
+ title: Tabletop Gallery Explorer 🎨
3
+ colorFrom: red
4
+ colorTo: yellow
5
+ emoji: 🐳
6
  sdk: static
7
  pinned: false
8
+ tags:
9
+ - deepsite-v3
10
  ---
11
 
12
+ # Welcome to your new DeepSite project!
13
+ This project was created with [DeepSite](https://huggingface.co/deepsite).
components/magazine-viewer.js ADDED
@@ -0,0 +1,443 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ class MagazineViewer extends HTMLElement {
2
+ constructor() {
3
+ super();
4
+ this.attachShadow({ mode: 'open' });
5
+ this.currentPage = 0;
6
+ this.pages = [];
7
+ }
8
+
9
+ connectedCallback() {
10
+ this.pages = JSON.parse(this.getAttribute('pages') || '[]');
11
+ this.title = this.getAttribute('title') || 'Magazine';
12
+ this.color = this.getAttribute('color') || 'blue';
13
+
14
+ this.render();
15
+ this.setupNavigation();
16
+ this.setupKeyboardNav();
17
+ }
18
+
19
+ render() {
20
+ this.shadowRoot.innerHTML = `
21
+ <style>
22
+ :host {
23
+ display: block;
24
+ width: 100%;
25
+ height: 100%;
26
+ perspective: 1200px;
27
+ }
28
+
29
+ .magazine-viewer {
30
+ width: 100%;
31
+ height: 100%;
32
+ position: relative;
33
+ transform-style: preserve-3d;
34
+ }
35
+
36
+ .page-container {
37
+ width: 100%;
38
+ max-width: 800px;
39
+ height: 100%;
40
+ margin: 0 auto;
41
+ position: relative;
42
+ transform-style: preserve-3d;
43
+ transition: transform 0.6s cubic-bezier(0.4, 0, 0.2, 1);
44
+ }
45
+
46
+ .page {
47
+ position: absolute;
48
+ width: 100%;
49
+ height: 100%;
50
+ background: white;
51
+ border-radius: 8px;
52
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
53
+ backface-visibility: hidden;
54
+ padding: 40px;
55
+ display: flex;
56
+ flex-direction: column;
57
+ overflow: hidden;
58
+ }
59
+
60
+ .page-content {
61
+ flex: 1;
62
+ display: flex;
63
+ flex-direction: column;
64
+ align-items: center;
65
+ justify-content: center;
66
+ text-align: center;
67
+ position: relative;
68
+ }
69
+
70
+ .page-header {
71
+ position: absolute;
72
+ top: 20px;
73
+ left: 20px;
74
+ right: 20px;
75
+ display: flex;
76
+ justify-content: space-between;
77
+ align-items: center;
78
+ font-size: 12px;
79
+ color: #6b7280;
80
+ }
81
+
82
+ .page-footer {
83
+ position: absolute;
84
+ bottom: 20px;
85
+ left: 20px;
86
+ right: 20px;
87
+ text-align: center;
88
+ font-size: 12px;
89
+ color: #6b7280;
90
+ }
91
+
92
+ .cover {
93
+ background: linear-gradient(135deg, ${this.getGradientColors(this.color)[0]} 0%, ${this.getGradientColors(this.color)[1]} 100%);
94
+ color: white;
95
+ }
96
+
97
+ .cover h1 {
98
+ font-size: 3rem;
99
+ margin-bottom: 1rem;
100
+ font-weight: bold;
101
+ }
102
+
103
+ .cover .subtitle {
104
+ font-size: 1.5rem;
105
+ opacity: 0.9;
106
+ }
107
+
108
+ .navigation {
109
+ position: absolute;
110
+ bottom: -60px;
111
+ left: 50%;
112
+ transform: translateX(-50%);
113
+ display: flex;
114
+ gap: 16px;
115
+ background: rgba(255, 255, 255, 0.9);
116
+ backdrop-filter: blur(10px);
117
+ padding: 12px 24px;
118
+ border-radius: 30px;
119
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
120
+ }
121
+
122
+ .nav-btn {
123
+ padding: 8px 16px;
124
+ background: ${this.getColorValue(this.color)};
125
+ color: white;
126
+ border: none;
127
+ border-radius: 20px;
128
+ cursor: pointer;
129
+ transition: all 0.3s ease;
130
+ font-size: 14px;
131
+ display: flex;
132
+ align-items: center;
133
+ gap: 4px;
134
+ }
135
+
136
+ .nav-btn:hover:not(:disabled) {
137
+ transform: translateY(-2px);
138
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
139
+ }
140
+
141
+ .nav-btn:disabled {
142
+ opacity: 0.5;
143
+ cursor: not-allowed;
144
+ }
145
+
146
+ .page-indicator {
147
+ display: flex;
148
+ align-items: center;
149
+ padding: 0 16px;
150
+ font-size: 14px;
151
+ color: #374151;
152
+ }
153
+
154
+ .ripple {
155
+ position: absolute;
156
+ border-radius: 50%;
157
+ background: rgba(255, 255, 255, 0.6);
158
+ transform: scale(0);
159
+ animation: ripple 0.6s linear;
160
+ }
161
+
162
+ @keyframes ripple {
163
+ to {
164
+ transform: scale(4);
165
+ opacity: 0;
166
+ }
167
+ }
168
+
169
+ .content-animation {
170
+ animation: fadeInUp 0.8s ease-out;
171
+ }
172
+
173
+ @keyframes fadeInUp {
174
+ from {
175
+ opacity: 0;
176
+ transform: translateY(30px);
177
+ }
178
+ to {
179
+ opacity: 1;
180
+ transform: translateY(0);
181
+ }
182
+ }
183
+
184
+ .page-number {
185
+ position: absolute;
186
+ bottom: 20px;
187
+ right: 20px;
188
+ font-size: 12px;
189
+ color: #6b7280;
190
+ }
191
+
192
+ /* Comic panel styles */
193
+ .comic-panel {
194
+ background: #f3f4f6;
195
+ border: 3px solid #1f293b;
196
+ border-radius: 8px;
197
+ padding: 20px;
198
+ margin: 10px;
199
+ position: relative;
200
+ }
201
+
202
+ .comic-panel::before {
203
+ content: '';
204
+ position: absolute;
205
+ top: -3px;
206
+ left: -3px;
207
+ right: -3px;
208
+ bottom: -3px;
209
+ background: #1f293b;
210
+ border-radius: 10px;
211
+ z-index: -1;
212
+ }
213
+ </style>
214
+
215
+ <div class="magazine-viewer">
216
+ <div class="page-container" id="pageContainer">
217
+ ${this.renderPages()}
218
+ </div>
219
+
220
+ <div class="navigation">
221
+ <button class="nav-btn" id="prevBtn">
222
+ <i data-feather="chevron-left"></i>
223
+ Previous
224
+ </button>
225
+
226
+ <div class="page-indicator">
227
+ <span id="currentPage">${this.currentPage + 1}</span>
228
+ <span> / </span>
229
+ <span id="totalPages">${this.pages.length}</span>
230
+ </div>
231
+
232
+ <button class="nav-btn" id="nextBtn">
233
+ Next
234
+ <i data-feather="chevron-right"></i>
235
+ </button>
236
+ </div>
237
+ </div>
238
+ `;
239
+
240
+ // Replace feather icons
241
+ setTimeout(() => {
242
+ if (window.feather) {
243
+ feather.replace({
244
+ 'stroke-width': 2,
245
+ width: 16,
246
+ height: 16
247
+ });
248
+ }
249
+ }, 0);
250
+ }
251
+
252
+ renderPages() {
253
+ return this.pages.map((page, index) => {
254
+ const isActive = index === this.currentPage;
255
+ const transform = this.calculatePageTransform(index);
256
+
257
+ return `
258
+ <div class="page ${page.type}"
259
+ style="transform: ${transform}; ${isActive ? 'z-index: 10;' : ''}">
260
+
261
+ <div class="page-header">
262
+ <span>${this.title}</span>
263
+ <span>${new Date().toLocaleDateString()}</span>
264
+ </div>
265
+
266
+ <div class="page-content">
267
+ ${this.renderPageContent(page, index)}
268
+ </div>
269
+
270
+ <div class="page-footer">
271
+ <span>Β© 2024 Tabletop Gallery</span>
272
+ </div>
273
+
274
+ ${index > 0 ? `<div class="page-number">${index}</div>` : ''}
275
+ </div>
276
+ `;
277
+ }).join('');
278
+ }
279
+
280
+ calculatePageTransform(index) {
281
+ const diff = index - this.currentPage;
282
+ const baseTransform = 'rotateY(0deg)';
283
+
284
+ if (diff === 0) {
285
+ return baseTransform;
286
+ } else if (diff < 0) {
287
+ return `translateX(${diff * 50}px) rotateY(-15deg) translateZ(${-Math.abs(diff) * 10}px)`;
288
+ } else {
289
+ return `translateX(${diff * 50}px) rotateY(15deg) translateZ(${-Math.abs(diff) * 10}px)`;
290
+ }
291
+ }
292
+
293
+ renderPageContent(page, index) {
294
+ switch(page.type) {
295
+ case 'cover':
296
+ return `
297
+ <div class="content-animation">
298
+ <h1>${this.title}</h1>
299
+ <div class="subtitle">${page.content}</div>
300
+ </div>
301
+ `;
302
+
303
+ case 'back':
304
+ return `
305
+ <div class="content-animation">
306
+ <h2>${page.content}</h2>
307
+ <div style="margin-top: 2rem; font-size: 1.1rem; opacity: 0.8;">
308
+ The End
309
+ </div>
310
+ </div>
311
+ `;
312
+
313
+ default:
314
+ return `
315
+ <div class="content-animation comic-panel">
316
+ <h3 style="margin-bottom: 1rem;">${page.content}</h3>
317
+ <p style="font-size: 1rem; line-height: 1.6; opacity: 0.9;">
318
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
319
+ Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
320
+ </p>
321
+ </div>
322
+ `;
323
+ }
324
+ }
325
+
326
+ setupNavigation() {
327
+ const prevBtn = this.shadowRoot.getElementById('prevBtn');
328
+ const nextBtn = this.shadowRoot.getElementById('nextBtn');
329
+
330
+ prevBtn.addEventListener('click', () => this.previousPage());
331
+ nextBtn.addEventListener('click', () => this.nextPage());
332
+
333
+ this.updateNavigationButtons();
334
+ }
335
+
336
+ setupKeyboardNav() {
337
+ this.addEventListener('keydown', (e) => {
338
+ if (e.key === 'ArrowLeft') {
339
+ this.previousPage();
340
+ } else if (e.key === 'ArrowRight') {
341
+ this.nextPage();
342
+ }
343
+ });
344
+
345
+ this.setAttribute('tabindex', '0');
346
+ this.focus();
347
+ }
348
+
349
+ previousPage() {
350
+ if (this.currentPage > 0) {
351
+ this.currentPage--;
352
+ this.updateDisplay();
353
+ this.addRippleEffect('left');
354
+ }
355
+ }
356
+
357
+ nextPage() {
358
+ if (this.currentPage < this.pages.length - 1) {
359
+ this.currentPage++;
360
+ this.updateDisplay();
361
+ this.addRippleEffect('right');
362
+ }
363
+ }
364
+
365
+ updateDisplay() {
366
+ const pageContainer = this.shadowRoot.getElementById('pageContainer');
367
+ pageContainer.innerHTML = this.renderPages();
368
+
369
+ this.updateNavigationButtons();
370
+
371
+ // Animate content
372
+ const content = pageContainer.querySelector('.content-animation');
373
+ if (content) {
374
+ content.style.animation = 'none';
375
+ setTimeout(() => {
376
+ content.style.animation = 'fadeInUp 0.8s ease-out';
377
+ }, 50);
378
+ }
379
+
380
+ // Update feather icons
381
+ setTimeout(() => {
382
+ if (window.feather) {
383
+ feather.replace({
384
+ 'stroke-width': 2,
385
+ width: 16,
386
+ height: 16
387
+ });
388
+ }
389
+ }, 0);
390
+ }
391
+
392
+ updateNavigationButtons() {
393
+ const prevBtn = this.shadowRoot.getElementById('prevBtn');
394
+ const nextBtn = this.shadowRoot.getElementById('nextBtn');
395
+
396
+ prevBtn.disabled = this.currentPage === 0;
397
+ nextBtn.disabled = this.currentPage === this.pages.length - 1;
398
+
399
+ // Update page indicator
400
+ this.shadowRoot.getElementById('currentPage').textContent = this.currentPage + 1;
401
+ }
402
+
403
+ addRippleEffect(direction) {
404
+ const ripple = document.createElement('div');
405
+ ripple.className = 'ripple';
406
+ ripple.style.width = '20px';
407
+ ripple.style.height = '20px';
408
+ ripple.style.left = direction === 'left' ? '20%' : '80%';
409
+ ripple.style.top = '50%';
410
+
411
+ this.shadowRoot.querySelector('.page').appendChild(ripple);
412
+
413
+ setTimeout(() => ripple.remove(), 600);
414
+ }
415
+
416
+ getGradientColors(color) {
417
+ const colors = {
418
+ blue: ['#3b82f6', '#1d4ed8'],
419
+ purple: ['#8b5cf6', '#6d28d9'],
420
+ green: ['#10b981', '#047857'],
421
+ red: ['#ef4444', '#dc2626'],
422
+ orange: ['#f97316', '#ea580c'],
423
+ pink: ['#ec4899', '#db2777'],
424
+ yellow: ['#f59e0b', '#d97706']
425
+ };
426
+ return colors[color] || colors.blue;
427
+ }
428
+
429
+ getColorValue(color) {
430
+ const colors = {
431
+ blue: '#3b82f6',
432
+ purple: '#8b5cf6',
433
+ green: '#10b981',
434
+ red: '#ef4444',
435
+ orange: '#f97316',
436
+ pink: '#ec4899',
437
+ yellow: '#f59e0b'
438
+ };
439
+ return colors[color] || colors.blue;
440
+ }
441
+ }
442
+
443
+ customElements.define('magazine-viewer', MagazineViewer);
components/tabletop-object.js ADDED
@@ -0,0 +1,278 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ class TabletopObject extends HTMLElement {
2
+ constructor() {
3
+ super();
4
+ this.attachShadow({ mode: 'open' });
5
+ }
6
+
7
+ connectedCallback() {
8
+ const type = this.getAttribute('type');
9
+ const title = this.getAttribute('title');
10
+ const color = this.getAttribute('color') || 'blue';
11
+ const image = this.getAttribute('image');
12
+
13
+ this.shadowRoot.innerHTML = `
14
+ <style>
15
+ :host {
16
+ display: block;
17
+ position: absolute;
18
+ cursor: pointer;
19
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
20
+ outline: none;
21
+ }
22
+
23
+ :host(:hover) {
24
+ transform: translateY(-8px) rotateZ(2deg);
25
+ filter: drop-shadow(0 20px 40px rgba(0, 0, 0, 0.3));
26
+ z-index: 100;
27
+ }
28
+
29
+ :host(:focus-visible) {
30
+ outline: 3px solid #3b82f6;
31
+ outline-offset: 2px;
32
+ }
33
+
34
+ .object-container {
35
+ width: 100%;
36
+ height: 100%;
37
+ position: relative;
38
+ }
39
+
40
+ /* Magazine styles */
41
+ .magazine {
42
+ background: linear-gradient(135deg, ${this.getColorShades(color)[0]} 0%, ${this.getColorShades(color)[1]} 100%);
43
+ border-radius: 4px;
44
+ box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);
45
+ position: relative;
46
+ overflow: hidden;
47
+ }
48
+
49
+ .magazine::before {
50
+ content: '';
51
+ position: absolute;
52
+ inset: 0;
53
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
54
+ transform: translateX(-100%);
55
+ }
56
+
57
+ .magazine:hover::before {
58
+ animation: shimmer 1.5s infinite;
59
+ }
60
+
61
+ @keyframes shimmer {
62
+ 100% { transform: translateX(100%); }
63
+ }
64
+
65
+ .magazine-title {
66
+ writing-mode: vertical-rl;
67
+ text-orientation: mixed;
68
+ color: white;
69
+ font-weight: bold;
70
+ font-size: 14px;
71
+ padding: 12px 8px;
72
+ text-align: center;
73
+ height: 100%;
74
+ display: flex;
75
+ align-items: center;
76
+ justify-content: center;
77
+ }
78
+
79
+ /* Resource/file styles */
80
+ .file-folder {
81
+ background: linear-gradient(135deg, #fbbf24 0%, #f59e0b 100%);
82
+ border-radius: 8px;
83
+ box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);
84
+ position: relative;
85
+ }
86
+
87
+ .file-folder::after {
88
+ content: '';
89
+ position: absolute;
90
+ top: 0;
91
+ right: 12px;
92
+ width: 30px;
93
+ height: 10px;
94
+ background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
95
+ border-radius: 0 0 8px 8px;
96
+ }
97
+
98
+ .file-label {
99
+ position: absolute;
100
+ bottom: 8px;
101
+ left: 8px;
102
+ right: 8px;
103
+ background: rgba(0, 0, 0, 0.7);
104
+ color: white;
105
+ padding: 4px 8px;
106
+ border-radius: 4px;
107
+ font-size: 12px;
108
+ }
109
+
110
+ /* Art frame styles */
111
+ .art-frame {
112
+ background: linear-gradient(135deg, #d97706 0%, #92400e 100%);
113
+ padding: 12px;
114
+ border-radius: 4px;
115
+ box-shadow:
116
+ inset 0 0 20px rgba(0, 0, 0, 0.3),
117
+ 0 10px 30px rgba(0, 0, 0, 0.2);
118
+ }
119
+
120
+ .art-canvas {
121
+ background: #f3f4f6;
122
+ border-radius: 2px;
123
+ overflow: hidden;
124
+ position: relative;
125
+ }
126
+
127
+ .art-image {
128
+ width: 100%;
129
+ height: 100%;
130
+ object-fit: cover;
131
+ }
132
+
133
+ .art-title {
134
+ position: absolute;
135
+ bottom: 0;
136
+ left: 0;
137
+ right: 0;
138
+ background: linear-gradient(to top, rgba(0, 0, 0, 0.8), transparent);
139
+ color: white;
140
+ padding: 12px 8px 8px;
141
+ font-size: 12px;
142
+ font-weight: bold;
143
+ }
144
+
145
+ /* Demo device styles */
146
+ .device {
147
+ background: #1f2937;
148
+ border-radius: 20px;
149
+ padding: 20px;
150
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
151
+ position: relative;
152
+ }
153
+
154
+ .device-screen {
155
+ background: #000;
156
+ border-radius: 10px;
157
+ overflow: hidden;
158
+ }
159
+
160
+ .device-image {
161
+ width: 100%;
162
+ height: 100%;
163
+ object-fit: cover;
164
+ }
165
+
166
+ .device-title {
167
+ position: absolute;
168
+ bottom: -32px;
169
+ left: 50%;
170
+ transform: translateX(-50%);
171
+ background: rgba(255, 255, 255, 0.9);
172
+ backdrop-filter: blur(8px);
173
+ padding: 6px 16px;
174
+ border-radius: 20px;
175
+ font-size: 14px;
176
+ font-weight: 500;
177
+ white-space: nowrap;
178
+ }
179
+ </style>
180
+
181
+ <div class="object-container">
182
+ ${this.renderObject(type, title, color, image)}
183
+ </div>
184
+ `;
185
+
186
+ this.setupInteractions();
187
+ }
188
+
189
+ getColorShades(color) {
190
+ const colorMap = {
191
+ blue: ['#3b82f6', '#1d4ed8'],
192
+ purple: ['#8b5cf6', '#6d28d9'],
193
+ green: ['#10b981', '#047857'],
194
+ red: ['#ef4444', '#dc2626'],
195
+ orange: ['#f97316', '#ea580c'],
196
+ pink: ['#ec4899', '#db2777'],
197
+ yellow: ['#f59e0b', '#d97706']
198
+ };
199
+ return colorMap[color] || colorMap.blue;
200
+ }
201
+
202
+ renderObject(type, title, color, image) {
203
+ switch(type) {
204
+ case 'magazine':
205
+ return `
206
+ <div class="magazine w-32 h-44">
207
+ <div class="magazine-title">${title}</div>
208
+ </div>
209
+ `;
210
+
211
+ case 'resource':
212
+ return `
213
+ <div class="file-folder w-24 h-32">
214
+ <div class="file-label">${title}</div>
215
+ </div>
216
+ `;
217
+
218
+ case 'art':
219
+ return `
220
+ <div class="art-frame">
221
+ <div class="art-canvas w-48 h-32">
222
+ <img src="${image}" alt="${title}" class="art-image">
223
+ <div class="art-title">${title}</div>
224
+ </div>
225
+ </div>
226
+ `;
227
+
228
+ case 'demo':
229
+ return `
230
+ <div class="device w-64 h-40">
231
+ <div class="device-screen">
232
+ <img src="${image}" alt="${title}" class="device-image">
233
+ </div>
234
+ <div class="device-title">${title}</div>
235
+ </div>
236
+ `;
237
+
238
+ default:
239
+ return '<div>Unknown object type</div>';
240
+ }
241
+ }
242
+
243
+ setupInteractions() {
244
+ this.addEventListener('click', (e) => {
245
+ this.dispatchEvent(new CustomEvent('object-click', {
246
+ detail: {
247
+ id: this.getAttribute('object-id'),
248
+ type: this.getAttribute('type'),
249
+ title: this.getAttribute('title')
250
+ },
251
+ bubbles: true
252
+ }));
253
+ });
254
+
255
+ // Adding hover preview
256
+ this.addEventListener('mouseenter', () => {
257
+ this.dispatchEvent(new CustomEvent('object-hover', {
258
+ detail: {
259
+ id: this.getAttribute('object-id'),
260
+ type: this.getAttribute('type'),
261
+ title: this.getAttribute('title')
262
+ },
263
+ bubbles: true
264
+ }));
265
+ });
266
+
267
+ this.addEventListener('mouseleave', () => {
268
+ this.dispatchEvent(new CustomEvent('object-unhover', {
269
+ detail: {
270
+ id: this.getAttribute('object-id')
271
+ },
272
+ bubbles: true
273
+ }));
274
+ });
275
+ }
276
+ }
277
+
278
+ customElements.define('tabletop-object', TabletopObject);
index.html CHANGED
@@ -1,19 +1,100 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Tabletop Gallery Explorer</title>
7
+ <link rel="stylesheet" href="style.css">
8
+ <script src="https://cdn.tailwindcss.com"></script>
9
+ <script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
10
+ <script src="https://unpkg.com/feather-icons"></script>
11
+ </head>
12
+ <body class="bg-gradient-to-br from-amber-50 to-orange-50 dark:from-gray-900 dark:to-gray-800 transition-colors duration-500">
13
+ <!-- Header -->
14
+ <header class="fixed top-0 left-0 right-0 z-50 bg-white/90 dark:bg-gray-800/90 backdrop-blur-md border-b border-amber-200 dark:border-gray-700">
15
+ <div class="container mx-auto px-4 py-3 flex items-center justify-between">
16
+ <h1 class="text-2xl font-bold bg-gradient-to-r from-amber-600 to-orange-600 bg-clip-text text-transparent">
17
+ Tabletop Gallery
18
+ </h1>
19
+ <nav class="flex items-center gap-4">
20
+ <button id="searchBtn" class="p-2 rounded-lg hover:bg-amber-100 dark:hover:bg-gray-700 transition-colors">
21
+ <i data-feather="search" class="w-5 h-5"></i>
22
+ </button>
23
+ <button id="viewModeBtn" class="p-2 rounded-lg hover:bg-amber-100 dark:hover:bg-gray-700 transition-colors">
24
+ <i data-feather="grid" class="w-5 h-5"></i>
25
+ </button>
26
+ <button id="themeToggle" class="p-2 rounded-lg hover:bg-amber-100 dark:hover:bg-gray-700 transition-colors">
27
+ <i data-feather="sun" class="w-5 h-5 hidden dark:block"></i>
28
+ <i data-feather="moon" class="w-5 h-5 block dark:hidden"></i>
29
+ </button>
30
+ </nav>
31
+ </div>
32
+ </header>
33
+
34
+ <!-- Main Tabletop Canvas -->
35
+ <main class="relative h-screen overflow-hidden pt-16">
36
+ <div id="tabletop" class="absolute inset-0 cursor-grab active:cursor-grabbing">
37
+ <!-- Infinite Surface -->
38
+ <div id="surface" class="absolute w-[300vw] h-[300vh] top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2">
39
+ <!-- Surface Texture -->
40
+ <div class="absolute inset-0 opacity-20">
41
+ <div class="wood-grain"></div>
42
+ </div>
43
+
44
+ <!-- Content Objects Container -->
45
+ <div id="objectsContainer" class="relative w-full h-full">
46
+ <!-- Objects will be dynamically generated here -->
47
+ </div>
48
+ </div>
49
+ </div>
50
+
51
+ <!-- Zoom Controls -->
52
+ <div class="fixed bottom-6 right-6 flex flex-col gap-2 z-40">
53
+ <button id="zoomIn" class="p-3 bg-white dark:bg-gray-800 rounded-full shadow-lg hover:shadow-xl transition-all">
54
+ <i data-feather="zoom-in" class="w-5 h-5"></i>
55
+ </button>
56
+ <button id="zoomOut" class="p-3 bg-white dark:bg-gray-800 rounded-full shadow-lg hover:shadow-xl transition-all">
57
+ <i data-feather="zoom-out" class="w-5 h-5"></i>
58
+ </button>
59
+ <button id="resetView" class="p-3 bg-white dark:bg-gray-800 rounded-full shadow-lg hover:shadow-xl transition-all">
60
+ <i data-feather="home" class="w-5 h-5"></i>
61
+ </button>
62
+ </div>
63
+ </main>
64
+
65
+ <!-- Magazine View Modal -->
66
+ <div id="magazineModal" class="fixed inset-0 bg-black/90 z-50 hidden items-center justify-center p-4">
67
+ <div class="relative max-w-4xl w-full h-full flex items-center justify-center">
68
+ <button id="closeMagazine" class="absolute top-4 right-4 p-3 bg-white/20 rounded-full hover:bg-white/30 transition-colors z-10">
69
+ <i data-feather="x" class="w-6 h-6 text-white"></i>
70
+ </button>
71
+ <div id="magazineContent" class="w-full max-h-[90vh] overflow-hidden">
72
+ <!-- Magazine content will be loaded here -->
73
+ </div>
74
+ </div>
75
+ </div>
76
+
77
+ <!-- Search Modal -->
78
+ <div id="searchModal" class="fixed inset-0 bg-black/50 z-50 hidden items-center justify-center p-4">
79
+ <div class="bg-white dark:bg-gray-800 rounded-2xl p-6 max-w-2xl w-full">
80
+ <div class="flex items-center gap-4 mb-4">
81
+ <i data-feather="search" class="w-5 h-5 text-gray-400"></i>
82
+ <input type="text" id="searchInput" placeholder="Search magazines, resources, or art..."
83
+ class="flex-1 bg-transparent outline-none text-lg dark:text-white">
84
+ </div>
85
+ <div id="searchResults" class="space-y-2">
86
+ <!-- Search results will appear here -->
87
+ </div>
88
+ </div>
89
+ </div>
90
+
91
+ <!-- Component Scripts -->
92
+ <script src="components/tabletop-object.js"></script>
93
+ <script src="components/magazine-viewer.js"></script>
94
+
95
+ <!-- Main Scripts -->
96
+ <script src="script.js"></script>
97
+ <script>feather.replace();</script>
98
+ <script src="https://huggingface.co/deepsite/deepsite-badge.js"></script>
99
+ </body>
100
+ </html>
script.js ADDED
@@ -0,0 +1,643 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Tabletop State Management
2
+ class TabletopState {
3
+ constructor() {
4
+ this.scale = 1;
5
+ this.translateX = 0;
6
+ this.translateY = 0;
7
+ this.isDragging = false;
8
+ this.startX = 0;
9
+ this.startY = 0;
10
+ this.selectedObject = null;
11
+ this.isDarkMode = false;
12
+ this.viewMode = 'chronological'; // chronological, categorical, groups
13
+ this.objects = [];
14
+ this.filteredObjects = [];
15
+ }
16
+
17
+ setTransform(x, y, scale) {
18
+ this.translateX = x;
19
+ this.translateY = y;
20
+ this.scale = scale;
21
+ this.updateTransform();
22
+ }
23
+
24
+ updateTransform() {
25
+ const surface = document.getElementById('surface');
26
+ surface.style.transform = `translate(${this.translateX}px, ${this.translateY}px) scale(${this.scale})`;
27
+ }
28
+ }
29
+
30
+ // Initialize state
31
+ const state = new TabletopState();
32
+
33
+ // Sample data for demonstration
34
+ const sampleData = {
35
+ magazines: [
36
+ {
37
+ id: 'mag-1',
38
+ type: 'magazine',
39
+ title: 'Design Systems Quarterly',
40
+ excerpt: 'Exploring the latest trends in digital design systems',
41
+ date: '2024-01',
42
+ color: 'blue',
43
+ pages: [
44
+ { type: 'cover', content: 'Cover Design' },
45
+ { type: 'page', content: 'Understanding Design Tokens' },
46
+ { type: 'page', content: 'Component Library Best Practices' },
47
+ { type: 'back', content: 'Resources & Tools' }
48
+ ]
49
+ },
50
+ {
51
+ id: 'mag-2',
52
+ type: 'magazine',
53
+ title: 'Creative Coding',
54
+ excerpt: 'Art meets technology in creative programming',
55
+ date: '2024-02',
56
+ color: 'purple',
57
+ pages: [
58
+ { type: 'cover', content: 'Creative Coding Cover' },
59
+ { type: 'page', content: 'Generative Art Techniques' },
60
+ { type: 'page', content: 'Interactive Installations' },
61
+ { type: 'back', content: 'Gallery Showcase' }
62
+ ]
63
+ }
64
+ ],
65
+ resources: [
66
+ {
67
+ id: 'res-1',
68
+ type: 'resource',
69
+ title: 'Development Tools',
70
+ items: ['Code Editors', 'Version Control', 'Testing Frameworks', 'Deployment Tools']
71
+ },
72
+ {
73
+ id: 'res-2',
74
+ type: 'resource',
75
+ title: 'Design Assets',
76
+ items: ['Icons & Illustrations', 'Color Palettes', 'Typography', 'Stock Photos']
77
+ }
78
+ ],
79
+ art: [
80
+ {
81
+ id: 'art-1',
82
+ type: 'art',
83
+ title: 'Generative Landscape',
84
+ image: 'http://static.photos/nature/640x360/42',
85
+ description: 'Algorithmic nature scenes'
86
+ },
87
+ {
88
+ id: 'art-2',
89
+ type: 'art',
90
+ title: 'Abstract Geometry',
91
+ image: 'http://static.photos/abstract/640x360/133',
92
+ description: 'Mathematical beauty in art'
93
+ }
94
+ ],
95
+ demos: [
96
+ {
97
+ id: 'demo-1',
98
+ type: 'demo',
99
+ title: 'Interactive Dashboard',
100
+ preview: 'http://static.photos/technology/640x360/1'
101
+ }
102
+ ]
103
+ };
104
+
105
+ // Generate objects from sample data
106
+ function generateObjects() {
107
+ const objects = [];
108
+
109
+ // Generate magazines
110
+ sampleData.magazines.forEach((magazine, index) => {
111
+ objects.push({
112
+ ...magazine,
113
+ x: 200 + (index % 3) * 300,
114
+ y: 200 + Math.floor(index / 3) * 400,
115
+ rotation: Math.random() * 15 - 7.5
116
+ });
117
+ });
118
+
119
+ // Generate resources
120
+ sampleData.resources.forEach((resource, index) => {
121
+ objects.push({
122
+ ...resource,
123
+ x: 600 + (index % 2) * 200,
124
+ y: 800 + Math.floor(index / 2) * 200,
125
+ rotation: Math.random() * 10 - 5
126
+ });
127
+ });
128
+
129
+ // Generate art
130
+ sampleData.art.forEach((art, index) => {
131
+ objects.push({
132
+ ...art,
133
+ x: 1000 + (index % 2) * 250,
134
+ y: 300 + Math.floor(index / 2) * 300,
135
+ rotation: 0
136
+ });
137
+ });
138
+
139
+ // Generate demos
140
+ sampleData.demos.forEach((demo, index) => {
141
+ objects.push({
142
+ ...demo,
143
+ x: 1200,
144
+ y: 600,
145
+ rotation: 0
146
+ });
147
+ });
148
+
149
+ state.objects = objects;
150
+ state.filteredObjects = [...objects];
151
+ }
152
+
153
+ // Render objects on tabletop
154
+ function renderObjects() {
155
+ const container = document.getElementById('objectsContainer');
156
+ container.innerHTML = '';
157
+
158
+ state.filteredObjects.forEach(obj => {
159
+ const element = createTabletopObject(obj);
160
+ container.appendChild(element);
161
+ });
162
+ }
163
+
164
+ // Create individual tabletop object
165
+ function createTabletopObject(obj) {
166
+ const wrapper = document.createElement('div');
167
+ wrapper.className = 'tabletop-object absolute cursor-pointer';
168
+ wrapper.style.left = `${obj.x}px`;
169
+ wrapper.style.top = `${obj.y}px`;
170
+ wrapper.style.transform = `rotate(${obj.rotation}deg)`;
171
+ wrapper.dataset.objectId = obj.id;
172
+ wrapper.dataset.objectType = obj.type;
173
+ wrapper.tabIndex = 0;
174
+ wrapper.setAttribute('role', 'button');
175
+ wrapper.setAttribute('aria-label', `${obj.title} - ${obj.type}`);
176
+
177
+ let content = '';
178
+
179
+ switch(obj.type) {
180
+ case 'magazine':
181
+ content = `
182
+ <div class="magazine-spine w-32 h-44 rounded shadow-xl relative">
183
+ <div class="absolute inset-0 bg-gradient-to-r from-transparent via-white/10 to-transparent"></div>
184
+ <div class="p-3 h-full flex flex-col justify-between">
185
+ <div class="text-white text-xs font-bold transform -rotate-90 origin-center whitespace-nowrap">
186
+ ${obj.title}
187
+ </div>
188
+ <div class="text-white/70 text-xs">
189
+ ${obj.date}
190
+ </div>
191
+ </div>
192
+ </div>
193
+ `;
194
+ break;
195
+
196
+ case 'resource':
197
+ content = `
198
+ <div class="file-cabinet w-24 h-32 rounded shadow-lg relative">
199
+ <div class="absolute inset-2 bg-gray-700 rounded">
200
+ <div class="p-2">
201
+ <div class="bg-gray-600 h-1 mb-2 rounded"></div>
202
+ <div class="bg-gray-600 h-1 mb-2 rounded"></div>
203
+ <div class="bg-gray-600 h-1 mb-2 rounded"></div>
204
+ <div class="text-white text-xs mt-4">${obj.title}</div>
205
+ </div>
206
+ </div>
207
+ </div>
208
+ `;
209
+ break;
210
+
211
+ case 'art':
212
+ content = `
213
+ <div class="art-frame shadow-2xl">
214
+ <div class="art-canvas w-48 h-32 relative overflow-hidden">
215
+ <img src="${obj.image}" alt="${obj.title}" class="w-full h-full object-cover">
216
+ <div class="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/70 to-transparent p-2">
217
+ <div class="text-white text-xs font-semibold">${obj.title}</div>
218
+ </div>
219
+ </div>
220
+ </div>
221
+ `;
222
+ break;
223
+
224
+ case 'demo':
225
+ content = `
226
+ <div class="device-mockup shadow-2xl relative floating">
227
+ <div class="device-screen w-64 h-40 relative overflow-hidden">
228
+ <img src="${obj.preview}" alt="${obj.title}" class="w-full h-full object-cover">
229
+ </div>
230
+ <div class="absolute -bottom-6 left-0 right-0 text-center">
231
+ <div class="text-gray-700 dark:text-gray-300 text-sm bg-white/80 dark:bg-gray-800/80 backdrop-blur-sm rounded-full px-3 py-1 inline-block">
232
+ ${obj.title}
233
+ </div>
234
+ </div>
235
+ </div>
236
+ `;
237
+ break;
238
+ }
239
+
240
+ wrapper.innerHTML = content;
241
+
242
+ // Add event listeners
243
+ wrapper.addEventListener('click', () => handleObjectClick(obj));
244
+ wrapper.addEventListener('keydown', (e) => {
245
+ if (e.key === 'Enter' || e.key === ' ') {
246
+ handleObjectClick(obj);
247
+ }
248
+ });
249
+
250
+ // Add drag functionality
251
+ let isDragging = false;
252
+ let startX, startY, initialX, initialY;
253
+
254
+ wrapper.addEventListener('mousedown', (e) => {
255
+ isDragging = true;
256
+ startX = e.clientX;
257
+ startY = e.clientY;
258
+ initialX = obj.x;
259
+ initialY = obj.y;
260
+ wrapper.classList.add('dragging');
261
+ e.stopPropagation();
262
+ });
263
+
264
+ document.addEventListener('mousemove', (e) => {
265
+ if (!isDragging) return;
266
+
267
+ const dx = (e.clientX - startX) / state.scale;
268
+ const dy = (e.clientY - startY) / state.scale;
269
+
270
+ obj.x = initialX + dx;
271
+ obj.y = initialY + dy;
272
+
273
+ wrapper.style.left = `${obj.x}px`;
274
+ wrapper.style.top = `${obj.y}px`;
275
+ });
276
+
277
+ document.addEventListener('mouseup', () => {
278
+ if (isDragging) {
279
+ isDragging = false;
280
+ wrapper.classList.remove('dragging');
281
+ saveLayout();
282
+ }
283
+ });
284
+
285
+ return wrapper;
286
+ }
287
+
288
+ // Handle object click
289
+ function handleObjectClick(obj) {
290
+ state.selectedObject = obj;
291
+
292
+ switch(obj.type) {
293
+ case 'magazine':
294
+ openMagazineViewer(obj);
295
+ break;
296
+ case 'resource':
297
+ openResourceViewer(obj);
298
+ break;
299
+ case 'art':
300
+ openArtViewer(obj);
301
+ break;
302
+ case 'demo':
303
+ openDemoViewer(obj);
304
+ break;
305
+ }
306
+ }
307
+
308
+ // Magazine Viewer
309
+ function openMagazineViewer(magazine) {
310
+ const modal = document.getElementById('magazineModal');
311
+ const content = document.getElementById('magazineContent');
312
+
313
+ content.innerHTML = `
314
+ <magazine-viewer
315
+ title="${magazine.title}"
316
+ pages='${JSON.stringify(magazine.pages)}'
317
+ color="${magazine.color}">
318
+ </magazine-viewer>
319
+ `;
320
+
321
+ modal.classList.remove('hidden');
322
+ modal.classList.add('flex');
323
+ }
324
+
325
+ // Resource Viewer (placeholder)
326
+ function openResourceViewer(resource) {
327
+ alert(`Opening ${resource.title}\nItems: ${resource.items.join(', ')}`);
328
+ }
329
+
330
+ // Art Viewer (placeholder)
331
+ function openArtViewer(art) {
332
+ alert(`Viewing ${art.title}\nDescription: ${art.description}`);
333
+ }
334
+
335
+ // Demo Viewer (placeholder)
336
+ function openDemoViewer(demo) {
337
+ alert(`Launching ${demo.title}`);
338
+ }
339
+
340
+ // Pan and Zoom functionality
341
+ function setupPanZoom() {
342
+ const tabletop = document.getElementById('tabletop');
343
+ let lastX = 0;
344
+ let lastY = 0;
345
+
346
+ // Pan with mouse
347
+ tabletop.addEventListener('mousedown', (e) => {
348
+ if (e.target === tabletop || e.target.id === 'surface') {
349
+ state.isDragging = true;
350
+ state.startX = e.clientX - state.translateX;
351
+ state.startY = e.clientY - state.translateY;
352
+ tabletop.style.cursor = 'grabbing';
353
+ }
354
+ });
355
+
356
+ document.addEventListener('mousemove', (e) => {
357
+ if (!state.isDragging) return;
358
+
359
+ e.preventDefault();
360
+ state.translateX = e.clientX - state.startX;
361
+ state.translateY = e.clientY - state.startY;
362
+ state.updateTransform();
363
+ });
364
+
365
+ document.addEventListener('mouseup', () => {
366
+ state.isDragging = false;
367
+ tabletop.style.cursor = 'grab';
368
+ });
369
+
370
+ // Zoom with mouse wheel
371
+ tabletop.addEventListener('wheel', (e) => {
372
+ e.preventDefault();
373
+
374
+ const delta = e.deltaY > 0 ? 0.9 : 1.1;
375
+ const newScale = Math.min(Math.max(state.scale * delta, 0.5), 3);
376
+
377
+ const rect = tabletop.getBoundingClientRect();
378
+ const x = e.clientX - rect.left;
379
+ const y = e.clientY - rect.top;
380
+
381
+ state.translateX = x - (x - state.translateX) * (newScale / state.scale);
382
+ state.translateY = y - (y - state.translateY) * (newScale / state.scale);
383
+ state.scale = newScale;
384
+
385
+ state.updateTransform();
386
+ });
387
+
388
+ // Zoom controls
389
+ document.getElementById('zoomIn').addEventListener('click', () => {
390
+ state.scale = Math.min(state.scale * 1.2, 3);
391
+ state.updateTransform();
392
+ });
393
+
394
+ document.getElementById('zoomOut').addEventListener('click', () => {
395
+ state.scale = Math.max(state.scale * 0.8, 0.5);
396
+ state.updateTransform();
397
+ });
398
+
399
+ document.getElementById('resetView').addEventListener('click', () => {
400
+ state.setTransform(0, 0, 1);
401
+ });
402
+ }
403
+
404
+ // Touch support
405
+ function setupTouchSupport() {
406
+ const tabletop = document.getElementById('tabletop');
407
+ let touchStartX, touchStartY;
408
+ let initialDistance = 0;
409
+ let initialScale = 1;
410
+
411
+ tabletop.addEventListener('touchstart', (e) => {
412
+ if (e.touches.length === 2) {
413
+ const dx = e.touches[0].clientX - e.touches[1].clientX;
414
+ const dy = e.touches[0].clientY - e.touches[1].clientY;
415
+ initialDistance = Math.sqrt(dx * dx + dy * dy);
416
+ initialScale = state.scale;
417
+ } else if (e.touches.length === 1) {
418
+ touchStartX = e.touches[0].clientX - state.translateX;
419
+ touchStartY = e.touches[0].clientY - state.translateY;
420
+ }
421
+ });
422
+
423
+ tabletop.addEventListener('touchmove', (e) => {
424
+ e.preventDefault();
425
+
426
+ if (e.touches.length === 2) {
427
+ const dx = e.touches[0].clientX - e.touches[1].clientX;
428
+ const dy = e.touches[0].clientY - e.touches[1].clientY;
429
+ const distance = Math.sqrt(dx * dx + dy * dy);
430
+ const scale = (distance / initialDistance) * initialScale;
431
+
432
+ state.scale = Math.min(Math.max(scale, 0.5), 3);
433
+ state.updateTransform();
434
+ } else if (e.touches.length === 1) {
435
+ state.translateX = e.touches[0].clientX - touchStartX;
436
+ state.translateY = e.touches[0].clientY - touchStartY;
437
+ state.updateTransform();
438
+ }
439
+ });
440
+ }
441
+
442
+ // Search functionality
443
+ function setupSearch() {
444
+ const searchBtn = document.getElementById('searchBtn');
445
+ const searchModal = document.getElementById('searchModal');
446
+ const searchInput = document.getElementById('searchInput');
447
+ const searchResults = document.getElementById('searchResults');
448
+
449
+ searchBtn.addEventListener('click', () => {
450
+ searchModal.classList.remove('hidden');
451
+ searchModal.classList.add('flex');
452
+ searchInput.focus();
453
+ });
454
+
455
+ searchModal.addEventListener('click', (e) => {
456
+ if (e.target === searchModal) {
457
+ searchModal.classList.add('hidden');
458
+ searchModal.classList.remove('flex');
459
+ }
460
+ });
461
+
462
+ searchInput.addEventListener('input', (e) => {
463
+ const query = e.target.value.toLowerCase();
464
+
465
+ if (query.length < 2) {
466
+ searchResults.innerHTML = '';
467
+ return;
468
+ }
469
+
470
+ const results = state.objects.filter(obj =>
471
+ obj.title.toLowerCase().includes(query) ||
472
+ (obj.excerpt && obj.excerpt.toLowerCase().includes(query))
473
+ );
474
+
475
+ searchResults.innerHTML = results.map(obj => `
476
+ <div class="p-3 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg cursor-pointer transition-colors"
477
+ onclick="focusOnObject('${obj.id}')">
478
+ <div class="font-semibold">${obj.title}</div>
479
+ <div class="text-sm text-gray-500 dark:text-gray-400">${obj.type}</div>
480
+ </div>
481
+ `).join('');
482
+ });
483
+ }
484
+
485
+ // Focus on object by ID
486
+ function focusOnObject(id) {
487
+ const obj = state.objects.find(o => o.id === id);
488
+ if (!obj) return;
489
+
490
+ state.setTransform(
491
+ -(obj.x - window.innerWidth / 4),
492
+ -(obj.y - window.innerHeight / 4),
493
+ 1.5
494
+ );
495
+
496
+ document.getElementById('searchModal').classList.add('hidden');
497
+ document.getElementById('searchModal').classList.remove('flex');
498
+ }
499
+
500
+ // Theme toggle
501
+ function setupThemeToggle() {
502
+ const themeToggle = document.getElementById('themeToggle');
503
+ const html = document.documentElement;
504
+
505
+ // Check for saved theme preference or default to light mode
506
+ const savedTheme = localStorage.getItem('theme');
507
+ if (savedTheme === 'dark' || (!savedTheme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
508
+ html.classList.add('dark');
509
+ state.isDarkMode = true;
510
+ }
511
+
512
+ themeToggle.addEventListener('click', () => {
513
+ html.classList.toggle('dark');
514
+ state.isDarkMode = !state.isDarkMode;
515
+ localStorage.setItem('theme', state.isDarkMode ? 'dark' : 'light');
516
+ });
517
+ }
518
+
519
+ // View mode toggle
520
+ function setupViewMode() {
521
+ const viewModeBtn = document.getElementById('viewModeBtn');
522
+
523
+ viewModeBtn.addEventListener('click', () => {
524
+ const modes = ['chronological', 'categorical', 'groups'];
525
+ const currentIndex = modes.indexOf(state.viewMode);
526
+ state.viewMode = modes[(currentIndex + 1) % modes.length];
527
+
528
+ renderObjects();
529
+ });
530
+ }
531
+
532
+ // Close magazine modal
533
+ document.getElementById('closeMagazine').addEventListener('click', () => {
534
+ const modal = document.getElementById('magazineModal');
535
+ modal.classList.add('hidden');
536
+ modal.classList.remove('flex');
537
+ });
538
+
539
+ // Keyboard navigation
540
+ function setupKeyboardNav() {
541
+ document.addEventListener('keydown', (e) => {
542
+ if (e.key === 'Escape') {
543
+ // Close any open modals
544
+ const modals = document.querySelectorAll('.modal:not(.hidden)');
545
+ modals.forEach(modal => {
546
+ modal.classList.add('hidden');
547
+ modal.classList.remove('flex');
548
+ });
549
+ }
550
+
551
+ if (e.key === '/' && !e.ctrlKey && !e.metaKey) {
552
+ e.preventDefault();
553
+ document.getElementById('searchBtn').click();
554
+ }
555
+
556
+ // Arrow keys for navigation
557
+ if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) {
558
+ e.preventDefault();
559
+ const step = e.shiftKey ? 50 : 10;
560
+
561
+ switch(e.key) {
562
+ case 'ArrowUp':
563
+ state.translateY += step;
564
+ break;
565
+ case 'ArrowDown':
566
+ state.translateY -= step;
567
+ break;
568
+ case 'ArrowLeft':
569
+ state.translateX += step;
570
+ break;
571
+ case 'ArrowRight':
572
+ state.translateX -= step;
573
+ break;
574
+ }
575
+
576
+ state.updateTransform();
577
+ }
578
+ });
579
+ }
580
+
581
+ // Save layout to localStorage
582
+ function saveLayout() {
583
+ localStorage.setItem('tabletopLayout', JSON.stringify({
584
+ objects: state.objects,
585
+ viewMode: state.viewMode,
586
+ transform: {
587
+ x: state.translateX,
588
+ y: state.translateY,
589
+ scale: state.scale
590
+ }
591
+ }));
592
+ }
593
+
594
+ // Load layout from localStorage
595
+ function loadLayout() {
596
+ const saved = localStorage.getItem('tabletopLayout');
597
+ if (saved) {
598
+ const data = JSON.parse(saved);
599
+ state.objects = data.objects || state.objects;
600
+ state.viewMode = data.viewMode || state.viewMode;
601
+
602
+ if (data.transform) {
603
+ state.setTransform(data.transform.x, data.transform.y, data.transform.scale);
604
+ }
605
+ }
606
+ }
607
+
608
+ // Initialize everything
609
+ document.addEventListener('DOMContentLoaded', () => {
610
+ generateObjects();
611
+ loadLayout();
612
+ renderObjects();
613
+ setupPanZoom();
614
+ setupTouchSupport();
615
+ setupSearch();
616
+ setupThemeToggle();
617
+ setupViewMode();
618
+ setupKeyboardNav();
619
+
620
+ // Close magazine modal on escape
621
+ document.getElementById('magazineModal').addEventListener('click', (e) => {
622
+ if (e.target === document.getElementById('magazineModal')) {
623
+ document.getElementById('closeMagazine').click();
624
+ }
625
+ });
626
+ });
627
+
628
+ // Handle window resize
629
+ window.addEventListener('resize', () => {
630
+ saveLayout();
631
+ });
632
+
633
+ // Save layout periodically
634
+ setInterval(saveLayout, 10000);
635
+
636
+ // Offline capability
637
+ if ('serviceWorker' in navigator) {
638
+ window.addEventListener('load', () => {
639
+ navigator.serviceWorker.register('/sw.js').catch(() => {
640
+ console.log('Service worker registration failed');
641
+ });
642
+ });
643
+ }
style.css CHANGED
@@ -1,28 +1,203 @@
1
- body {
2
- padding: 2rem;
3
- font-family: -apple-system, BlinkMacSystemFont, "Arial", sans-serif;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  }
5
 
6
- h1 {
7
- font-size: 16px;
8
- margin-top: 0;
9
  }
10
 
11
- p {
12
- color: rgb(107, 114, 128);
13
- font-size: 15px;
14
- margin-bottom: 10px;
15
- margin-top: 5px;
16
  }
17
 
18
- .card {
19
- max-width: 620px;
20
- margin: 0 auto;
21
- padding: 16px;
22
- border: 1px solid lightgray;
23
- border-radius: 16px;
24
  }
25
 
26
- .card p:last-child {
27
- margin-bottom: 0;
28
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Custom Wood Grain Effect */
2
+ .wood-grain {
3
+ position: absolute;
4
+ inset: 0;
5
+ background-image:
6
+ repeating-linear-gradient(90deg,
7
+ transparent,
8
+ transparent 50px,
9
+ rgba(139, 69, 19, 0.1) 50px,
10
+ rgba(139, 69, 19, 0.1) 51px
11
+ ),
12
+ repeating-linear-gradient(0deg,
13
+ transparent,
14
+ transparent 100px,
15
+ rgba(160, 82, 45, 0.05) 100px,
16
+ rgba(160, 82, 45, 0.05) 101px
17
+ );
18
  }
19
 
20
+ /* Custom Scrollbar for Dark Mode */
21
+ .dark::-webkit-scrollbar {
22
+ width: 12px;
23
  }
24
 
25
+ .dark::-webkit-scrollbar-track {
26
+ background: #1f2937;
 
 
 
27
  }
28
 
29
+ .dark::-webkit-scrollbar-thumb {
30
+ background: #4b5563;
31
+ border-radius: 6px;
 
 
 
32
  }
33
 
34
+ .dark::-webkit-scrollbar-thumb:hover {
35
+ background: #6b7280;
36
  }
37
+
38
+ /* Magazine Animation */
39
+ @keyframes magazineOpen {
40
+ from {
41
+ transform: scale(0.8) rotateY(-90deg);
42
+ opacity: 0;
43
+ }
44
+ to {
45
+ transform: scale(1) rotateY(0);
46
+ opacity: 1;
47
+ }
48
+ }
49
+
50
+ .magazine-open {
51
+ animation: magazineOpen 0.5s ease-out;
52
+ }
53
+
54
+ /* Object Hover Effects */
55
+ .tabletop-object {
56
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
57
+ }
58
+
59
+ .tabletop-object:hover {
60
+ transform: translateY(-8px) rotateZ(2deg);
61
+ filter: drop-shadow(0 20px 40px rgba(0, 0, 0, 0.3));
62
+ }
63
+
64
+ .tabletop-object.dragging {
65
+ transform: scale(1.1) rotateZ(5deg);
66
+ z-index: 1000;
67
+ filter: drop-shadow(0 30px 60px rgba(0, 0, 0, 0.4));
68
+ }
69
+
70
+ /* Magazine Spine */
71
+ .magazine-spine {
72
+ background: linear-gradient(135deg, #1e293b 0%, #334155 50%, #1e293b 100%);
73
+ position: relative;
74
+ overflow: hidden;
75
+ }
76
+
77
+ .magazine-spine::before {
78
+ content: '';
79
+ position: absolute;
80
+ top: 0;
81
+ left: 0;
82
+ right: 0;
83
+ bottom: 0;
84
+ background: repeating-linear-gradient(
85
+ 90deg,
86
+ transparent,
87
+ transparent 2px,
88
+ rgba(255, 255, 255, 0.1) 2px,
89
+ rgba(255, 255, 255, 0.1) 4px
90
+ );
91
+ }
92
+
93
+ /* File Cabinet Texture */
94
+ .file-cabinet {
95
+ background: linear-gradient(135deg, #6b7280 0%, #4b5563 50%, #374151 100%);
96
+ position: relative;
97
+ }
98
+
99
+ .file-cabinet::after {
100
+ content: '';
101
+ position: absolute;
102
+ top: 10%;
103
+ left: 10%;
104
+ right: 10%;
105
+ height: 2px;
106
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
107
+ }
108
+
109
+ /* Art Frame */
110
+ .art-frame {
111
+ background: linear-gradient(135deg, #d97706 0%, #92400e 100%);
112
+ padding: 12px;
113
+ box-shadow:
114
+ inset 0 0 20px rgba(0, 0, 0, 0.3),
115
+ 0 10px 30px rgba(0, 0, 0, 0.2);
116
+ }
117
+
118
+ .art-canvas {
119
+ background: #f3f4f6;
120
+ box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.1);
121
+ }
122
+
123
+ /* Sticky Note */
124
+ .sticky-note {
125
+ transform: rotate(-3deg);
126
+ transition: transform 0.2s ease;
127
+ }
128
+
129
+ .sticky-note:hover {
130
+ transform: rotate(-1deg) scale(1.05);
131
+ }
132
+
133
+ /* Device Mockup */
134
+ .device-mockup {
135
+ background: #1f2937;
136
+ border-radius: 20px;
137
+ padding: 20px;
138
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
139
+ }
140
+
141
+ .device-screen {
142
+ background: #000;
143
+ border-radius: 10px;
144
+ overflow: hidden;
145
+ }
146
+
147
+ /* Physics-based animations */
148
+ @keyframes gentleFloat {
149
+ 0%, 100% { transform: translateY(0) rotateZ(0); }
150
+ 50% { transform: translateY(-5px) rotateZ(1deg); }
151
+ }
152
+
153
+ .floating {
154
+ animation: gentleFloat 4s ease-in-out infinite;
155
+ }
156
+
157
+ /* Loading States */
158
+ .loading-shimmer {
159
+ background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
160
+ background-size: 200% 100%;
161
+ animation: shimmer 2s infinite;
162
+ }
163
+
164
+ @keyframes shimmer {
165
+ 0% { background-position: -200% 0; }
166
+ 100% { background-position: 200% 0; }
167
+ }
168
+
169
+ /* Accessibility Focus States */
170
+ *:focus-visible {
171
+ outline: 3px solid #3b82f6;
172
+ outline-offset: 2px;
173
+ }
174
+
175
+ /* Touch Device Optimizations */
176
+ @media (hover: none) {
177
+ .tabletop-object:hover {
178
+ transform: none;
179
+ filter: none;
180
+ }
181
+
182
+ .tabletop-object:active {
183
+ transform: scale(1.05);
184
+ filter: drop-shadow(0 15px 30px rgba(0, 0, 0, 0.3));
185
+ }
186
+ }
187
+
188
+ /* Print Styles */
189
+ @media print {
190
+ body {
191
+ background: white;
192
+ }
193
+
194
+ #tabletop {
195
+ position: static;
196
+ overflow: visible;
197
+ }
198
+
199
+ .tabletop-object {
200
+ break-inside: avoid;
201
+ page-break-inside: avoid;
202
+ }
203
+ }