@firedrill-tools/slack 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +369 -0
  3. package/firedrill/agent.target.json +17 -0
  4. package/firedrill/baseline.scenario.json +5 -0
  5. package/firedrill/bounds.scenario.json +112 -0
  6. package/firedrill/conformance.suite.json +21 -0
  7. package/firedrill/history-unavailable.scenario.json +11 -0
  8. package/firedrill/open-long-handles.scenario.json +263 -0
  9. package/firedrill/open-member-bound.scenario.json +24 -0
  10. package/firedrill/post-rate-limited.scenario.json +11 -0
  11. package/firedrill/response-budget.scenario.json +24 -0
  12. package/firedrill/slack-bounds.drill.json +428 -0
  13. package/firedrill/slack-denied.drill.json +74 -0
  14. package/firedrill/slack-fresh-install.drill.json +132 -0
  15. package/firedrill/slack-history-unavailable.drill.json +83 -0
  16. package/firedrill/slack-invalid-auth.drill.json +408 -0
  17. package/firedrill/slack-mcp-aliases.drill.json +219 -0
  18. package/firedrill/slack-open-long-handles.drill.json +82 -0
  19. package/firedrill/slack-open-member-bound.drill.json +135 -0
  20. package/firedrill/slack-post-rate-limited.drill.json +96 -0
  21. package/firedrill/slack-response-budget.drill.json +118 -0
  22. package/firedrill/slack-thread-many-repliers.drill.json +100 -0
  23. package/firedrill/slack-ts-sequence-full.drill.json +165 -0
  24. package/firedrill/slack-visibility.drill.json +110 -0
  25. package/firedrill/slack-web-api-flow.drill.json +896 -0
  26. package/firedrill/thread-many-repliers.scenario.json +79 -0
  27. package/firedrill/tools/slack/app/assets/ATTRIBUTION.md +40 -0
  28. package/firedrill/tools/slack/app/assets/fonts/OFL.txt +93 -0
  29. package/firedrill/tools/slack/app/assets/fonts/lato-latin-400.woff2 +0 -0
  30. package/firedrill/tools/slack/app/assets/fonts/lato-latin-700.woff2 +0 -0
  31. package/firedrill/tools/slack/app/assets/fonts/lato-latin-900.woff2 +0 -0
  32. package/firedrill/tools/slack/app/assets/slack-wordmark.svg +1 -0
  33. package/firedrill/tools/slack/app/assets/slack.svg +1 -0
  34. package/firedrill/tools/slack/app/site/app.js +2202 -0
  35. package/firedrill/tools/slack/app/site/assets/fonts/lato-latin-400.woff2 +0 -0
  36. package/firedrill/tools/slack/app/site/assets/fonts/lato-latin-700.woff2 +0 -0
  37. package/firedrill/tools/slack/app/site/assets/fonts/lato-latin-900.woff2 +0 -0
  38. package/firedrill/tools/slack/app/site/assets/slack-wordmark.svg +1 -0
  39. package/firedrill/tools/slack/app/site/assets/slack.svg +1 -0
  40. package/firedrill/tools/slack/app/site/chrome.css +73 -0
  41. package/firedrill/tools/slack/app/site/chrome.js +96 -0
  42. package/firedrill/tools/slack/app/site/icons.js +96 -0
  43. package/firedrill/tools/slack/app/site/index.html +253 -0
  44. package/firedrill/tools/slack/app/site/styles.css +2719 -0
  45. package/firedrill/tools/slack/app/site/ui.js +491 -0
  46. package/firedrill/tools/slack/behavior.mjs +1171 -0
  47. package/firedrill/tools/slack/lib/access.mjs +138 -0
  48. package/firedrill/tools/slack/lib/budget.mjs +67 -0
  49. package/firedrill/tools/slack/lib/ids.mjs +146 -0
  50. package/firedrill/tools/slack/lib/search.mjs +172 -0
  51. package/firedrill/tools/slack/lib/wire.mjs +144 -0
  52. package/firedrill/tools/slack/slack.tool.json +6114 -0
  53. package/firedrill/ts-sequence-full.scenario.json +38 -0
  54. package/firedrill/world.json +2008 -0
  55. package/firedrill.json +5 -0
  56. package/package.json +62 -0
  57. package/starter.json +1562 -0
  58. package/test/conformance.mjs +1166 -0
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 130"><path d="m163.593 101.885l6.387-14.835c6.902 5.151 16.07 7.83 25.136 7.83c6.696 0 10.92-2.576 10.92-6.49c-.103-10.92-40.074-2.37-40.383-29.773c-.103-13.907 12.26-24.621 29.772-24.621c10.405 0 20.81 2.575 28.227 8.447l-5.975 15.144c-6.799-4.327-15.246-7.417-23.282-7.417c-5.46 0-9.065 2.575-9.065 5.872c.103 10.714 40.383 4.842 40.795 31.008c0 14.217-12.053 24.21-29.36 24.21c-12.672 0-24.313-2.988-33.172-9.375m245.08-20.192l17.616 9.787c-6.593 11.847-19.161 19.78-33.687 19.78c-21.325 0-38.632-17.307-38.632-38.632s17.307-38.632 38.632-38.632c14.423 0 27.094 8.035 33.687 19.78l-17.616 9.786c-3.194-5.563-9.169-9.374-16.071-9.374c-10.199 0-18.44 8.241-18.44 18.44s8.241 18.44 18.44 18.44c6.902 0 12.877-3.812 16.07-9.375M234.984 1.957h22.046v107.86h-22.046zm199.958 0h22.046v63.356l24.93-29.772h26.991l-30.802 35.85l33.275 38.426h-28.227L456.988 77.47v32.347h-22.046zM322.55 81.9V63.665c-3.194-5.357-9.787-9.477-17.204-9.477c-10.2 0-18.44 8.241-18.44 18.44s8.24 18.44 18.44 18.44c7.417 0 14.01-3.915 17.204-9.169m0-46.358h22.046v74.173H322.55v-8.756c-3.606 6.078-12.569 10.302-21.943 10.302c-19.368 0-34.614-17.307-34.614-38.735s15.246-38.529 34.614-38.529c9.374 0 18.337 4.224 21.943 10.302z"/><path fill="#e01e5a" d="M27.3 81.796c0 7.52-6.078 13.599-13.599 13.599A13.585 13.585 0 0 1 .103 81.796c0-7.52 6.078-13.598 13.598-13.598H27.3zm6.799 0c0-7.52 6.078-13.598 13.598-13.598s13.599 6.078 13.599 13.598v33.996c0 7.52-6.078 13.599-13.599 13.599A13.585 13.585 0 0 1 34.1 115.792z"/><path fill="#36c5f0" d="M47.697 27.197A13.585 13.585 0 0 1 34.1 13.598C34.099 6.078 40.177 0 47.697 0s13.599 6.078 13.599 13.598v13.599zm0 6.902c7.52 0 13.599 6.078 13.599 13.598s-6.078 13.599-13.599 13.599H13.598A13.585 13.585 0 0 1 0 47.697C0 40.177 6.078 34.1 13.598 34.1z"/><path fill="#2eb67d" d="M102.194 47.697c0-7.52 6.078-13.598 13.598-13.598s13.599 6.078 13.599 13.598s-6.078 13.599-13.599 13.599h-13.598zm-6.8 0c0 7.52-6.077 13.599-13.598 13.599a13.585 13.585 0 0 1-13.598-13.599V13.598C68.198 6.078 74.276 0 81.796 0s13.599 6.078 13.599 13.598z"/><path fill="#ecb22e" d="M81.796 102.194c7.52 0 13.599 6.078 13.599 13.598s-6.078 13.599-13.599 13.599a13.585 13.585 0 0 1-13.598-13.599v-13.598zm0-6.8a13.585 13.585 0 0 1-13.598-13.598c0-7.52 6.078-13.598 13.598-13.598h34.1c7.52 0 13.598 6.078 13.598 13.598s-6.078 13.599-13.599 13.599z"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="#e01e5a" d="M53.841 161.32c0 14.832-11.987 26.82-26.819 26.82S.203 176.152.203 161.32c0-14.831 11.987-26.818 26.82-26.818H53.84zm13.41 0c0-14.831 11.987-26.818 26.819-26.818s26.819 11.987 26.819 26.819v67.047c0 14.832-11.987 26.82-26.82 26.82c-14.83 0-26.818-11.988-26.818-26.82z"/><path fill="#36c5f0" d="M94.07 53.638c-14.832 0-26.82-11.987-26.82-26.819S79.239 0 94.07 0s26.819 11.987 26.819 26.819v26.82zm0 13.613c14.832 0 26.819 11.987 26.819 26.819s-11.987 26.819-26.82 26.819H26.82C11.987 120.889 0 108.902 0 94.069c0-14.83 11.987-26.818 26.819-26.818z"/><path fill="#2eb67d" d="M201.55 94.07c0-14.832 11.987-26.82 26.818-26.82s26.82 11.988 26.82 26.82s-11.988 26.819-26.82 26.819H201.55zm-13.41 0c0 14.832-11.988 26.819-26.82 26.819c-14.831 0-26.818-11.987-26.818-26.82V26.82C134.502 11.987 146.489 0 161.32 0s26.819 11.987 26.819 26.819z"/><path fill="#ecb22e" d="M161.32 201.55c14.832 0 26.82 11.987 26.82 26.818s-11.988 26.82-26.82 26.82c-14.831 0-26.818-11.988-26.818-26.82V201.55zm0-13.41c-14.831 0-26.818-11.988-26.818-26.82c0-14.831 11.987-26.818 26.819-26.818h67.25c14.832 0 26.82 11.987 26.82 26.819s-11.988 26.819-26.82 26.819z"/></svg>
@@ -0,0 +1,73 @@
1
+ /* Client chrome added around the simulated surfaces: nav rows, section actions, huddle split button, clip buttons,
2
+ schedule caret, and the "not simulated" panel. */
3
+ .sb-nav { padding: 0 0 12px; }
4
+ .sb-nav .sb-link { padding-left: 8px; }
5
+ .sb-nav .sb-glyph .icon { width: 18px; height: 18px; }
6
+ .section-head { position: relative; display: flex; align-items: center; }
7
+ .section-head .section-toggle { flex: 1; min-width: 0; }
8
+ .section-toggle > span:last-child { font-weight: 400; }
9
+ .section-action {
10
+ position: absolute; top: 2px; width: 24px; height: 24px; border-radius: 4px;
11
+ display: none; align-items: center; justify-content: center; color: var(--sidebar-text);
12
+ }
13
+ .section-action[data-section-add], .section-action[data-ns="apps"] { right: 14px; }
14
+ .section-action[data-section-options] { right: 40px; }
15
+ .section-action .icon { width: 16px; height: 16px; }
16
+ .section-action:hover, .section-action[aria-expanded="true"] { background: var(--sidebar-hover); color: #fff; }
17
+ .section-head:hover .section-action, .section-head:focus-within .section-action, .section-action[aria-expanded="true"] { display: inline-flex; }
18
+ .members-stack .avatar { width: 22px; height: 22px; font-size: 11px; border: 2px solid #fff; border-radius: 6px; margin-right: -4px; }
19
+ .members-pill { padding-left: 3px; height: 30px; }
20
+ .huddle-split {
21
+ display: inline-flex; align-items: stretch; height: 30px; border: 1px solid var(--border); border-radius: 6px; overflow: hidden;
22
+ }
23
+ .huddle-split[hidden] { display: none; }
24
+ .huddle-btn, .huddle-caret { display: inline-flex; align-items: center; gap: 4px; color: var(--text-2); font-size: 13px; font-weight: 700; }
25
+ .huddle-btn { padding: 0 8px; }
26
+ .huddle-caret { padding: 0 4px; border-left: 1px solid var(--border); }
27
+ .huddle-btn .icon { width: 18px; height: 18px; }
28
+ .huddle-caret .icon { width: 14px; height: 14px; }
29
+ .huddle-btn:hover, .huddle-caret:hover, .huddle-caret[aria-expanded="true"] { background: var(--hover); color: var(--text); }
30
+ .pane-tab-add { padding: 0 6px; }
31
+ .pane-tab-add .icon { width: 16px; height: 16px; }
32
+ .send-divider { width: 1px; height: 18px; background: rgba(255, 255, 255, 0.4); }
33
+ .send-btn { border-radius: 4px 0 0 4px; }
34
+ .send-caret {
35
+ width: 22px; height: 28px; border-radius: 0 4px 4px 0; display: inline-flex; align-items: center; justify-content: center;
36
+ background: var(--primary); color: #fff;
37
+ }
38
+ .send-caret .icon { width: 14px; height: 14px; }
39
+ .send-caret:hover { background: var(--primary-hover); }
40
+ .send-btn:disabled + .send-divider { background: var(--border); }
41
+ .send-btn:disabled + .send-divider + .send-caret { background: transparent; color: var(--text-2); }
42
+ .send-btn:disabled + .send-divider + .send-caret:hover { background: var(--hover); color: var(--text); }
43
+ .menu.ns-menu { padding: 0; min-width: 0; }
44
+ .ns-panel { width: 280px; padding: 16px 20px; white-space: normal; }
45
+ .ns-title { font-size: 15px; font-weight: 900; color: var(--text); }
46
+ .ns-subtitle { font-size: 13px; color: var(--text-2); margin-top: 2px; }
47
+ .ns-text { margin: 10px 0 12px; font-size: 13px; line-height: 1.45; color: var(--text); }
48
+ .ns-tag {
49
+ display: inline-block; font-size: 11px; font-weight: 700; letter-spacing: 0.02em; color: var(--text-2);
50
+ background: var(--surface-3); border: 1px solid var(--border); border-radius: 4px; padding: 2px 6px;
51
+ }
52
+ @media (max-width: 768px) {
53
+ .huddle-text { display: none; }
54
+ .composer-actions-start [data-ns="video"], .composer-actions-start [data-ns="audio"] { display: none; }
55
+ }
56
+ .fd-tooltip {
57
+ position: fixed; z-index: 60; max-width: 240px; padding: 6px 10px; border-radius: 8px; pointer-events: none;
58
+ background: #1d1c1d; color: #fff; font-size: 13px; font-weight: 700; line-height: 1.35; text-align: center;
59
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.18);
60
+ }
61
+ .fd-tooltip::after {
62
+ content: ""; position: absolute; left: var(--arrow-x, 50%); top: 100%; margin-left: -5px;
63
+ border: 5px solid transparent; border-top-color: #1d1c1d;
64
+ }
65
+ .fd-tooltip.below::after { top: auto; bottom: 100%; border-top-color: transparent; border-bottom-color: #1d1c1d; }
66
+ .menu.wide-menu .menu-item, .menu.wide-menu .menu-label { white-space: normal; overflow: visible; text-overflow: clip; }
67
+ .thread-composer .composer-toolbar { flex-wrap: wrap; }
68
+ .thread-composer .composer-actions-start { flex-wrap: wrap; min-width: 0; }
69
+ .thread-composer .composer-actions { gap: 4px; }
70
+ /* Current Slack (2023+) selected sidebar row: a light lavender pill with aubergine text, not the older blue fill. */
71
+ .sb-link[aria-current="page"], .sb-link[aria-current="page"]:hover { background: rgba(249, 237, 255, 0.9); color: #39063a; }
72
+ .sb-link[aria-current="page"] .sb-glyph, .sb-link[aria-current="page"] .sb-glyph .icon { color: #39063a; }
73
+ .sb-link[aria-current="page"] .sb-glyph .presence { border-color: #f5e9fa; }
@@ -0,0 +1,96 @@
1
+ // Slack client chrome that the Tool does not simulate (huddles, clips, saved items, forwarding, scheduling, files,
2
+ // apps, slash commands). These controls stay in their real place so the client reads as Slack, and each opens a short
3
+ // panel saying it is outside this Tool's scope. Nothing here calls an operation or invents workspace data.
4
+ import { el, openMenu } from "./ui.js";
5
+
6
+ const PANELS = {
7
+ activity: ["Activity", "Mentions, reactions and thread replies", "There is no activity or notification feed in this Tool. Unread conversations are shown in bold in the sidebar."],
8
+ later: ["Later", "Saved messages and reminders", "Saved items and reminders are not part of this Tool."],
9
+ threads: ["Threads", "A feed of threads you follow", "Open a thread from its channel with “N replies”; a cross-channel followed-threads feed is not modelled."],
10
+ huddles: ["Huddles", "Audio and video huddles", "Calls and huddles are not part of this Tool."],
11
+ huddle: ["Start huddle", "Audio and video huddles", "Calls and huddles are not part of this Tool."],
12
+ "huddle-options": ["Huddle options", "Copy huddle link, start with video", "Calls and huddles are not part of this Tool."],
13
+ drafts: ["Drafts & sent", "Unsent drafts, scheduled and sent messages", "Drafts and scheduled messages are not stored by this Tool; composer text stays in this tab only."],
14
+ apps: ["Add apps", "Install and manage apps", "App installation is not part of this Tool. Bot users seeded in the world are listed under Apps."],
15
+ files: ["Files", "Files shared in this conversation", "files.* methods are not part of this Tool, so no files exist in this world."],
16
+ "add-tab": ["Add a tab", "Canvas, list, workflow or bookmark", "canvases.*, lists and bookmarks.* are not part of this Tool."],
17
+ video: ["Record video clip", "Clips", "Recording and uploading clips is not part of this Tool."],
18
+ audio: ["Record audio clip", "Clips", "Recording and uploading clips is not part of this Tool."],
19
+ shortcuts: ["Shortcuts", "Slash commands and workflows", "Slash commands and workflow shortcuts are not part of this Tool."],
20
+ schedule: ["Schedule for later", "Scheduled messages", "chat.scheduleMessage is not part of this Tool. Send posts immediately."],
21
+ forward: ["Forward message", "Share to another conversation", "Message sharing is not part of this Tool. Use “Copy link to message” from the More menu."],
22
+ save: ["Save for later", "Saved items", "Saved items and reminders are not part of this Tool."],
23
+ };
24
+
25
+ /** Open the "not simulated" panel next to `anchor`. */
26
+ export function notSimulated(anchor, key) {
27
+ const [title, subtitle, text] = (Object.hasOwn(PANELS, key) ? PANELS[key] : undefined) ?? [anchor.getAttribute("aria-label") ?? "Not available", "", "This feature is not part of this Tool."];
28
+ const panel = el("div", { class: "ns-panel", attrs: { role: "dialog", "aria-label": `${title} is not simulated` } }, [
29
+ el("div", { class: "ns-title", text: title }),
30
+ subtitle ? el("div", { class: "ns-subtitle", text: subtitle }) : undefined,
31
+ el("p", { class: "ns-text", text }),
32
+ el("div", { class: "ns-tag", text: "Not simulated by this Tool" }),
33
+ ]);
34
+ const rect = anchor.getBoundingClientRect();
35
+ const above = rect.top > window.innerHeight * 0.6;
36
+ const align = rect.left > window.innerWidth * 0.6 ? "end" : "start";
37
+ openMenu(anchor, panel, { align, above, className: "ns-menu" });
38
+ }
39
+
40
+ document.addEventListener("click", (event) => {
41
+ const control = event.target instanceof Element ? event.target.closest("[data-ns]") : null;
42
+ if (!control || control.disabled) return;
43
+ event.preventDefault();
44
+ event.stopPropagation();
45
+ notSimulated(control, control.dataset.ns);
46
+ }, true);
47
+
48
+ // Slack-style tooltips: a dark bubble with an arrow instead of the native title box. The title moves to data-tip on
49
+ // first hover or focus so the browser's own tooltip never appears; the accessible name stays on aria-label.
50
+ const tip = el("div", { class: "fd-tooltip", attrs: { role: "tooltip", id: "fd-tooltip" } });
51
+ tip.hidden = true;
52
+ document.body.append(tip);
53
+ let tipTarget;
54
+ function tipSource(node) {
55
+ const target = node instanceof Element ? node.closest("[title], [data-tip]") : null;
56
+ if (!target || target.closest(".menu, dialog:not([open])")) return undefined;
57
+ if (target.hasAttribute("title")) {
58
+ const text = target.getAttribute("title");
59
+ target.removeAttribute("title");
60
+ if (text) target.dataset.tip = text;
61
+ }
62
+ return target.dataset.tip ? target : undefined;
63
+ }
64
+ function showTip(target) {
65
+ const visible = target.getBoundingClientRect();
66
+ const hit = document.elementFromPoint(visible.left + visible.width / 2, visible.top + visible.height / 2);
67
+ if (visible.width === 0 || !hit || !(target === hit || target.contains(hit))) return hideTip(); // hidden or covered
68
+ tipTarget = target;
69
+ tip.textContent = target.dataset.tip;
70
+ tip.hidden = false;
71
+ const rect = target.getBoundingClientRect();
72
+ const box = tip.getBoundingClientRect();
73
+ const below = rect.top < box.height + 16;
74
+ tip.classList.toggle("below", below);
75
+ const left = Math.max(8, Math.min(window.innerWidth - box.width - 8, rect.left + rect.width / 2 - box.width / 2));
76
+ tip.style.left = `${left}px`;
77
+ tip.style.top = `${below ? rect.bottom + 8 : rect.top - box.height - 8}px`;
78
+ tip.style.setProperty("--arrow-x", `${rect.left + rect.width / 2 - left}px`);
79
+ }
80
+ function hideTip() {
81
+ tipTarget = undefined;
82
+ tip.hidden = true;
83
+ }
84
+ document.addEventListener("pointerover", (event) => {
85
+ if (event.pointerType && event.pointerType !== "mouse") return; // touch and pen show no hover tooltip, as in Slack
86
+ const target = tipSource(event.target);
87
+ if (target && target !== tipTarget) showTip(target);
88
+ else if (!target) hideTip();
89
+ });
90
+ document.addEventListener("focusin", (event) => {
91
+ const target = tipSource(event.target);
92
+ if (target && target.matches(":focus-visible")) showTip(target);
93
+ });
94
+ for (const type of ["pointerdown", "focusout", "scroll", "keydown"]) document.addEventListener(type, hideTip, true);
95
+ window.addEventListener("resize", hideTip);
96
+ window.addEventListener("hashchange", hideTip);
@@ -0,0 +1,96 @@
1
+ // Monochrome 20 px glyphs in the outlined style of the Slack client, drawn as path/stroke data so the app ships no
2
+ // icon font and makes no remote requests. Everything is `currentColor`.
3
+ const STROKE = { fill: "none", stroke: "currentColor", "stroke-width": "1.5", "stroke-linecap": "round", "stroke-linejoin": "round" };
4
+
5
+ // Each entry: [viewBox, elements]. Elements are [tag, attributes]; `s: true` applies the stroke preset.
6
+ const GLYPHS = {
7
+ hash: ["0 0 20 20", [["path", { s: true, d: "M7.5 3.5 5.5 16.5M14.5 3.5l-2 13M3.5 7.5h13M3 12.5h13" }]]],
8
+ lock: ["0 0 20 20", [["rect", { s: true, x: "4", y: "8.5", width: "12", height: "8.5", rx: "1.5" }], ["path", { s: true, d: "M6.5 8.5V6a3.5 3.5 0 0 1 7 0v2.5" }]]],
9
+ chevron_down: ["0 0 20 20", [["path", { s: true, d: "m6 8 4 4 4-4" }]]],
10
+ chevron_right: ["0 0 20 20", [["path", { s: true, d: "m8 6 4 4-4 4" }]]],
11
+ chevron_left: ["0 0 20 20", [["path", { s: true, d: "m12 6-4 4 4 4" }]]],
12
+ arrow_left: ["0 0 20 20", [["path", { s: true, d: "M16 10H4M9 5l-5 5 5 5" }]]],
13
+ arrow_right: ["0 0 20 20", [["path", { s: true, d: "M4 10h12M11 5l5 5-5 5" }]]],
14
+ clock: ["0 0 20 20", [["circle", { s: true, cx: "10", cy: "10", r: "7" }], ["path", { s: true, d: "M10 6v4l2.5 2" }]]],
15
+ search: ["0 0 20 20", [["circle", { s: true, cx: "8.5", cy: "8.5", r: "5.5" }], ["path", { s: true, d: "m12.5 12.5 4.5 4.5" }]]],
16
+ help: ["0 0 20 20", [["circle", { s: true, cx: "10", cy: "10", r: "7.5" }], ["path", { s: true, d: "M7.75 7.75a2.25 2.25 0 1 1 3.5 1.9c-.8.5-1.25 1-1.25 1.85" }], ["circle", { fill: "currentColor", cx: "10", cy: "14.25", r: ".9" }]]],
17
+ home: ["0 0 20 20", [["path", { s: true, d: "M3.5 9.5 10 3.5l6.5 6v7a1 1 0 0 1-1 1h-3.5v-5h-4v5H4.5a1 1 0 0 1-1-1z" }]]],
18
+ dm: ["0 0 20 20", [["path", { s: true, d: "M10 3.5c-3.9 0-7 2.6-7 5.8 0 1.6.8 3.1 2.1 4.1L4.5 17l3.6-1.6c.6.1 1.2.2 1.9.2 3.9 0 7-2.6 7-5.8s-3.1-6.3-7-6.3z" }]]],
19
+ bell: ["0 0 20 20", [["path", { s: true, d: "M5.5 8.5a4.5 4.5 0 0 1 9 0v3l1.5 2.5h-12L5.5 11.5z" }], ["path", { s: true, d: "M8.5 16.5a1.5 1.5 0 0 0 3 0" }]]],
20
+ bookmark: ["0 0 20 20", [["path", { s: true, d: "M5.5 3.5h9v13l-4.5-3-4.5 3z" }]]],
21
+ more_h: ["0 0 20 20", [["circle", { fill: "currentColor", cx: "5", cy: "10", r: "1.5" }], ["circle", { fill: "currentColor", cx: "10", cy: "10", r: "1.5" }], ["circle", { fill: "currentColor", cx: "15", cy: "10", r: "1.5" }]]],
22
+ more_v: ["0 0 20 20", [["circle", { fill: "currentColor", cx: "10", cy: "5", r: "1.5" }], ["circle", { fill: "currentColor", cx: "10", cy: "10", r: "1.5" }], ["circle", { fill: "currentColor", cx: "10", cy: "15", r: "1.5" }]]],
23
+ compose: ["0 0 20 20", [["path", { s: true, d: "M11 4H5a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 5 16h9a1.5 1.5 0 0 0 1.5-1.5V9" }], ["path", { s: true, d: "m14.6 3.4 2 2-6.6 6.6H8v-2z" }]]],
24
+ plus: ["0 0 20 20", [["path", { s: true, d: "M10 4.5v11M4.5 10h11" }]]],
25
+ close: ["0 0 20 20", [["path", { s: true, d: "m5 5 10 10M15 5 5 15" }]]],
26
+ check: ["0 0 20 20", [["path", { s: true, d: "m4 10.5 4 4 8-9" }]]],
27
+ emoji: ["0 0 20 20", [["circle", { s: true, cx: "10", cy: "10", r: "7.5" }], ["path", { s: true, d: "M6.5 11.5c.7 1.5 2 2.3 3.5 2.3s2.8-.8 3.5-2.3" }], ["circle", { fill: "currentColor", cx: "7.5", cy: "8", r: "1" }], ["circle", { fill: "currentColor", cx: "12.5", cy: "8", r: "1" }]]],
28
+ emoji_add: ["0 0 20 20", [["path", { s: true, d: "M17.4 11a7.5 7.5 0 1 1-8.4-8.4" }], ["path", { s: true, d: "M6.5 11.5c.7 1.5 2 2.3 3.5 2.3s2.8-.8 3.5-2.3" }], ["circle", { fill: "currentColor", cx: "7.5", cy: "8", r: "1" }], ["circle", { fill: "currentColor", cx: "12.5", cy: "8", r: "1" }], ["path", { s: true, d: "M15.5 2.5v5M13 5h5" }]]],
29
+ thread: ["0 0 20 20", [["path", { s: true, d: "M4 4.5h12a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1H9l-4 3v-3H4a1 1 0 0 1-1-1v-7a1 1 0 0 1 1-1z" }]]],
30
+ reply: ["0 0 20 20", [["path", { s: true, d: "M8 5 3.5 9.5 8 14" }], ["path", { s: true, d: "M3.5 9.5H12a4.5 4.5 0 0 1 4.5 4.5v1.5" }]]],
31
+ pin: ["0 0 20 20", [["path", { s: true, d: "M12.5 3.5l4 4-1.5 1.5-.5-.5-3 3 .5 3-1.5 1.5-3-3-4 4-1-1 4-4-3-3L5 8l3 .5 3-3-.5-.5z" }]]],
32
+ pencil: ["0 0 20 20", [["path", { s: true, d: "m13.5 3.5 3 3L7 16H4v-3z" }], ["path", { s: true, d: "m11.5 5.5 3 3" }]]],
33
+ trash: ["0 0 20 20", [["path", { s: true, d: "M4 5.5h12M8 5.5V4a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v1.5M5.5 5.5l.8 10a1 1 0 0 0 1 .9h5.4a1 1 0 0 0 1-.9l.8-10M8.5 8.5v5M11.5 8.5v5" }]]],
34
+ link: ["0 0 20 20", [["path", { s: true, d: "M8.5 11.5a3 3 0 0 1 0-4.2l2-2a3 3 0 0 1 4.2 4.2l-1 1" }], ["path", { s: true, d: "M11.5 8.5a3 3 0 0 1 0 4.2l-2 2a3 3 0 0 1-4.2-4.2l1-1" }]]],
35
+ at: ["0 0 20 20", [["circle", { s: true, cx: "10", cy: "10", r: "3" }], ["path", { s: true, d: "M13 10v1.5a1.5 1.5 0 0 0 3 0V10a6 6 0 1 0-2.4 4.8" }]]],
36
+ bold: ["0 0 20 20", [["path", { s: true, "stroke-width": "1.8", d: "M6 4h4.5a2.75 2.75 0 0 1 0 5.5H6zM6 9.5h5.25a3 3 0 0 1 0 6H6z" }]]],
37
+ italic: ["0 0 20 20", [["path", { s: true, d: "M8.5 4h6M5.5 16h6M11.5 4l-3 12" }]]],
38
+ strike: ["0 0 20 20", [["path", { s: true, d: "M4 10h12M13.5 6.2C13 4.9 11.7 4 10 4 7.8 4 6.5 5.2 6.5 6.6c0 .6.2 1 .5 1.4M6.5 13.8c.5 1.3 1.8 2.2 3.5 2.2 2.2 0 3.5-1.2 3.5-2.6 0-.6-.2-1-.5-1.4" }]]],
39
+ code: ["0 0 20 20", [["path", { s: true, d: "m7 6-4 4 4 4M13 6l4 4-4 4" }]]],
40
+ code_block: ["0 0 20 20", [["rect", { s: true, x: "3", y: "4", width: "14", height: "12", rx: "1.5" }], ["path", { s: true, d: "m8.5 8-2 2 2 2M11.5 8l2 2-2 2" }]]],
41
+ list_ol: ["0 0 20 20", [["path", { s: true, d: "M8 5.5h9M8 10h9M8 14.5h9M3.5 4.5 4.5 4v3M3.5 12.5h1.8c.5 0 .7.5.4.9L3.5 15.5h2.3" }]]],
42
+ list_ul: ["0 0 20 20", [["path", { s: true, d: "M8 5.5h9M8 10h9M8 14.5h9" }], ["circle", { fill: "currentColor", cx: "4.5", cy: "5.5", r: "1" }], ["circle", { fill: "currentColor", cx: "4.5", cy: "10", r: "1" }], ["circle", { fill: "currentColor", cx: "4.5", cy: "14.5", r: "1" }]]],
43
+ quote: ["0 0 20 20", [["path", { s: true, d: "M4 4v12M8 6h8M8 10h8M8 14h5" }]]],
44
+ format: ["0 0 20 20", [["path", { s: true, d: "M4 5h12M4 10h12M4 15h7" }], ["path", { s: true, d: "M14 13v4" }]]],
45
+ send: ["0 0 20 20", [["path", { fill: "currentColor", d: "M2.5 3.5 17.5 10 2.5 16.5l2-6.5zM5.5 10.5 4.2 14.8 14.3 10 4.2 5.2l1.3 4.3H10v1z" }]]],
46
+ users: ["0 0 20 20", [["circle", { s: true, cx: "7.5", cy: "7", r: "2.75" }], ["path", { s: true, d: "M2.5 16a5 5 0 0 1 10 0" }], ["path", { s: true, d: "M12.5 4.6a2.75 2.75 0 0 1 0 4.8M14 11.3a5 5 0 0 1 3.5 4.7" }]]],
47
+ user_add: ["0 0 20 20", [["circle", { s: true, cx: "8", cy: "7", r: "3" }], ["path", { s: true, d: "M2.5 16.5a5.5 5.5 0 0 1 11 0M15.5 6.5v5M13 9h5" }]]],
48
+ info: ["0 0 20 20", [["circle", { s: true, cx: "10", cy: "10", r: "7.5" }], ["path", { s: true, d: "M10 9v5" }], ["circle", { fill: "currentColor", cx: "10", cy: "6.5", r: ".9" }]]],
49
+ star: ["0 0 20 20", [["path", { s: true, d: "m10 3 2.1 4.4 4.9.6-3.6 3.3.9 4.8L10 13.8l-4.3 2.3.9-4.8L3 8l4.9-.6z" }]]],
50
+ archive: ["0 0 20 20", [["path", { s: true, d: "M3 4.5h14v3H3zM4 7.5v8a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1v-8M8 11h4" }]]],
51
+ filter: ["0 0 20 20", [["path", { s: true, d: "M3 5h14M5.5 10h9M8 15h4" }]]],
52
+ channel_browse: ["0 0 20 20", [["path", { s: true, d: "M7 3.5 5.5 12.5M12 3.5l-1.5 9M3.5 6.5h10M3 10.5h10" }], ["circle", { s: true, cx: "14.5", cy: "14", r: "2.5" }], ["path", { s: true, d: "m16.3 15.8 1.5 1.5" }]]],
53
+ headphones: ["0 0 20 20", [["path", { s: true, d: "M4 12V10a6 6 0 0 1 12 0v2" }], ["rect", { s: true, x: "3", y: "11.5", width: "3.5", height: "5", rx: "1" }], ["rect", { s: true, x: "13.5", y: "11.5", width: "3.5", height: "5", rx: "1" }]]],
54
+ refresh: ["0 0 20 20", [["path", { s: true, d: "M16 9.5A6 6 0 0 0 5.2 6.5M4 10.5a6 6 0 0 0 10.8 3" }], ["path", { s: true, d: "M4.5 3.5v3.5H8M15.5 16.5V13H12" }]]],
55
+ spinner: ["0 0 20 20", [["path", { s: true, d: "M10 3a7 7 0 1 1-6.1 3.6" }]]],
56
+ menu: ["0 0 20 20", [["path", { s: true, d: "M3 5.5h14M3 10h14M3 14.5h14" }]]],
57
+ sort: ["0 0 20 20", [["path", { s: true, d: "M6 4v12M6 16l-2.5-2.5M6 16l2.5-2.5M14 16V4M14 4l-2.5 2.5M14 4l2.5 2.5" }]]],
58
+ canvas: ["0 0 20 20", [["rect", { s: true, x: "3.5", y: "3.5", width: "13", height: "13", rx: "1.5" }], ["path", { s: true, d: "M6.5 7.5h7M6.5 10.5h7M6.5 13.5h4" }]]],
59
+ hash_circle: ["0 0 20 20", [["circle", { s: true, cx: "10", cy: "10", r: "7.5" }], ["path", { s: true, d: "M8.5 6.5 7.5 13.5M12.5 6.5l-1 7M6.5 8.5h7M6 11.5h7" }]]],
60
+ forward: ["0 0 20 20", [["path", { s: true, d: "M11.5 4.5 16.5 9l-5 4.5" }], ["path", { s: true, d: "M16.5 9H9a5 5 0 0 0-5 5v1.5" }]]],
61
+ video: ["0 0 20 20", [["rect", { s: true, x: "2.5", y: "5.5", width: "10.5", height: "9", rx: "1.5" }], ["path", { s: true, d: "m13 9 4.5-2.5v7L13 11" }]]],
62
+ mic: ["0 0 20 20", [["rect", { s: true, x: "7.5", y: "3", width: "5", height: "9", rx: "2.5" }], ["path", { s: true, d: "M4.5 9.5a5.5 5.5 0 0 0 11 0M10 15v2.5" }]]],
63
+ slash: ["0 0 20 20", [["rect", { s: true, x: "3", y: "3", width: "14", height: "14", rx: "3" }], ["path", { s: true, d: "m11.5 6.5-3 7" }]]],
64
+ drafts: ["0 0 20 20", [["path", { s: true, d: "M3 9.5 16.5 3.5 13 16.5l-3.5-4.5z" }], ["path", { s: true, d: "m9.5 12 7-8.5" }]]],
65
+ directory: ["0 0 20 20", [["rect", { s: true, x: "4", y: "3", width: "12", height: "14", rx: "1.5" }], ["circle", { s: true, cx: "10", cy: "8.5", r: "2" }], ["path", { s: true, d: "M7 14a3 3 0 0 1 6 0" }]]],
66
+ apps: ["0 0 20 20", [["rect", { s: true, x: "3.5", y: "3.5", width: "5", height: "5", rx: "1" }], ["rect", { s: true, x: "11.5", y: "3.5", width: "5", height: "5", rx: "1" }], ["rect", { s: true, x: "3.5", y: "11.5", width: "5", height: "5", rx: "1" }], ["rect", { s: true, x: "11.5", y: "11.5", width: "5", height: "5", rx: "1" }]]],
67
+ files: ["0 0 20 20", [["path", { s: true, d: "M5.5 3h6l3.5 3.5V16a1 1 0 0 1-1 1h-8.5a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1z" }], ["path", { s: true, d: "M11.5 3v3.5H15" }]]],
68
+ };
69
+
70
+ const NS = "http://www.w3.org/2000/svg";
71
+
72
+ /** Create one icon as an inline `<svg>` element (decorative; callers give labels to the surrounding control). */
73
+ export function icon(name, className = "") {
74
+ const entry = Object.hasOwn(GLYPHS, name) ? GLYPHS[name] : GLYPHS.help;
75
+ const svg = document.createElementNS(NS, "svg");
76
+ svg.setAttribute("viewBox", entry[0]);
77
+ svg.setAttribute("aria-hidden", "true");
78
+ svg.setAttribute("focusable", "false");
79
+ svg.setAttribute("class", `icon ${className}`.trim());
80
+ svg.dataset.icon = name;
81
+ for (const [tag, attributes] of entry[1]) {
82
+ const element = document.createElementNS(NS, tag);
83
+ if (attributes.s) for (const [key, value] of Object.entries(STROKE)) element.setAttribute(key, value);
84
+ for (const [key, value] of Object.entries(attributes)) if (key !== "s") element.setAttribute(key, value);
85
+ svg.append(element);
86
+ }
87
+ return svg;
88
+ }
89
+
90
+ /** Replace every `<span data-icon="…">` placeholder in `root` with its glyph. */
91
+ export function hydrateIcons(root = document) {
92
+ for (const placeholder of root.querySelectorAll("span[data-icon]")) {
93
+ const glyph = icon(placeholder.dataset.icon, placeholder.className);
94
+ placeholder.replaceWith(glyph);
95
+ }
96
+ }
@@ -0,0 +1,253 @@
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" />
6
+ <title>Slack (synthetic)</title>
7
+ <link rel="icon" type="image/svg+xml" href="./assets/slack.svg" />
8
+ <link rel="stylesheet" href="./styles.css" />
9
+ <link rel="stylesheet" href="./chrome.css" />
10
+ <script type="module" src="./app.js"></script>
11
+ <script type="module" src="./chrome.js"></script>
12
+ </head>
13
+ <body>
14
+ <div class="shell" id="shell">
15
+ <header class="topbar">
16
+ <div class="topbar-start">
17
+ <button type="button" class="topbar-btn" id="nav-menu" aria-label="Show sidebar" title="Sidebar"><span data-icon="menu"></span></button>
18
+ <button type="button" class="topbar-btn" id="nav-back" aria-label="Back" title="Back" disabled><span data-icon="arrow_left"></span></button>
19
+ <button type="button" class="topbar-btn" id="nav-forward" aria-label="Forward" title="Forward" disabled><span data-icon="arrow_right"></span></button>
20
+ <button type="button" class="topbar-btn" id="nav-history" aria-label="History" aria-expanded="false" title="History"><span data-icon="clock"></span></button>
21
+ </div>
22
+ <form class="topbar-search" id="search-form" role="search">
23
+ <label class="visually-hidden" for="search">Search</label>
24
+ <span data-icon="search" class="search-glyph"></span>
25
+ <input id="search" type="text" placeholder="Search" autocomplete="off" spellcheck="false" />
26
+ <button type="button" class="search-clear" id="search-clear" aria-label="Clear search" hidden><span data-icon="close"></span></button>
27
+ </form>
28
+ <div class="topbar-end">
29
+ <button type="button" class="topbar-btn" id="help" aria-label="Help" aria-expanded="false" title="Help"><span data-icon="help"></span></button>
30
+ </div>
31
+ </header>
32
+
33
+ <nav class="rail" id="rail" aria-label="Workspace">
34
+ <button type="button" class="rail-workspace" id="workspace-tile" aria-label="Workspace menu" aria-expanded="false" title="Workspace">
35
+ <span class="rail-tile" id="workspace-initial">·</span>
36
+ </button>
37
+ <div class="rail-items">
38
+ <button type="button" class="rail-item" data-view="home" aria-current="page"><span class="rail-icon"><span data-icon="home"></span></span><span class="rail-label">Home</span></button>
39
+ <button type="button" class="rail-item" data-view="dms"><span class="rail-icon"><span data-icon="dm"></span></span><span class="rail-label">DMs</span></button>
40
+ <button type="button" class="rail-item" data-view="activity" data-ns="activity" aria-expanded="false"><span class="rail-icon"><span data-icon="bell"></span></span><span class="rail-label">Activity</span></button>
41
+ <button type="button" class="rail-item" data-view="later" data-ns="later"><span class="rail-icon"><span data-icon="bookmark"></span></span><span class="rail-label">Later</span></button>
42
+ <button type="button" class="rail-item" data-view="more" aria-expanded="false"><span class="rail-icon"><span data-icon="more_h"></span></span><span class="rail-label">More</span></button>
43
+ </div>
44
+ <div class="rail-bottom">
45
+ <button type="button" class="rail-plus" id="rail-create" aria-label="Create new" aria-expanded="false" title="Create new"><span data-icon="plus"></span></button>
46
+ <button type="button" class="rail-avatar" id="me-button" aria-label="Your profile" aria-expanded="false" title="You"><span class="avatar md" id="me-avatar" aria-hidden="true">?</span><span class="presence active" aria-hidden="true"></span></button>
47
+ </div>
48
+ </nav>
49
+
50
+ <div class="workspace" id="workspace">
51
+ <aside class="sidebar" id="sidebar" aria-label="Conversations">
52
+ <div class="sidebar-header">
53
+ <button type="button" class="sidebar-title" id="workspace-menu" aria-expanded="false"><span class="sidebar-title-text" id="workspace-name">Loading…</span><span data-icon="chevron_down"></span></button>
54
+ <div class="sidebar-header-actions">
55
+ <button type="button" class="sidebar-filter" id="sidebar-filter" aria-label="Filter conversations" aria-expanded="false" title="Filter conversations"><span data-icon="filter"></span></button>
56
+ <button type="button" class="sidebar-compose" id="compose" aria-label="New message" title="New message"><span data-icon="compose"></span></button>
57
+ </div>
58
+ </div>
59
+ <div class="sidebar-scroll" id="sidebar-scroll">
60
+ <div class="sidebar-denied" id="sidebar-denied" hidden></div>
61
+ <ul class="sidebar-list sb-nav" role="list" aria-label="Views">
62
+ <li><button type="button" class="sb-link sb-nav-link" data-ns="threads" title="Threads"><span class="sb-glyph"><span data-icon="thread"></span></span><span class="sb-name">Threads</span></button></li>
63
+ <li><button type="button" class="sb-link sb-nav-link" data-ns="huddles" title="Huddles"><span class="sb-glyph"><span data-icon="headphones"></span></span><span class="sb-name">Huddles</span></button></li>
64
+ <li><button type="button" class="sb-link sb-nav-link" data-ns="drafts" title="Drafts &amp; sent"><span class="sb-glyph"><span data-icon="drafts"></span></span><span class="sb-name">Drafts &amp; sent</span></button></li>
65
+ <li><button type="button" class="sb-link sb-nav-link" id="nav-directories" title="Directories"><span class="sb-glyph"><span data-icon="directory"></span></span><span class="sb-name">Directories</span></button></li>
66
+ </ul>
67
+ <section class="sidebar-section" id="section-channels">
68
+ <div class="section-head"><button type="button" class="section-toggle" id="channels-toggle" aria-expanded="true" aria-controls="channel-list"><span data-icon="chevron_down" class="section-chevron"></span><span>Channels</span></button><button type="button" class="section-action" data-section-options="Channels" aria-label="Channels section options" title="Section options" aria-expanded="false"><span data-icon="more_v"></span></button><button type="button" class="section-action" data-section-add="add-channels" aria-label="Add channels" title="Add channels" aria-expanded="false"><span data-icon="plus"></span></button></div>
69
+ <ul class="sidebar-list" id="channel-list" role="list" aria-label="Channels"></ul>
70
+ <button type="button" class="sidebar-add" id="add-channels" aria-expanded="false"><span class="add-box"><span data-icon="plus"></span></span><span>Add channels</span></button>
71
+ </section>
72
+ <section class="sidebar-section" id="section-dms">
73
+ <div class="section-head"><button type="button" class="section-toggle" id="dms-toggle" aria-expanded="true" aria-controls="dm-list"><span data-icon="chevron_down" class="section-chevron"></span><span>Direct messages</span></button><button type="button" class="section-action" data-section-options="Direct messages" aria-label="Direct messages section options" title="Section options" aria-expanded="false"><span data-icon="more_v"></span></button><button type="button" class="section-action" data-section-add="add-dm" aria-label="Add a direct message" title="New message" aria-expanded="false"><span data-icon="plus"></span></button></div>
74
+ <ul class="sidebar-list" id="dm-list" role="list" aria-label="Direct messages"></ul>
75
+ <button type="button" class="sidebar-add" id="add-dm"><span class="add-box"><span data-icon="plus"></span></span><span>New message</span></button>
76
+ </section>
77
+ <section class="sidebar-section" id="section-apps" hidden>
78
+ <div class="section-head"><button type="button" class="section-toggle" id="apps-toggle" aria-expanded="true" aria-controls="app-list"><span data-icon="chevron_down" class="section-chevron"></span><span>Apps</span></button><button type="button" class="section-action" data-ns="apps" aria-label="Add apps" title="Add apps" aria-expanded="false"><span data-icon="plus"></span></button></div>
79
+ <ul class="sidebar-list" id="app-list" role="list" aria-label="Apps"></ul>
80
+ </section>
81
+ </div>
82
+ </aside>
83
+
84
+ <main class="pane" id="pane">
85
+ <header class="pane-header" id="pane-header">
86
+ <div class="pane-title-wrap">
87
+ <button type="button" class="pane-title" id="pane-title" aria-expanded="false"><span class="pane-title-glyph" id="pane-title-glyph"></span><span class="pane-title-text" id="pane-title-text">Loading…</span><span data-icon="chevron_down" class="pane-title-chevron"></span></button>
88
+ <button type="button" class="pane-topic" id="pane-topic" hidden></button>
89
+ </div>
90
+ <div class="pane-header-actions" id="pane-header-actions">
91
+ <button type="button" class="members-pill" id="members-pill" aria-label="View members" hidden><span class="members-stack" id="members-stack"></span><span class="members-count" id="members-count"></span></button>
92
+ <div class="huddle-split" id="huddle-split"><button type="button" class="huddle-btn" data-ns="huddle" aria-label="Start huddle" title="Start huddle"><span data-icon="headphones"></span><span class="huddle-text">Huddle</span></button><button type="button" class="huddle-caret" data-ns="huddle-options" aria-label="Huddle options" title="Huddle options" aria-expanded="false"><span data-icon="chevron_down"></span></button></div>
93
+ <button type="button" class="icon-btn" id="pane-more" aria-label="More actions" title="More actions" aria-expanded="false"><span data-icon="more_v"></span></button>
94
+ <button type="button" class="icon-btn" id="pane-details" aria-label="Conversation details" title="Details" hidden><span data-icon="info"></span></button>
95
+ </div>
96
+ </header>
97
+ <nav class="pane-tabs" id="pane-tabs" aria-label="Conversation sections">
98
+ <button type="button" class="pane-tab" data-tab="messages" aria-current="page"><span data-icon="dm"></span>Messages</button>
99
+ <button type="button" class="pane-tab" data-tab="pins"><span data-icon="pin"></span>Pins<span class="tab-count" id="pins-count"></span></button>
100
+ <button type="button" class="pane-tab" data-ns="files" aria-label="Files"><span data-icon="files"></span>Files</button>
101
+ <button type="button" class="pane-tab pane-tab-add" data-ns="add-tab" aria-label="Add a tab" title="Add canvas, list, workflow or bookmark" aria-expanded="false"><span data-icon="plus"></span></button>
102
+ </nav>
103
+ <div class="pane-body" id="pane-body" tabindex="-1">
104
+ <div class="message-scroll" id="message-scroll">
105
+ <div class="message-list" id="messages" role="log" aria-live="polite" aria-label="Messages"></div>
106
+ </div>
107
+ </div>
108
+ <div class="pane-footer" id="pane-footer">
109
+ <div class="composer" id="composer">
110
+ <div class="composer-toolbar" id="composer-toolbar">
111
+ <button type="button" class="fmt-btn" data-fmt="*" aria-label="Bold" title="Bold"><span data-icon="bold"></span></button>
112
+ <button type="button" class="fmt-btn" data-fmt="_" aria-label="Italic" title="Italic"><span data-icon="italic"></span></button>
113
+ <button type="button" class="fmt-btn" data-fmt="~" aria-label="Strikethrough" title="Strikethrough"><span data-icon="strike"></span></button>
114
+ <span class="fmt-divider" role="separator"></span>
115
+ <button type="button" class="fmt-btn" data-fmt="link" aria-label="Link" title="Link"><span data-icon="link"></span></button>
116
+ <span class="fmt-divider" role="separator"></span>
117
+ <button type="button" class="fmt-btn" data-fmt="ol" aria-label="Ordered list" title="Ordered list"><span data-icon="list_ol"></span></button>
118
+ <button type="button" class="fmt-btn" data-fmt="ul" aria-label="Bulleted list" title="Bulleted list"><span data-icon="list_ul"></span></button>
119
+ <span class="fmt-divider" role="separator"></span>
120
+ <button type="button" class="fmt-btn" data-fmt="quote" aria-label="Blockquote" title="Blockquote"><span data-icon="quote"></span></button>
121
+ <span class="fmt-divider" role="separator"></span>
122
+ <button type="button" class="fmt-btn" data-fmt="`" aria-label="Code" title="Code"><span data-icon="code"></span></button>
123
+ <button type="button" class="fmt-btn" data-fmt="```" aria-label="Code block" title="Code block"><span data-icon="code_block"></span></button>
124
+ </div>
125
+ <label class="visually-hidden" for="composer-input" id="composer-label">Message</label>
126
+ <textarea id="composer-input" rows="1" placeholder="Message" spellcheck="true"></textarea>
127
+ <div class="composer-actions">
128
+ <div class="composer-actions-start">
129
+ <button type="button" class="composer-plus" id="composer-plus" aria-label="Attach" aria-expanded="false" title="Attach"><span data-icon="plus"></span></button>
130
+ <button type="button" class="fmt-btn" id="composer-format" aria-label="Hide formatting" aria-pressed="true" title="Hide formatting"><span data-icon="format"></span></button>
131
+ <button type="button" class="fmt-btn" id="composer-emoji" aria-label="Emoji" aria-expanded="false" title="Emoji"><span data-icon="emoji"></span></button>
132
+ <button type="button" class="fmt-btn" id="composer-mention" aria-label="Mention someone" aria-expanded="false" title="Mention someone"><span data-icon="at"></span></button>
133
+ <span class="fmt-divider" role="separator"></span>
134
+ <button type="button" class="fmt-btn" data-ns="video" aria-label="Record video clip" title="Record video clip"><span data-icon="video"></span></button>
135
+ <button type="button" class="fmt-btn" data-ns="audio" aria-label="Record audio clip" title="Record audio clip"><span data-icon="mic"></span></button>
136
+ <span class="fmt-divider" role="separator"></span>
137
+ <button type="button" class="fmt-btn" data-ns="shortcuts" aria-label="Run shortcut" title="Run shortcut" aria-expanded="false"><span data-icon="slash"></span></button>
138
+ </div>
139
+ <div class="composer-actions-end">
140
+ <button type="button" class="send-btn" id="send" aria-label="Send now" title="Send now" disabled><span data-icon="send"></span></button><span class="send-divider" aria-hidden="true"></span><button type="button" class="send-caret" id="send-schedule" data-ns="schedule" aria-label="Schedule for later" title="Schedule for later" aria-expanded="false"><span data-icon="chevron_down"></span></button>
141
+ </div>
142
+ </div>
143
+ </div>
144
+ <p class="composer-note" id="composer-note" role="alert" hidden></p>
145
+ <div class="composer-banner" id="composer-banner" hidden></div>
146
+ </div>
147
+ </main>
148
+
149
+ <aside class="side-panel" id="side-panel" aria-label="Details" hidden>
150
+ <header class="side-header">
151
+ <div class="side-title-wrap"><h2 class="side-title" id="side-title">Thread</h2><span class="side-subtitle" id="side-subtitle"></span></div>
152
+ <button type="button" class="icon-btn" id="side-close" aria-label="Close"><span data-icon="close"></span></button>
153
+ </header>
154
+ <div class="side-body" id="side-body"></div>
155
+ </aside>
156
+ </div>
157
+ </div>
158
+
159
+ <dialog class="modal" id="create-channel-dialog" aria-labelledby="create-channel-title">
160
+ <form id="create-channel-form" method="dialog" novalidate>
161
+ <header class="modal-header"><h2 id="create-channel-title">Create a channel</h2><button type="button" class="icon-btn modal-close" data-close aria-label="Close"><span data-icon="close"></span></button></header>
162
+ <div class="modal-body">
163
+ <label class="field-label" for="channel-name">Name</label>
164
+ <div class="field channel-name-field">
165
+ <span class="field-prefix" id="channel-name-prefix"><span data-icon="hash"></span></span>
166
+ <input id="channel-name" maxlength="80" autocomplete="off" placeholder="e.g. plan-budget" />
167
+ <span class="field-counter" id="channel-name-counter">80</span>
168
+ </div>
169
+ <p class="field-hint">Channels are where conversations happen around a topic. Use a name that is easy to find and understand.</p>
170
+ <fieldset class="visibility">
171
+ <legend class="field-label">Visibility</legend>
172
+ <label class="radio"><input type="radio" name="visibility" value="public" checked /><span><strong>Public</strong> — Anyone in the workspace</span></label>
173
+ <label class="radio"><input type="radio" name="visibility" value="private" /><span><strong>Private</strong> — Only specific people<br /><small>Can only be viewed or joined by invitation</small></span></label>
174
+ </fieldset>
175
+ <p class="modal-error" id="create-channel-error" role="alert" hidden></p>
176
+ </div>
177
+ <footer class="modal-footer">
178
+ <button type="submit" class="btn primary" id="create-channel-submit" disabled>Create</button>
179
+ </footer>
180
+ </form>
181
+ </dialog>
182
+
183
+ <dialog class="modal" id="topic-dialog" aria-labelledby="topic-title">
184
+ <form id="topic-form" method="dialog" novalidate>
185
+ <header class="modal-header"><h2 id="topic-title">Edit topic</h2><button type="button" class="icon-btn modal-close" data-close aria-label="Close"><span data-icon="close"></span></button></header>
186
+ <div class="modal-body">
187
+ <label class="visually-hidden" for="topic-input">Topic</label>
188
+ <textarea id="topic-input" class="field-textarea" rows="3" maxlength="250" placeholder="Add a topic"></textarea>
189
+ <p class="field-hint">Let people know what this channel is focused on right now (e.g. a project milestone). Topics are always visible in the header.</p>
190
+ <p class="modal-error" id="topic-error" role="alert" hidden></p>
191
+ </div>
192
+ <footer class="modal-footer">
193
+ <button type="button" class="btn" data-close>Cancel</button>
194
+ <button type="submit" class="btn primary" id="topic-submit">Save</button>
195
+ </footer>
196
+ </form>
197
+ </dialog>
198
+
199
+ <dialog class="modal people-modal" id="people-dialog" aria-labelledby="people-title">
200
+ <form id="people-form" method="dialog" novalidate>
201
+ <header class="modal-header"><div><h2 id="people-title">Add people</h2><p class="modal-subtitle" id="people-subtitle"></p></div><button type="button" class="icon-btn modal-close" data-close aria-label="Close"><span data-icon="close"></span></button></header>
202
+ <div class="modal-body">
203
+ <label class="visually-hidden" for="people-filter">Search people</label>
204
+ <div class="field"><span class="field-prefix"><span data-icon="search"></span></span><input id="people-filter" autocomplete="off" placeholder="Search by name" /></div>
205
+ <div class="people-selected" id="people-selected" aria-live="polite"></div>
206
+ <ul class="people-list" id="people-list" role="listbox" aria-multiselectable="true" aria-label="People"></ul>
207
+ <p class="modal-error" id="people-error" role="alert" hidden></p>
208
+ </div>
209
+ <footer class="modal-footer">
210
+ <button type="submit" class="btn primary" id="people-submit" disabled>Add</button>
211
+ </footer>
212
+ </form>
213
+ </dialog>
214
+
215
+ <dialog class="modal details-modal" id="details-dialog" aria-labelledby="details-title">
216
+ <header class="modal-header">
217
+ <h2 id="details-title" class="details-title"><span id="details-glyph"></span><span id="details-name"></span></h2>
218
+ <button type="button" class="icon-btn modal-close" data-close aria-label="Close"><span data-icon="close"></span></button>
219
+ </header>
220
+ <nav class="modal-tabs" id="details-tabs" aria-label="Details sections">
221
+ <button type="button" class="modal-tab" data-tab="about" aria-current="page">About</button>
222
+ <button type="button" class="modal-tab" data-tab="members">Members<span class="tab-count" id="details-members-count"></span></button>
223
+ <button type="button" class="modal-tab" data-tab="settings">Settings</button>
224
+ </nav>
225
+ <div class="modal-body details-body" id="details-body"></div>
226
+ </dialog>
227
+
228
+ <dialog class="modal about-modal" id="about-dialog" aria-labelledby="about-title">
229
+ <header class="modal-header"><h2 id="about-title">About this workspace</h2><button type="button" class="icon-btn modal-close" data-close aria-label="Close"><span data-icon="close"></span></button></header>
230
+ <div class="modal-body about-body">
231
+ <div class="about-brand"><img src="./assets/slack.svg" alt="" width="40" height="40" /><img src="./assets/slack-wordmark.svg" alt="Slack" class="about-wordmark" height="24" /></div>
232
+ <dl class="about-list" id="about-list"></dl>
233
+ <p class="field-hint">A synthetic workspace served by a Firedrill Tool. Messages, channels and members exist only inside this world; nothing is delivered to Slack.</p>
234
+ </div>
235
+ </dialog>
236
+
237
+ <dialog class="modal confirm-modal" id="confirm-dialog" aria-labelledby="confirm-title" aria-describedby="confirm-text">
238
+ <form method="dialog">
239
+ <header class="modal-header"><h2 id="confirm-title">Confirm</h2><button type="submit" value="cancel" class="icon-btn modal-close" aria-label="Close"><span data-icon="close"></span></button></header>
240
+ <div class="modal-body">
241
+ <p id="confirm-text"></p>
242
+ <div class="confirm-preview" id="confirm-preview" hidden></div>
243
+ </div>
244
+ <footer class="modal-footer">
245
+ <button type="submit" value="cancel" class="btn" autofocus>Cancel</button>
246
+ <button type="submit" value="ok" class="btn primary" id="confirm-ok">OK</button>
247
+ </footer>
248
+ </form>
249
+ </dialog>
250
+
251
+ <div class="toast" id="toast" role="status" aria-live="polite" hidden><span id="toast-text"></span></div>
252
+ </body>
253
+ </html>