diff --git a/examples/css/browser.css b/examples/css/browser.css
index 6ff3009..e424f20 100644
--- a/examples/css/browser.css
+++ b/examples/css/browser.css
@@ -4,31 +4,48 @@
box-sizing: border-box;
}
+:root {
+ color-scheme: dark;
+
+ --color-bg: #121212; /* page base / behind iframe */
+ --color-surface: #1a1a1a; /* sidebar */
+ --color-surface-raised: #242424; /* search input, QR modal, hamburger */
+ --color-border: #333;
+ --color-text: #e0e0e0;
+ --color-text-dim: #999;
+ --color-accent: #f60; /* PlayCanvas orange */
+ --color-accent-soft: rgba(255, 102, 0, 0.15);
+ --color-focus: #ff8533;
+}
+
/* Base layout */
body {
font-family: system-ui, -apple-system, sans-serif;
display: grid;
- grid-template-columns: minmax(250px, max-content) 1fr;
+ grid-template-columns: 300px 1fr;
+ background: var(--color-bg);
+ color: var(--color-text);
+}
+
+:focus-visible {
+ outline: 2px solid var(--color-focus);
+ outline-offset: -2px;
}
/* Hide menu toggle by default */
.menu-toggle {
display: none;
- color: #2c3e50;
+ color: var(--color-text);
}
/* Sidebar */
.sidebar {
- background: #f5f5f5;
- padding: 20px;
- padding-top: max(20px, env(safe-area-inset-top));
- padding-left: max(20px, env(safe-area-inset-left));
- padding-bottom: max(20px, env(safe-area-inset-bottom));
- border-right: 1px solid #ddd;
- overflow-y: auto;
- min-width: 250px;
- width: max-content;
+ display: flex;
+ flex-direction: column;
+ background: var(--color-surface);
+ border-right: 1px solid var(--color-border);
height: 100dvh;
+ padding-left: env(safe-area-inset-left);
}
/* Header styles */
@@ -36,43 +53,153 @@ body {
display: flex;
align-items: center;
gap: 12px;
- margin-bottom: 24px;
+ padding: 20px 16px 16px;
+ padding-top: max(20px, env(safe-area-inset-top));
}
.logo {
width: 40px;
height: 40px;
+ border-radius: 8px;
}
.header h2 {
- font-size: 18px;
- line-height: 1.2;
+ font-size: 16px;
+ line-height: 1.25;
font-weight: 600;
- color: #2c3e50;
+ color: var(--color-text);
+}
+
+.github-link {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-left: auto;
+ padding: 6px;
+ border-radius: 6px;
+ color: var(--color-text-dim);
+ transition: color 0.2s;
+}
+
+.github-link:hover {
+ color: var(--color-text);
+}
+
+/* Search */
+.search {
+ display: flex;
+ align-items: center;
+ position: relative;
+ margin: 0 16px 12px;
+}
+
+.search-icon {
+ position: absolute;
+ left: 11px;
+ color: var(--color-text-dim);
+ pointer-events: none;
+}
+
+#search-input {
+ width: 100%;
+ padding: 8px 12px 8px 34px;
+ font-size: 13px;
+ font-family: inherit;
+ color: var(--color-text);
+ background: var(--color-surface-raised);
+ border: 1px solid var(--color-border);
+ border-radius: 6px;
+ outline: none;
+ transition: border-color 0.2s;
+}
+
+#search-input::placeholder {
+ color: var(--color-text-dim);
+}
+
+#search-input:focus {
+ border-color: var(--color-accent);
}
/* Example list styles */
+#example-list {
+ flex: 1;
+ overflow-y: auto;
+ padding: 0 12px;
+ padding-bottom: max(12px, env(safe-area-inset-bottom));
+ scrollbar-width: thin;
+ scrollbar-color: var(--color-border) transparent;
+}
+
+#example-list::-webkit-scrollbar {
+ width: 8px;
+}
+
+#example-list::-webkit-scrollbar-track {
+ background: transparent;
+}
+
+#example-list::-webkit-scrollbar-thumb {
+ background: var(--color-border);
+ border-radius: 4px;
+}
+
+#example-list::-webkit-scrollbar-thumb:hover {
+ background: #4a4a4a;
+}
+
+.category-title {
+ font-size: 11px;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ color: var(--color-text-dim);
+ margin: 16px 8px 4px;
+}
+
+.category:first-of-type .category-title {
+ margin-top: 4px;
+}
+
+.hidden {
+ display: none !important;
+}
+
+.no-results {
+ padding: 16px 8px;
+ font-size: 13px;
+ color: var(--color-text-dim);
+}
+
.example-link {
display: flex;
justify-content: space-between;
align-items: center;
- padding: 8px 12px;
- margin: 4px 0;
- border-radius: 4px;
- color: #2c3e50;
+ gap: 8px;
+ padding: 7px 8px 7px 12px;
+ margin: 1px 0;
+ border-radius: 6px;
+ color: var(--color-text);
+ font-size: 14px;
text-decoration: none;
- transition: background-color 0.2s;
+ transition: background-color 0.15s;
}
.example-link:hover {
- background-color: #ffe0cc;
+ background-color: var(--color-accent-soft);
}
.example-link.active {
- background-color: #ff8533;
+ background-color: var(--color-accent);
color: white;
}
+.link-buttons {
+ display: flex;
+ gap: 4px;
+ flex-shrink: 0;
+}
+
.qr-button,
.open-in-new {
display: flex;
@@ -92,7 +219,7 @@ body {
.qr-button:hover,
.open-in-new:hover {
opacity: 1;
- background: rgba(0, 0, 0, 0.1);
+ background: rgba(255, 255, 255, 0.1);
}
.qr-button svg,
@@ -110,6 +237,12 @@ body {
width: 100%;
height: 100%;
border: none;
+ background: var(--color-bg);
+}
+
+/* Sidebar overlay (mobile) */
+.sidebar-overlay {
+ display: none;
}
/* Mobile styles */
@@ -127,8 +260,8 @@ body {
left: max(16px, env(safe-area-inset-left));
width: 40px;
height: 40px;
- background: rgba(255, 255, 255, 0.9);
- border: 1px solid #ddd;
+ background: rgba(26, 26, 26, 0.9);
+ border: 1px solid var(--color-border);
border-radius: 8px;
z-index: 999;
cursor: pointer;
@@ -151,6 +284,15 @@ body {
.sidebar.open {
transform: translateX(0);
+ box-shadow: 0 0 40px rgba(0, 0, 0, 0.6);
+ }
+
+ .sidebar-overlay.open {
+ display: block;
+ position: fixed;
+ inset: 0;
+ background: rgba(0, 0, 0, 0.5);
+ z-index: 999;
}
.main {
@@ -168,14 +310,15 @@ body {
left: 50%;
transform: translate(-50%, -50%);
padding: 0;
- border: none;
- border-radius: 8px;
- background: white;
+ border: 1px solid var(--color-border);
+ border-radius: 12px;
+ background: var(--color-surface-raised);
+ color: var(--color-text);
margin: 0;
}
#qr-modal::backdrop {
- background: rgba(0, 0, 0, 0.5);
+ background: rgba(0, 0, 0, 0.7);
}
.qr-modal-content {
@@ -185,7 +328,14 @@ body {
.qr-modal-content p {
margin-top: 16px;
- color: #2c3e50;
+ font-size: 14px;
+ color: var(--color-text-dim);
+}
+
+#qr-code {
+ background: #fff;
+ padding: 12px;
+ border-radius: 8px;
}
#qr-code img {
diff --git a/examples/index.html b/examples/index.html
index 1712c9d..ae04edd 100644
--- a/examples/index.html
+++ b/examples/index.html
@@ -21,7 +21,7 @@
-
+
@@ -37,11 +37,21 @@
diff --git a/examples/js/browser.mjs b/examples/js/browser.mjs
index 7957e82..e6a8b29 100644
--- a/examples/js/browser.mjs
+++ b/examples/js/browser.mjs
@@ -6,28 +6,68 @@ class ExampleBrowser {
constructor() {
this.frame = document.getElementById('example-frame');
this.exampleList = document.getElementById('example-list');
+ this.searchInput = document.getElementById('search-input');
this.menuToggle = document.querySelector('.menu-toggle');
this.sidebar = document.querySelector('.sidebar');
this.overlay = document.querySelector('.sidebar-overlay');
+ // Per-example render state used by the search filter
+ this.entries = [];
+
this.setupNavigation();
this.createExampleList();
+ this.setupSearch();
this.loadInitialExample();
this.setupMobileMenu();
}
setupNavigation() {
- const loadExample = (path) => {
- this.frame.src = path;
- };
- this.updateURL = setupNavigation(loadExample).updateURL;
+ this.updateURL = setupNavigation(path => this.loadExample(path)).updateURL;
+ }
+
+ loadExample(path) {
+ // Use location.replace() rather than setting the iframe's src: it
+ // swaps the iframe's history entry instead of pushing a new one, so
+ // browser back/forward only steps through the hash entries
+ this.frame.contentWindow.location.replace(new URL(path, window.location.href));
}
createExampleList() {
+ // Group examples by category, preserving first-occurrence order
+ const categories = new Map();
examples.forEach((example) => {
- const link = this.createExampleLink(example);
- this.exampleList.appendChild(link);
+ if (!categories.has(example.category)) {
+ categories.set(example.category, []);
+ }
+ categories.get(example.category).push(example);
+ });
+
+ categories.forEach((categoryExamples, category) => {
+ const section = document.createElement('section');
+ section.className = 'category';
+
+ const title = document.createElement('h3');
+ title.className = 'category-title';
+ title.textContent = category;
+ section.appendChild(title);
+
+ categoryExamples.forEach((example) => {
+ const link = this.createExampleLink(example);
+ section.appendChild(link);
+ this.entries.push({
+ link,
+ searchText: `${example.name} ${example.category}`.toLowerCase()
+ });
+ });
+
+ this.exampleList.appendChild(section);
});
+
+ this.noResults = document.createElement('p');
+ this.noResults.className = 'no-results';
+ this.noResults.textContent = 'No examples found';
+ this.noResults.hidden = true;
+ this.exampleList.appendChild(this.noResults);
}
createExampleLink(example) {
@@ -49,8 +89,7 @@ class ExampleBrowser {
createButtonContainer(example) {
const container = document.createElement('div');
- container.style.display = 'flex';
- container.style.gap = '4px';
+ container.className = 'link-buttons';
container.appendChild(this.createQRButton(example));
container.appendChild(this.createOpenInNewButton(example));
@@ -98,7 +137,7 @@ class ExampleBrowser {
handleExampleClick(e, example, link) {
if (e.target.className !== 'open-in-new') {
e.preventDefault();
- this.frame.src = example.path;
+ this.loadExample(example.path);
this.updateURL(example.path);
document.querySelectorAll('.example-link')
.forEach(l => l.classList.remove('active'));
@@ -106,16 +145,62 @@ class ExampleBrowser {
}
}
+ setupSearch() {
+ this.searchInput.addEventListener('input', () => {
+ this.filterExamples(this.searchInput.value);
+ });
+
+ this.searchInput.addEventListener('keydown', (e) => {
+ if (e.key === 'Escape') {
+ e.preventDefault();
+ if (this.searchInput.value) {
+ this.searchInput.value = '';
+ this.filterExamples('');
+ } else {
+ this.searchInput.blur();
+ }
+ } else if (e.key === 'Enter') {
+ this.exampleList.querySelector('.example-link:not(.hidden)')?.click();
+ }
+ });
+
+ // Press / anywhere to focus the search box
+ document.addEventListener('keydown', (e) => {
+ if (e.key === '/' && document.activeElement !== this.searchInput) {
+ e.preventDefault();
+ this.searchInput.focus();
+ this.searchInput.select();
+ }
+ });
+ }
+
+ filterExamples(query) {
+ const q = query.trim().toLowerCase();
+ let visible = 0;
+
+ this.entries.forEach(({ link, searchText }) => {
+ const match = q === '' || searchText.includes(q);
+ link.classList.toggle('hidden', !match);
+ if (match) visible++;
+ });
+
+ this.exampleList.querySelectorAll('.category').forEach((section) => {
+ section.classList.toggle('hidden', !section.querySelector('.example-link:not(.hidden)'));
+ });
+
+ this.noResults.hidden = visible > 0;
+ }
+
loadInitialExample() {
if (examples.length > 0) {
const hash = window.location.hash.slice(1);
if (hash && examples.some(ex => ex.path === hash)) {
- this.frame.src = hash;
+ this.loadExample(hash);
document.querySelector(`a[href="#${hash}"]`)?.classList.add('active');
} else {
- this.frame.src = examples[0].path;
- this.updateURL(examples[0].path);
- this.exampleList.firstChild?.classList.add('active');
+ this.loadExample(examples[0].path);
+ this.updateURL(examples[0].path, true);
+ this.exampleList.querySelector('.example-link')?.classList.add('active');
}
}
}
diff --git a/examples/js/example-list.mjs b/examples/js/example-list.mjs
index 59ea915..23e785a 100644
--- a/examples/js/example-list.mjs
+++ b/examples/js/example-list.mjs
@@ -1,27 +1,37 @@
export const examples = [
- { name: 'Animation', path: 'animation.html' },
- { name: 'Annotations', path: 'annotations.html' },
- { name: 'AR Avatar', path: 'ar-avatar.html' },
- { name: 'Basic Shapes', path: 'basic-shapes.html' },
- { name: 'Basic Particles', path: 'basic-particles.html' },
- { name: 'Car Configurator', path: 'car-configurator.html' },
- { name: 'First Person Teleport', path: 'first-person-teleport.html' },
- { name: 'FPS Controller', path: 'fps-controller.html' },
- { name: 'Game: Falling Blocks', path: 'vibe-falling-blocks.html' },
- { name: 'GLB Loader', path: 'glb.html' },
- { name: 'Physics', path: 'physics.html' },
- { name: 'Physics Cluster', path: 'physics-cluster.html' },
- { name: 'Positional Sound', path: 'positional-sound.html' },
- { name: 'Shoe Configurator', path: 'shoe-configurator.html' },
- { name: 'Solar System', path: 'solar-system.html' },
- { name: 'Sound', path: 'sound.html' },
- { name: 'Splat: Annotations', path: 'splat-annotations.html' },
- { name: 'Splat: Simple', path: 'splat-simple.html' },
- { name: 'Splat: Flipbook', path: 'splat-flipbook.html' },
- { name: 'Text Elements', path: 'text.html' },
- { name: 'Text 3D', path: 'text3d.html' },
- { name: 'Tweening', path: 'tween.html' },
- { name: 'UI: Scroll View', path: 'ui-scroll-view.html' },
- { name: 'Video Recorder', path: 'video-recorder.html' },
- { name: 'Video Texture', path: 'video-texture.html' }
+ // Graphics
+ { name: 'Basic Shapes', path: 'basic-shapes.html', category: 'Graphics' },
+ { name: 'Basic Particles', path: 'basic-particles.html', category: 'Graphics' },
+ { name: 'GLB Loader', path: 'glb.html', category: 'Graphics' },
+ { name: 'Video Texture', path: 'video-texture.html', category: 'Graphics' },
+ { name: 'Video Recorder', path: 'video-recorder.html', category: 'Graphics' },
+ // Animation
+ { name: 'Animation', path: 'animation.html', category: 'Animation' },
+ { name: 'Tweening', path: 'tween.html', category: 'Animation' },
+ // Physics
+ { name: 'Physics', path: 'physics.html', category: 'Physics' },
+ { name: 'Physics Cluster', path: 'physics-cluster.html', category: 'Physics' },
+ { name: 'FPS Controller', path: 'fps-controller.html', category: 'Physics' },
+ // Sound
+ { name: 'Sound', path: 'sound.html', category: 'Sound' },
+ { name: 'Positional Sound', path: 'positional-sound.html', category: 'Sound' },
+ // UI & Text
+ { name: '2D Screen', path: 'screen.html', category: 'UI & Text' },
+ { name: 'Text', path: 'text.html', category: 'UI & Text' },
+ { name: '3D Text', path: 'text3d.html', category: 'UI & Text' },
+ { name: 'Scroll View', path: 'ui-scroll-view.html', category: 'UI & Text' },
+ // Gaussian Splatting
+ { name: 'Basic Splat', path: 'splat-simple.html', category: 'Gaussian Splatting' },
+ { name: 'Splat Annotations', path: 'splat-annotations.html', category: 'Gaussian Splatting' },
+ { name: 'Splat Flipbook', path: 'splat-flipbook.html', category: 'Gaussian Splatting' },
+ // XR
+ { name: 'AR Avatar', path: 'ar-avatar.html', category: 'XR' },
+ { name: 'AR Hand Gestures', path: 'ar-hand-gestures.html', category: 'XR' },
+ { name: 'First Person Teleport', path: 'first-person-teleport.html', category: 'XR' },
+ // Showcases
+ { name: 'Annotations', path: 'annotations.html', category: 'Showcases' },
+ { name: 'Car Configurator', path: 'car-configurator.html', category: 'Showcases' },
+ { name: 'Shoe Configurator', path: 'shoe-configurator.html', category: 'Showcases' },
+ { name: 'Solar System', path: 'solar-system.html', category: 'Showcases' },
+ { name: 'Falling Blocks', path: 'vibe-falling-blocks.html', category: 'Showcases' }
];
diff --git a/examples/js/navigation.mjs b/examples/js/navigation.mjs
index 33908b8..8835cbe 100644
--- a/examples/js/navigation.mjs
+++ b/examples/js/navigation.mjs
@@ -1,8 +1,12 @@
import { examples } from './example-list.mjs';
export function setupNavigation(loadExample) {
- function updateURL(path) {
- history.pushState(null, '', `#${path}`);
+ function updateURL(path, replace = false) {
+ if (replace) {
+ history.replaceState(null, '', `#${path}`);
+ } else {
+ history.pushState(null, '', `#${path}`);
+ }
}
window.addEventListener('popstate', () => {
@@ -18,7 +22,8 @@ export function setupNavigation(loadExample) {
document.addEventListener('keydown', (e) => {
if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
e.preventDefault();
- const links = Array.from(document.querySelectorAll('.example-link'));
+ const links = Array.from(document.querySelectorAll('.example-link:not(.hidden)'));
+ if (links.length === 0) return;
const currentIndex = links.findIndex(link => link.classList.contains('active'));
const nextIndex = e.key === 'ArrowUp' ?
Math.max(0, currentIndex - 1) :
diff --git a/examples/js/qr-code.mjs b/examples/js/qr-code.mjs
index 9cf5760..eecb8ca 100644
--- a/examples/js/qr-code.mjs
+++ b/examples/js/qr-code.mjs
@@ -1,3 +1,5 @@
+let backdropHandlerAttached = false;
+
export function showQRCode(path) {
const qr = window.qrcode(0, 'L');
const url = `${window.location.origin}${window.location.pathname}${path}`;
@@ -8,15 +10,18 @@ export function showQRCode(path) {
const qrDiv = document.getElementById('qr-code');
qrDiv.innerHTML = qr.createImgTag(4);
- // Add click handler to close on backdrop click
- modal.addEventListener('click', (e) => {
- const rect = modal.getBoundingClientRect();
- const isInDialog = (rect.top <= e.clientY && e.clientY <= rect.top + rect.height &&
- rect.left <= e.clientX && e.clientX <= rect.left + rect.width);
- if (!isInDialog) {
- modal.close();
- }
- });
+ // Add click handler (once) to close on backdrop click
+ if (!backdropHandlerAttached) {
+ modal.addEventListener('click', (e) => {
+ const rect = modal.getBoundingClientRect();
+ const isInDialog = (rect.top <= e.clientY && e.clientY <= rect.top + rect.height &&
+ rect.left <= e.clientX && e.clientX <= rect.left + rect.width);
+ if (!isInDialog) {
+ modal.close();
+ }
+ });
+ backdropHandlerAttached = true;
+ }
modal.showModal();
}
diff --git a/examples/manifest.json b/examples/manifest.json
index 4d2c98b..a256653 100644
--- a/examples/manifest.json
+++ b/examples/manifest.json
@@ -4,8 +4,8 @@
"description": "Examples showcasing PlayCanvas Web Components capabilities and features",
"start_url": "./index.html",
"display": "standalone",
- "background_color": "#ffffff",
- "theme_color": "#ffffff",
+ "background_color": "#121212",
+ "theme_color": "#1a1a1a",
"icons": [
{
"src": "img/playcanvas-192.png",