clay-server 4.6.1-beta.1 → 4.7.0-beta.2

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.
@@ -21,7 +21,7 @@ function normalizeSessions(value) {
21
21
  }
22
22
 
23
23
  function normalizePrimarySurface(value) {
24
- return value === "home" || value === "project" ? value : null;
24
+ return value === "home" || value === "project" || value === "projects" ? value : null;
25
25
  }
26
26
 
27
27
  function normalizeProjectSlug(value) {
@@ -51,6 +51,7 @@ export function updateHomeSurfacePreference(patch) {
51
51
  matesCollapsed: state.homeMatesCollapsed === true,
52
52
  chatScope: normalizeChatScope(state.homeChatScope),
53
53
  subSurface: normalizeSubSurface(state.homeSubSurface),
54
+ onboardingStep: state.onboardingStep || "welcome",
54
55
  };
55
56
  if (patch && Object.prototype.hasOwnProperty.call(patch, "surface")) next.surface = normalizePrimarySurface(patch.surface);
56
57
  if (patch && Object.prototype.hasOwnProperty.call(patch, "projectSlug")) next.projectSlug = normalizeProjectSlug(patch.projectSlug);
@@ -60,7 +61,12 @@ export function updateHomeSurfacePreference(patch) {
60
61
  if (patch && Object.prototype.hasOwnProperty.call(patch, "matesCollapsed")) next.matesCollapsed = patch.matesCollapsed === true;
61
62
  if (patch && Object.prototype.hasOwnProperty.call(patch, "chatScope")) next.chatScope = normalizeChatScope(patch.chatScope);
62
63
  if (patch && Object.prototype.hasOwnProperty.call(patch, "subSurface")) next.subSurface = normalizeSubSurface(patch.subSurface);
64
+ if (patch && ["welcome", "project", "complete"].indexOf(patch.onboardingStep) !== -1) {
65
+ next.onboardingStep = patch.onboardingStep;
66
+ store.set({ onboardingIntent: patch.onboardingStep });
67
+ }
63
68
  store.set({
69
+ onboardingStep: next.onboardingStep,
64
70
  homePrimarySurface: next.surface,
65
71
  homeSurfaceProjectSlug: next.projectSlug,
66
72
  homeActiveSessionByMate: normalizeSessions(next.activeSessionByMate),
@@ -78,6 +84,7 @@ export function updateHomeSurfacePreference(patch) {
78
84
  if (patch && Object.prototype.hasOwnProperty.call(patch, "matesCollapsed")) outgoing.matesCollapsed = next.matesCollapsed;
79
85
  if (patch && Object.prototype.hasOwnProperty.call(patch, "chatScope")) outgoing.chatScope = next.chatScope;
80
86
  if (patch && Object.prototype.hasOwnProperty.call(patch, "subSurface")) outgoing.subSurface = next.subSurface;
87
+ if (patch && Object.prototype.hasOwnProperty.call(patch, "onboardingStep")) outgoing.onboardingStep = next.onboardingStep;
81
88
  return send({ type: "home_surface_set", preference: outgoing });
82
89
  }
83
90
 
@@ -127,7 +134,11 @@ export function handleHomeSurfaceState(msg) {
127
134
  var savedSurface = normalizePrimarySurface(preference.surface);
128
135
  var savedProject = normalizeProjectSlug(preference.projectSlug);
129
136
  var intentConfirmed = !!intent && intent === savedSurface && intendedProject === savedProject;
137
+ var onboardingIntent = store.get('onboardingIntent');
138
+ var savedStep = ["welcome", "project", "complete"].indexOf(preference.onboardingStep) !== -1 ? preference.onboardingStep : "welcome";
130
139
  var update = {
140
+ onboardingStep: onboardingIntent || savedStep,
141
+ onboardingIntent: onboardingIntent === savedStep ? null : onboardingIntent,
131
142
  homeSurfaceLoaded: true,
132
143
  homePrimarySurface: intent || savedSurface,
133
144
  homeSurfaceProjectSlug: intent ? intendedProject : savedProject,
@@ -142,5 +153,6 @@ export function handleHomeSurfaceState(msg) {
142
153
  update.homePreferredMateId = store.get('homePreferredMateId') || preference.activeMateId || store.get('homeChatMateId') || null;
143
154
  }
144
155
  store.set(update);
156
+ if (onboardingIntent && onboardingIntent !== savedStep) send({ type: "home_surface_set", preference: { onboardingStep: onboardingIntent } });
145
157
  if (intent && !intentConfirmed) send({ type: "home_surface_set", preference: { surface: intent, projectSlug: intendedProject } });
146
158
  }
@@ -0,0 +1,62 @@
1
+ import { escapeHtml } from './utils.js';
2
+ import { iconHtml } from './icons.js';
3
+
4
+ export function ordinaryProjects(projects) {
5
+ return (Array.isArray(projects) ? projects : []).filter(function (project) {
6
+ return project && !project.isMate && project.slug;
7
+ });
8
+ }
9
+
10
+ function button(action, label, extra) {
11
+ return '<button type="button" class="projects-button ' + (extra || '') + '" data-projects-action="' + action + '">' + label + '</button>';
12
+ }
13
+
14
+ function setupAction(mode, icon, title, text, disabled) {
15
+ return '<button type="button" class="projects-setup-action" data-projects-mode="' + mode + '"' + (disabled ? ' disabled' : '') + '>' +
16
+ '<span class="projects-action-icon">' + iconHtml(icon) + '</span><span><strong>' + title + '</strong><span>' + text + '</span></span>' + iconHtml('arrow-up-right') + '</button>';
17
+ }
18
+
19
+ export function projectsHubContent(state) {
20
+ var projects = ordinaryProjects(state.projectsHubList);
21
+ var ready = state.projectListLoaded && state.homeSurfaceLoaded;
22
+ var welcome = ready && (state.projectsShowGuide || (!projects.length && (state.onboardingStep || 'welcome') === 'welcome'));
23
+ var allowed = state.projectsAccessLoaded && (!state.permissions || state.permissions.createProject === true);
24
+ var available = allowed && state.connected && !state.pendingHomeProjectSlug;
25
+ var html = '<header class="projects-header"><a class="projects-brand" href="/" data-projects-action="home"><img src="/clay-studio-symbol.png" alt="">Clay Studio</a>' +
26
+ '<nav aria-label="Workspace navigation">' + button('home', 'Home', 'projects-button-quiet') + button('settings', 'Settings', 'projects-button-quiet') + '</nav></header>';
27
+ html += '<main class="projects-main" aria-labelledby="projects-title">';
28
+ if (!ready) return html + '<div class="projects-loading" role="status"><h1 id="projects-title" tabindex="-1">Your projects</h1><p>Loading your workspace…</p></div></main>';
29
+ if (welcome) {
30
+ return html + '<section class="projects-welcome"><div class="projects-eyebrow">WELCOME TO CLAY</div>' +
31
+ '<h1 id="projects-title" tabindex="-1">A place for<br>your next idea.</h1>' +
32
+ '<p class="projects-lead">Bring a project. Describe what you want to build.<br>Your AI Driver helps you take it from there.</p>' +
33
+ '<div class="projects-intro-grid"><div><span>01 / PROJECTS</span><h2>Work on something real.</h2><p>Open a folder, start fresh, or clone a repository. Your conversations and tools stay with the project.</p></div>' +
34
+ '<div><span>02 / HOME</span><h2>Make room to think.</h2><p>Talk with Clay and your Mates, explore ideas, and return to your projects whenever you’re ready.</p></div></div>' +
35
+ '<div class="projects-welcome-actions">' + button('start', projects.length ? 'Explore your projects ' + iconHtml('arrow-right') : 'Set up your first project ' + iconHtml('arrow-right'), 'projects-button-primary') +
36
+ button('skip', 'Explore Home first', 'projects-button-quiet') + '</div>' +
37
+ '<p class="projects-footnote">You can revisit this guide from Projects at any time.</p></section></main>';
38
+ }
39
+ html += '<div class="projects-heading"><div><div class="projects-eyebrow">YOUR WORKSPACE</div><h1 id="projects-title" tabindex="-1">' + (projects.length ? 'Projects' : 'Start with a project.') + '</h1>' +
40
+ '<p class="projects-lead">' + (projects.length ? 'Pick up where you left off, or begin something new.' : 'Choose where your work will live. You can add more projects later.') + '</p></div>' + button('guide', 'Getting started', 'projects-button-quiet') + '</div>';
41
+ if (state.pendingHomeProjectSlug) html += '<p class="projects-status" role="status">Opening your project…</p>';
42
+ if (projects.length) {
43
+ html += '<div class="projects-list" aria-label="Your projects">';
44
+ projects.forEach(function (project) {
45
+ html += '<button type="button" class="projects-list-item" data-project-slug="' + escapeHtml(project.slug) + '"' + (!state.connected || state.pendingHomeProjectSlug ? ' disabled' : '') + '>' +
46
+ '<span class="projects-action-icon">' + iconHtml(project.isWorktree ? 'git-branch' : 'folder') + '</span><span><strong>' + escapeHtml(project.title || project.project || project.slug) + '</strong>' +
47
+ '<span>' + escapeHtml(project.branch || project.path || project.slug) + '</span></span>' + iconHtml('arrow-right') + '</button>';
48
+ });
49
+ html += '</div><h2 class="projects-section-title">Add a project</h2>';
50
+ }
51
+ if (!state.projectsAccessLoaded) html += '<p class="projects-status" role="status">Checking project access…</p>';
52
+ else if (!allowed) html += '<p class="projects-status">Ask an administrator to create or share a project with you.</p>';
53
+ else {
54
+ html += '<div class="projects-setup-actions">' +
55
+ setupAction('existing', 'folder-open', 'Open a folder', 'Use a directory on the machine running Clay.', !available) +
56
+ setupAction('create', 'plus', 'Create a project', 'Start in a new, empty project directory.', !available) +
57
+ setupAction('clone', 'git-branch', 'Clone a repository', 'Bring an existing Git repository into Clay.', !available) + '</div>';
58
+ if (!projects.length) html += '<p class="projects-next-step"><span>UP NEXT</span> Choose your AI in the workspace, then tell your Driver what you want to build or fix.</p>';
59
+ }
60
+ if (!state.connected) html += '<p class="projects-status" role="status">Reconnecting… Project actions will be available when the connection returns.</p>';
61
+ return html + '</main><footer class="projects-footer">Your projects, conversations, and tools. One workspace.</footer>';
62
+ }
@@ -0,0 +1,105 @@
1
+ import { store } from './store.js';
2
+ import { getWs } from './ws-ref.js';
3
+ import { hideHomeHub, showHomeHub } from './app-home-hub.js';
4
+ import { openAddProjectModal, switchProject } from './app-projects.js';
5
+ import { openUserSettings } from './user-settings.js';
6
+ import { rememberHomePrimarySurface, updateHomeSurfacePreference } from './home-surface.js';
7
+ import { projectsHubContent } from './projects-hub-view.js';
8
+ import { refreshIcons } from './icons.js';
9
+
10
+ function root() { return document.getElementById('projects-hub'); }
11
+
12
+ function render() {
13
+ var element = root();
14
+ if (!element || !store.get('projectsHubVisible')) return;
15
+ var html = projectsHubContent(store.snap());
16
+ if (store.get('projectsHubMarkup') === html) return;
17
+ store.set({ projectsHubMarkup: html });
18
+ var active = element.contains(document.activeElement) ? document.activeElement : null;
19
+ var action = active && (active.dataset.projectsAction || active.dataset.projectsMode || active.dataset.projectSlug);
20
+ element.innerHTML = html;
21
+ refreshIcons();
22
+ if (action) {
23
+ var buttons = element.querySelectorAll('button, a');
24
+ for (var i = 0; i < buttons.length; i++) {
25
+ if ((buttons[i].dataset.projectsAction || buttons[i].dataset.projectsMode || buttons[i].dataset.projectSlug) === action && !buttons[i].disabled) {
26
+ buttons[i].focus({ preventScroll: true });
27
+ break;
28
+ }
29
+ }
30
+ }
31
+ }
32
+
33
+ function focusHeading() {
34
+ if (root()) root().scrollTop = 0;
35
+ var title = document.getElementById('projects-title');
36
+ if (title) title.focus({ preventScroll: true });
37
+ }
38
+
39
+ export function showProjectsHub(fromHistory) {
40
+ hideHomeHub();
41
+ store.set({ projectsHubVisible: true, pendingHomeProjectSlug: null });
42
+ document.body.classList.add('projects-active');
43
+ root().classList.remove('hidden');
44
+ rememberHomePrimarySurface('projects');
45
+ if (!fromHistory && location.pathname !== '/projects') {
46
+ var method = document.documentElement.classList.contains('pwa-standalone') ? 'replaceState' : 'pushState';
47
+ history[method](null, '', '/projects');
48
+ }
49
+ render();
50
+ focusHeading();
51
+ }
52
+
53
+ export function hideProjectsHub() {
54
+ if (!store.get('projectsHubVisible')) return;
55
+ store.set({ projectsHubVisible: false, projectsShowGuide: false });
56
+ document.body.classList.remove('projects-active');
57
+ if (root()) root().classList.add('hidden');
58
+ }
59
+
60
+ export function completeProjectsOnboarding() {
61
+ if (store.get('onboardingStep') !== 'project') return;
62
+ updateHomeSurfacePreference({ onboardingStep: 'complete' });
63
+ }
64
+
65
+ function handleClick(event) {
66
+ var button = event.target.closest('[data-projects-action], [data-projects-mode], [data-project-slug]');
67
+ if (!button || button.disabled) return;
68
+ event.preventDefault();
69
+ var action = button.dataset.projectsAction;
70
+ if (action === 'home') { showHomeHub(); return; }
71
+ if (action === 'settings') { openUserSettings(); return; }
72
+ if (action === 'guide') { store.set({ projectsShowGuide: true }); render(); focusHeading(); return; }
73
+ if (action === 'start' || action === 'skip') {
74
+ store.set({ projectsShowGuide: false });
75
+ updateHomeSurfacePreference({ onboardingStep: 'project' });
76
+ if (action === 'skip') showHomeHub();
77
+ else { render(); focusHeading(); }
78
+ return;
79
+ }
80
+ var ws = getWs();
81
+ if (!ws || ws.readyState !== 1) return;
82
+ if (button.dataset.projectSlug) { switchProject(button.dataset.projectSlug); return; }
83
+ if (button.dataset.projectsMode) {
84
+ var permissions = store.get('permissions');
85
+ if (!store.get('projectsAccessLoaded') || (permissions && permissions.createProject !== true)) return;
86
+ openAddProjectModal(button.dataset.projectsMode);
87
+ }
88
+ }
89
+
90
+ export function initProjectsHub() {
91
+ root().addEventListener('click', handleClick);
92
+ store.subscribe(function (state, previous) {
93
+ if (state.projectsHubRestoreRequested && !previous.projectsHubRestoreRequested) {
94
+ showProjectsHub(true);
95
+ if (location.pathname === '/') history.replaceState(null, '', '/projects');
96
+ store.set({ projectsHubRestoreRequested: false });
97
+ return;
98
+ }
99
+ if (!state.projectsHubVisible) return;
100
+ if (state.projectsHubList !== previous.projectsHubList || state.projectListLoaded !== previous.projectListLoaded ||
101
+ state.homeSurfaceLoaded !== previous.homeSurfaceLoaded || state.onboardingStep !== previous.onboardingStep ||
102
+ state.projectsAccessLoaded !== previous.projectsAccessLoaded || state.permissions !== previous.permissions ||
103
+ state.connected !== previous.connected || state.pendingHomeProjectSlug !== previous.pendingHomeProjectSlug) render();
104
+ });
105
+ }
@@ -57,3 +57,5 @@
57
57
  @import url("css/tui-attention.css?v=20260829-terminal-canvas1");
58
58
  @import url("css/pwa-mobile.css?v=20260824f");
59
59
  @import url("css/avatar-imprints.css?v=20260829-imprints1");
60
+
61
+ @import url("css/projects-hub.css");
package/lib/sdk-bridge.js CHANGED
@@ -670,6 +670,7 @@ function createSDKBridge(opts) {
670
670
  close_partner: true,
671
671
  record_partner_evaluation: true,
672
672
  respond_to_worker_permission: true,
673
+ respond_to_worker_question: true,
673
674
  };
674
675
  var pairDynamicTools = {
675
676
  propose_worker: true,
@@ -689,6 +690,7 @@ function createSDKBridge(opts) {
689
690
  close_partner: true,
690
691
  record_partner_evaluation: true,
691
692
  respond_to_worker_permission: true,
693
+ respond_to_worker_question: true,
692
694
  };
693
695
  if (toolName.indexOf("mcp__") === 0) {
694
696
  if (toolName.indexOf(PAIR_MCP_PREFIX) === 0
package/lib/server.js CHANGED
@@ -814,7 +814,7 @@ function createServer(opts) {
814
814
 
815
815
  // Root path — render the default project workspace while keeping Home
816
816
  // available as an explicit client-side surface.
817
- if (fullUrl === "/" && req.method === "GET") {
817
+ if ((fullUrl === "/" || fullUrl === "/projects" || fullUrl === "/projects/") && req.method === "GET") {
818
818
  if (!isRequestAuthed(req)) {
819
819
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
820
820
  res.end(auth.getAuthPage());
@@ -31,8 +31,8 @@ function attachHomeSurfacePreferences(deps) {
31
31
  if (!validId(mateIds[i]) || !validSessionReference(sessions[mateIds[i]])) continue;
32
32
  normalizedSessions[mateIds[i]] = sessions[mateIds[i]];
33
33
  }
34
- return {
35
- surface: source.surface === "home" || source.surface === "project" ? source.surface : null,
34
+ var result = {
35
+ surface: source.surface === "home" || source.surface === "project" || source.surface === "projects" ? source.surface : null,
36
36
  projectSlug: validProjectSlug(source.projectSlug) ? source.projectSlug : null,
37
37
  activeMateId: validId(source.activeMateId) ? source.activeMateId : null,
38
38
  activeSessionByMate: normalizedSessions,
@@ -41,6 +41,8 @@ function attachHomeSurfacePreferences(deps) {
41
41
  chatScope: source.chatScope === "current" ? "current" : "all",
42
42
  subSurface: source.subSurface === "debates" ? "debates" : "chat",
43
43
  };
44
+ if (["welcome", "project", "complete"].indexOf(source.onboardingStep) !== -1) result.onboardingStep = source.onboardingStep;
45
+ return result;
44
46
  }
45
47
 
46
48
  function loadSingleUserPreference() {
@@ -484,40 +484,6 @@ function attachPreferences(deps) {
484
484
  return { error: "User not found" };
485
485
  }
486
486
 
487
- // --- What's New seen ids ---
488
- //
489
- // Per-user list of dismissed "What's New" entry ids. The whats-new
490
- // service uses this to filter unseen entries on connect. Ids are stable
491
- // strings authored in lib/whats-new-content.js.
492
-
493
- function getWhatsNewSeenIds(userId) {
494
- var data = loadUsers();
495
- for (var i = 0; i < data.users.length; i++) {
496
- if (data.users[i].id === userId) {
497
- var ids = data.users[i].whatsNewSeenIds;
498
- return Array.isArray(ids) ? ids.slice() : [];
499
- }
500
- }
501
- return [];
502
- }
503
-
504
- function markWhatsNewSeen(userId, entryId) {
505
- if (!entryId || typeof entryId !== "string") return { error: "bad_id" };
506
- var data = loadUsers();
507
- for (var i = 0; i < data.users.length; i++) {
508
- if (data.users[i].id === userId) {
509
- var ids = Array.isArray(data.users[i].whatsNewSeenIds) ? data.users[i].whatsNewSeenIds.slice() : [];
510
- if (ids.indexOf(entryId) === -1) {
511
- ids.push(entryId);
512
- data.users[i].whatsNewSeenIds = ids;
513
- saveUsers(data);
514
- }
515
- return { ok: true, seenIds: ids };
516
- }
517
- }
518
- return { error: "User not found" };
519
- }
520
-
521
487
  // --- Mate onboarding ---
522
488
 
523
489
  function setMateOnboarded(userId) {
@@ -556,8 +522,6 @@ function attachPreferences(deps) {
556
522
  setMatesEnabled: setMatesEnabled,
557
523
  getToolPalettes: getToolPalettes,
558
524
  setToolPalette: setToolPalette,
559
- getWhatsNewSeenIds: getWhatsNewSeenIds,
560
- markWhatsNewSeen: markWhatsNewSeen,
561
525
  getTerminalFont: getTerminalFont,
562
526
  setTerminalFont: setTerminalFont,
563
527
  setMateOnboarded: setMateOnboarded,
package/lib/users.js CHANGED
@@ -453,8 +453,6 @@ var getMatesEnabled = preferences.getMatesEnabled;
453
453
  var setMatesEnabled = preferences.setMatesEnabled;
454
454
  var getToolPalettes = preferences.getToolPalettes;
455
455
  var setToolPalette = preferences.setToolPalette;
456
- var getWhatsNewSeenIds = preferences.getWhatsNewSeenIds;
457
- var markWhatsNewSeen = preferences.markWhatsNewSeen;
458
456
  var getTerminalFont = preferences.getTerminalFont;
459
457
  var setTerminalFont = preferences.setTerminalFont;
460
458
  var setMateOnboarded = preferences.setMateOnboarded;
@@ -541,8 +539,6 @@ module.exports = {
541
539
  setCapsulesEnabled: setCapsulesEnabled,
542
540
  getToolPalettes: getToolPalettes,
543
541
  setToolPalette: setToolPalette,
544
- getWhatsNewSeenIds: getWhatsNewSeenIds,
545
- markWhatsNewSeen: markWhatsNewSeen,
546
542
  getTerminalFont: getTerminalFont,
547
543
  setTerminalFont: setTerminalFont,
548
544
  getHomeDockPreference: getHomeDockPreference,
package/lib/ws-schema.js CHANGED
@@ -332,9 +332,6 @@ var schema = {
332
332
  // -----------------------------------------------------------------------
333
333
  // What's New
334
334
  // -----------------------------------------------------------------------
335
- "whats_new_state": { direction: "s2c", handler: "lib/public/modules/whats-new.js", description: "Unseen What's New entries for the connecting user" },
336
- "whats_new_seen": { direction: "c2s", handler: "lib/project-sessions.js", description: "Mark a What's New entry as dismissed by the current user" },
337
- "whats_new_seen_result": { direction: "s2c", handler: "lib/public/modules/whats-new.js", description: "Result of marking a What's New entry seen" },
338
335
  "home_dock_get": { direction: "c2s", handler: "lib/server-home-preferences.js", description: "Request the current user's home dock preference" },
339
336
  "home_dock_set": { direction: "c2s", handler: "lib/server-home-preferences.js", description: "Persist the current user's home dock preference" },
340
337
  "home_dock_state": { direction: "s2c", handler: "lib/public/modules/home-dock.js", description: "Current user home dock preference" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "4.6.1-beta.1",
3
+ "version": "4.7.0-beta.2",
4
4
  "description": "Self-hosted team workspace for Claude Code and Codex. Multi-user, browser-based, with persistent AI mates.",
5
5
  "bin": {
6
6
  "clay-server": "./bin/cli.js"
@@ -1,70 +0,0 @@
1
- // What's New article viewer.
2
- //
3
- // A full-screen blog-style reader for a single What's New entry. Opened
4
- // from either:
5
- // - the carousel popup ("Read more" jumps straight here)
6
- // - the home page list of titles (click on a title)
7
- //
8
- // Closing returns the user to whichever surface was visible underneath
9
- // (home or a session). The viewer is a fixed-position overlay so we
10
- // don't need to track or restore the previous view ourselves - hiding
11
- // this element simply reveals the layer below.
12
-
13
- import { getKnownEntries } from './whats-new.js';
14
-
15
- var rootEl = null;
16
- var dateEl = null;
17
- var titleEl = null;
18
- var bodyEl = null;
19
- var backBtn = null;
20
- var keyHandlerBound = false;
21
- var visible = false;
22
-
23
- export function initWhatsNewArticle() {
24
- rootEl = document.getElementById("whats-new-article");
25
- if (!rootEl) return;
26
- dateEl = document.getElementById("wna-date");
27
- titleEl = document.getElementById("wna-title");
28
- bodyEl = document.getElementById("wna-body");
29
- backBtn = document.getElementById("wna-back");
30
-
31
- if (backBtn) backBtn.addEventListener("click", function () { closeArticle(); });
32
- if (!keyHandlerBound) {
33
- document.addEventListener("keydown", function (ev) {
34
- if (visible && ev.key === "Escape") {
35
- ev.stopPropagation();
36
- closeArticle();
37
- }
38
- });
39
- keyHandlerBound = true;
40
- }
41
- }
42
-
43
- export function openArticle(entryId) {
44
- if (!rootEl) return;
45
- var entries = getKnownEntries();
46
- var entry = null;
47
- for (var i = 0; i < entries.length; i++) {
48
- if (entries[i] && entries[i].id === entryId) { entry = entries[i]; break; }
49
- }
50
- if (!entry) return;
51
-
52
- if (dateEl) dateEl.textContent = entry.publishedAt || "";
53
- if (titleEl) titleEl.textContent = entry.title || "";
54
- if (bodyEl) bodyEl.innerHTML = (typeof entry.body === "string") ? entry.body : "";
55
-
56
- rootEl.classList.remove("hidden");
57
- // Scroll to top in case a previous article was open.
58
- try { rootEl.scrollTop = 0; } catch (e) {}
59
- visible = true;
60
- }
61
-
62
- export function closeArticle() {
63
- if (!rootEl) return;
64
- rootEl.classList.add("hidden");
65
- visible = false;
66
- }
67
-
68
- export function isArticleVisible() {
69
- return visible;
70
- }