vl/demos/webapp.html
9.5 KB · 120 lines · html Raw
1 <!doctype html>
2 <html><head><meta charset="utf-8"><title>Acme Workspace</title>
3 <style>
4 body { font-family: "DejaVu Sans", Arial, sans-serif; margin: 0; background: #f4f6f9; color: #1d2733; }
5 .top { background: #24324a; color: #fff; padding: 12px 20px; display: flex; align-items: center; gap: 18px; font-size: 18px; }
6 .top .brand { font-weight: bold; margin-right: auto; }
7 .main { padding: 18px 24px; }
8 .btn { display: inline-block; padding: 9px 16px; margin: 4px 6px 4px 0; border-radius: 6px; border: 1px solid #c5ccd6; background: #fff; cursor: pointer; font-size: 15px; }
9 .top .btn { color: #1d2733; }
10 .btn.primary { background: #2f6fde; color: #fff; border-color: #2f6fde; }
11 .btn.danger { color: #b3261e; }
12 .row { background: #fff; border: 1px solid #dde2e8; border-radius: 6px; padding: 10px 14px; margin: 6px 0; cursor: pointer; display: flex; gap: 16px; font-size: 15px; }
13 .row b { width: 170px; }
14 .grid { display: grid; grid-template-columns: repeat(3, 230px); gap: 14px; }
15 .card { background: #fff; border: 1px solid #dde2e8; border-radius: 8px; padding: 12px; cursor: pointer; font-size: 15px; }
16 .card .img { height: 70px; border-radius: 6px; margin-bottom: 8px; }
17 .swatch { display: inline-block; width: 34px; height: 34px; border-radius: 50%; margin: 6px 10px 6px 0; border: 3px solid #fff; box-shadow: 0 0 0 1px #aaa; cursor: pointer; }
18 .swatch.sel { box-shadow: 0 0 0 3px #1d2733; }
19 .size { display: inline-block; padding: 8px 14px; border: 1px solid #aab; margin-right: 8px; border-radius: 4px; cursor: pointer; background: #fff; }
20 .size.sel { background: #1d2733; color: #fff; }
21 .side { width: 190px; float: left; }
22 .side .item { padding: 10px 12px; margin: 4px 0; border-radius: 6px; cursor: pointer; background: #fff; border: 1px solid #dde2e8; }
23 .side .item.sel { background: #dfe9fb; border-color: #2f6fde; }
24 .panel { margin-left: 215px; background: #fff; border: 1px solid #dde2e8; border-radius: 8px; padding: 16px 20px; min-height: 300px; }
25 .toggle { display: flex; align-items: center; justify-content: space-between; width: 470px; padding: 10px 0; border-bottom: 1px solid #eee; font-size: 15px; }
26 .sw { width: 52px; height: 28px; border-radius: 14px; background: #c9ced6; position: relative; cursor: pointer; }
27 .sw::after { content: ""; position: absolute; width: 22px; height: 22px; border-radius: 50%; background: #fff; top: 3px; left: 3px; }
28 .sw.on { background: #2e9d57; } .sw.on::after { left: 27px; }
29 .toast { margin-top: 14px; color: #2e9d57; font-weight: bold; }
30 h2 { margin: 6px 0 12px; font-size: 21px; }
31 </style></head>
32 <body><div id="app"></div>
33 <script>
34 // A tiny three-app workspace (mail, shop, settings) driven by window.S; every interactive element carries data-click.
35 const P = new URLSearchParams(location.search);
36 let seed = +(P.get("seed") || 1);
37 function rnd() { seed = (seed * 1103515245 + 12345) % 2147483648; return seed / 2147483648; }
38 function shuffle(a) { for (let i = a.length - 1; i > 0; i--) { const j = Math.floor(rnd() * (i + 1)); [a[i], a[j]] = [a[j], a[i]]; } return a; }
39 const app = P.get("app");
40 const S = window.S = { app, view: "home", archived: [], deleted: [], cart: [], ordered: false, sel: {}, section: null, saved: {}, toggles: {} };
41 const NAMES = shuffle(["Alice Chen", "Bob Martin", "Carla Diaz", "David Kim", "Emma Rossi", "Farid Khan", "Grace Lee", "Hugo Weber"]).slice(0, 6);
42 const TOPICS = shuffle(["invoice for March", "team offsite", "contract renewal", "server outage", "lunch on Friday", "design review", "travel booking", "quarterly report"]).slice(0, 6);
43 const MAILS = NAMES.map((n, i) => ({ id: i, from: n, subject: TOPICS[i] }));
44 const PRODUCTS = shuffle(["T-shirt", "Hoodie", "Sneakers", "Backpack", "Cap", "Jacket"]).map((n, i) => ({ id: i, name: n, price: 19 + 7 * i }));
45 const COLORS = { red: "#d93a3a", blue: "#2f6fde", green: "#2e9d57", black: "#222222", yellow: "#e8c21b" };
46 const SIZES = ["S", "M", "L", "XL"];
47 const SECTIONS = { Profile: ["Show online status", "Public profile"], Security: ["Two-factor authentication", "Login alerts"],
48 Notifications: ["Email digests", "Push notifications"], Appearance: ["Dark mode", "Compact layout"] };
49 const secOrder = shuffle(Object.keys(SECTIONS));
50 for (const s in SECTIONS) for (const t of SECTIONS[s]) S.toggles[t] = false;
51 window.WORLD = { MAILS, PRODUCTS, COLORS: Object.keys(COLORS), SIZES, SECTIONS };
52 function el(html) { document.getElementById("app").innerHTML = html; }
53 function click(k) { return `data-click="${k}"`; }
54 function header(title) { return `<div class="top"><span class="brand">${title}</span><span class="btn" ${click("nav:help")}>Help</span><span class="btn" ${click("nav:account")}>Account</span></div>`; }
55 function render() {
56 if (app === "mail") {
57 if (S.view === "home") {
58 const rows = MAILS.filter(m => !S.archived.includes(m.id) && !S.deleted.includes(m.id))
59 .map(m => `<div class="row" ${click("open:" + m.id)}><b>${m.from}</b><span>${m.subject}</span></div>`).join("");
60 el(header("Acme Mail") + `<div class="main"><h2>Inbox</h2><span class="btn primary" ${click("compose")}>Compose</span>${rows}</div>`);
61 } else {
62 const m = MAILS[S.open];
63 el(header("Acme Mail") + `<div class="main"><span class="btn" ${click("back")}>&larr; Back to inbox</span><h2>${m.subject}</h2>
64 <p>From: <b>${m.from}</b></p><p style="width:640px">Hi, please see the details about the ${m.subject} below. Let me know if you have questions.</p>
65 <span class="btn" ${click("reply")}>Reply</span><span class="btn" ${click("forward")}>Forward</span>
66 <span class="btn" ${click("archive")}>Archive</span><span class="btn danger" ${click("delete")}>Delete</span><span class="btn" ${click("unread")}>Mark unread</span></div>`);
67 }
68 } else if (app === "shop") {
69 const cartBtn = `<span class="btn" ${click("cart")}>Cart (${S.cart.length})</span>`;
70 if (S.view === "home") {
71 const cards = PRODUCTS.map(p => `<div class="card" ${click("product:" + p.id)}><div class="img" style="background:#e3e8ef"></div><b>${p.name}</b><br>$${p.price}</div>`).join("");
72 el(header("Acme Shop") + `<div class="main">${cartBtn}<h2>New arrivals</h2><div class="grid">${cards}</div></div>`);
73 } else if (S.view === "product") {
74 const p = PRODUCTS[S.product];
75 const sw = Object.entries(COLORS).map(([n, c]) => `<span class="swatch ${S.sel.color === n ? "sel" : ""}" style="background:${c}" ${click("color:" + n)}></span>`).join("");
76 const sz = SIZES.map(s => `<span class="size ${S.sel.size === s ? "sel" : ""}" ${click("size:" + s)}>${s}</span>`).join("");
77 el(header("Acme Shop") + `<div class="main"><span class="btn" ${click("back")}>&larr; All products</span>${cartBtn}<h2>${p.name} &mdash; $${p.price}</h2>
78 <p>Colour:</p><div>${sw}</div><p>Size:</p><div>${sz}</div><br><span class="btn primary" ${click("add")}>Add to cart</span>
79 ${S.added ? '<div class="toast">Added to cart</div>' : ""}</div>`);
80 } else if (S.view === "cart") {
81 const items = S.cart.map(c => `<div class="row"><b>${c.name}</b><span>${c.color}, size ${c.size}</span></div>`).join("") || "<p>Your cart is empty.</p>";
82 el(header("Acme Shop") + `<div class="main"><span class="btn" ${click("back")}>&larr; Continue shopping</span><h2>Your cart</h2>${items}
83 <span class="btn primary" ${click("checkout")}>Checkout</span><span class="btn danger" ${click("empty")}>Empty cart</span>
84 ${S.ordered ? '<div class="toast">Order placed. Thank you!</div>' : ""}</div>`);
85 }
86 } else if (app === "settings") {
87 const side = secOrder.map(s => `<div class="item ${S.section === s ? "sel" : ""}" ${click("section:" + s)}>${s}</div>`).join("");
88 let panel = "<p>Select a section on the left.</p>";
89 if (S.section) {
90 panel = `<h2>${S.section}</h2>` + SECTIONS[S.section].map(t => `<div class="toggle"><span>${t}</span><span class="sw ${S.toggles[t] ? "on" : ""}" ${click("toggle:" + t)}></span></div>`).join("")
91 + `<br><span class="btn primary" ${click("save")}>Save changes</span><span class="btn" ${click("cancel")}>Cancel</span>`
92 + (S.justSaved ? '<div class="toast">Settings saved</div>' : "");
93 }
94 el(header("Acme Settings") + `<div class="main"><div class="side">${side}</div><div class="panel">${panel}</div></div>`);
95 }
96 }
97 function act(k) {
98 const [a, v] = k.split(/:(.*)/s);
99 S.justSaved = false; S.added = false;
100 if (a === "open") { S.view = "mail"; S.open = +v; }
101 else if (a === "back") { S.view = "home"; }
102 else if (a === "archive") { S.archived.push(S.open); S.view = "home"; }
103 else if (a === "delete") { S.deleted.push(S.open); S.view = "home"; }
104 else if (a === "product") { S.view = "product"; S.product = +v; S.sel = {}; }
105 else if (a === "color") { S.sel.color = v; }
106 else if (a === "size") { S.sel.size = v; }
107 else if (a === "add") { if (S.sel.color && S.sel.size) { S.cart.push({ name: PRODUCTS[S.product].name, color: S.sel.color, size: S.sel.size }); S.added = true; } }
108 else if (a === "cart") { S.view = "cart"; }
109 else if (a === "checkout") { if (S.cart.length) S.ordered = true; }
110 else if (a === "empty") { S.cart = []; S.ordered = false; }
111 else if (a === "section") { S.section = v; }
112 else if (a === "toggle") { S.toggles[v] = !S.toggles[v]; }
113 else if (a === "save") { S.saved = Object.assign({}, S.toggles); S.justSaved = true; }
114 else if (a === "cancel") { for (const t in S.toggles) S.toggles[t] = !!S.saved[t]; }
115 render();
116 }
117 document.addEventListener("click", e => { const t = e.target.closest("[data-click]"); if (t) act(t.getAttribute("data-click")); });
118 render();
119 </script></body></html>
120