clay-server 5.0.0-beta.3 → 5.0.0-beta.4
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.
- package/lib/public/app.js +2 -0
- package/lib/public/css/fonts.css +6 -0
- package/lib/public/css/home-hub.css +52 -0
- package/lib/public/css/session-folders.css +19 -0
- package/lib/public/index.html +10 -1
- package/lib/public/modules/app-home-hub.js +60 -7
- package/lib/public/modules/home-project-empty-access.js +44 -0
- package/lib/public/modules/project-activation.js +11 -0
- package/lib/public/modules/session-create.js +2 -0
- package/lib/public/modules/session-date-group-header.js +57 -0
- package/lib/public/modules/session-date-groups.js +84 -0
- package/lib/public/modules/session-folder-delete.js +28 -2
- package/lib/public/modules/session-folders.js +2 -0
- package/lib/public/modules/sidebar-creation-effect.js +130 -0
- package/lib/public/modules/sidebar-mobile.js +1 -0
- package/lib/public/modules/sidebar-session-folders-ui.js +14 -2
- package/lib/public/modules/sidebar-sessions.js +2 -1
- package/lib/public/modules/sidebar.js +20 -5
- package/package.json +1 -1
package/lib/public/app.js
CHANGED
|
@@ -287,6 +287,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
|
|
|
287
287
|
sessionFolders: null,
|
|
288
288
|
sessionFoldersSlug: null,
|
|
289
289
|
sessionFolderCreate: null,
|
|
290
|
+
sidebarCreationEffects: [],
|
|
290
291
|
sessionSearch: null,
|
|
291
292
|
sessionFolderViewMenu: null,
|
|
292
293
|
sessionFolderLayouts: {},
|
|
@@ -316,6 +317,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
|
|
|
316
317
|
splitGroupsProjectSlug: null,
|
|
317
318
|
socketPath: null,
|
|
318
319
|
pendingHomeProjectSlug: null,
|
|
320
|
+
homeProjectEmptyVisible: false,
|
|
319
321
|
projectsHubVisible: false,
|
|
320
322
|
projectsHubMarkup: null,
|
|
321
323
|
projectsHubList: [],
|
package/lib/public/css/fonts.css
CHANGED
|
@@ -10,5 +10,11 @@
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
:root {
|
|
13
|
+
--font-ui: "Geist", "Twemoji", system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/* Keep the bundled Korean face scoped so other CJK scripts use platform fallback. */
|
|
17
|
+
[lang|="ko"] {
|
|
13
18
|
--font-ui: "Geist", "Noto Sans KR", "Twemoji", system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
|
|
19
|
+
font-family: var(--font-ui);
|
|
14
20
|
}
|
|
@@ -224,6 +224,55 @@ body.home-surface-boot-pending #connect-overlay.hidden {
|
|
|
224
224
|
height: 14px;
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
+
.home-project-empty {
|
|
228
|
+
position: absolute;
|
|
229
|
+
inset: 0;
|
|
230
|
+
z-index: 5;
|
|
231
|
+
display: grid;
|
|
232
|
+
place-items: center;
|
|
233
|
+
padding: 40px 24px;
|
|
234
|
+
background: var(--bg);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.home-project-empty[hidden] { display: none; }
|
|
238
|
+
|
|
239
|
+
.home-project-empty-content {
|
|
240
|
+
width: min(100%, 420px);
|
|
241
|
+
text-align: center;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.home-project-empty-icon {
|
|
245
|
+
display: inline-grid;
|
|
246
|
+
place-items: center;
|
|
247
|
+
width: 48px;
|
|
248
|
+
height: 48px;
|
|
249
|
+
margin-bottom: 18px;
|
|
250
|
+
border: 1px solid var(--border-subtle);
|
|
251
|
+
border-radius: 14px;
|
|
252
|
+
background: rgba(var(--overlay-rgb), 0.04);
|
|
253
|
+
color: var(--text-secondary);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.home-project-empty-icon .lucide { width: 20px; height: 20px; }
|
|
257
|
+
.home-project-empty-eyebrow { margin: 0 0 8px; color: var(--text-dimmer); font-size: 10px; font-weight: 650; letter-spacing: 0.13em; }
|
|
258
|
+
.home-project-empty h1 { margin: 0; color: var(--text); font-size: clamp(24px, 4vw, 34px); font-weight: 620; letter-spacing: -0.035em; }
|
|
259
|
+
.home-project-empty-status { max-width: 340px; margin: 12px auto 22px; color: var(--text-muted); font-size: 13px; line-height: 1.55; }
|
|
260
|
+
.home-project-empty button {
|
|
261
|
+
min-height: 40px;
|
|
262
|
+
padding: 8px 16px;
|
|
263
|
+
border: 1px solid var(--accent);
|
|
264
|
+
border-radius: 9px;
|
|
265
|
+
background: var(--accent);
|
|
266
|
+
color: var(--accent-text, #fff);
|
|
267
|
+
font: inherit;
|
|
268
|
+
font-size: 12px;
|
|
269
|
+
font-weight: 620;
|
|
270
|
+
cursor: pointer;
|
|
271
|
+
}
|
|
272
|
+
.home-project-empty button:hover:not(:disabled) { filter: brightness(1.06); }
|
|
273
|
+
.home-project-empty button:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
|
|
274
|
+
.home-project-empty button:disabled { border-color: var(--border-subtle); background: rgba(var(--overlay-rgb), 0.05); color: var(--text-dimmer); cursor: default; }
|
|
275
|
+
|
|
227
276
|
.home-mate-chat-stage {
|
|
228
277
|
position: relative;
|
|
229
278
|
flex: 1 1 auto;
|
|
@@ -982,6 +1031,9 @@ body.home-surface-boot-pending #connect-overlay.hidden {
|
|
|
982
1031
|
}
|
|
983
1032
|
|
|
984
1033
|
@media (max-width: 768px) {
|
|
1034
|
+
.home-project-empty { padding: 32px 20px; }
|
|
1035
|
+
.home-project-empty button { min-height: 44px; }
|
|
1036
|
+
|
|
985
1037
|
#home-hub:not(.dock-split):not(.dock-focus) .home-mate-chat-stage {
|
|
986
1038
|
padding-top: 52px;
|
|
987
1039
|
}
|
|
@@ -37,6 +37,13 @@
|
|
|
37
37
|
.session-folder-flat { margin-top: 4px; }
|
|
38
38
|
.session-folder-flat .session-folder-body { border: 0; }
|
|
39
39
|
.session-folder-empty { padding: 5px 4px 6px; font-size: 10.5px; line-height: 1.4; color: var(--text-muted); opacity: 0.78; }
|
|
40
|
+
.session-folder-body > .session-date-group-header { margin: 7px 4px 2px 6px; min-height: 20px; }
|
|
41
|
+
.session-folder-body > .session-date-group-header:first-child { margin-top: 3px; }
|
|
42
|
+
.session-date-group-count { margin-left: auto; color: var(--text-dimmer); font-size: 9.5px; font-variant-numeric: tabular-nums; }
|
|
43
|
+
.session-date-group-header .session-group-clear-btn { margin-left: 5px; height: 18px; padding-inline: 7px; }
|
|
44
|
+
.session-date-group-header .session-group-clear-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
|
|
45
|
+
.session-date-group-header.is-mobile { min-height: 44px; margin-top: 4px; font-size: 11px; }
|
|
46
|
+
.session-date-group-header.is-mobile .session-group-clear-btn { min-width: 44px; min-height: 44px; font-size: 11px; }
|
|
40
47
|
|
|
41
48
|
.session-folder-unit { position: relative; border-radius: 8px; }
|
|
42
49
|
.session-folder-unit.dragging, .session-folder-header.dragging { opacity: 0.45; }
|
|
@@ -376,8 +383,20 @@
|
|
|
376
383
|
.session-folder-new-btn { height: 34px; min-width: 44px; }
|
|
377
384
|
.session-create-row.is-mobile .session-create-picker, .session-create-row.is-mobile .session-create-icon-btn, .session-create-row.is-mobile .session-create-menu-item { min-height: 40px; }
|
|
378
385
|
}
|
|
386
|
+
|
|
387
|
+
/* A successful local creation keeps its normal geometry and readability while
|
|
388
|
+
the restrained production Thinking sheen crosses the title twice. */
|
|
389
|
+
.sidebar-creation-shimmer {
|
|
390
|
+
--creation-shimmer-base: var(--text-secondary);
|
|
391
|
+
background-image: linear-gradient(100deg, var(--creation-shimmer-base) 40%, var(--text-dimmer) 50%, var(--creation-shimmer-base) 60%);
|
|
392
|
+
background-repeat: no-repeat; background-size: 300% 100%; background-position: 100% 0;
|
|
393
|
+
background-clip: text; -webkit-background-clip: text; color: transparent; -webkit-text-fill-color: transparent;
|
|
394
|
+
animation: thinking-summary-shimmer 2.8s ease-in-out 2 both;
|
|
395
|
+
}
|
|
396
|
+
.session-folder-label.sidebar-creation-shimmer { --creation-shimmer-base: var(--text-muted); }
|
|
379
397
|
@media (prefers-reduced-motion: reduce) {
|
|
380
398
|
.session-folder-new-btn, .session-folder-new-label { transition: none; }
|
|
381
399
|
.session-folder-new-btn.is-label-changing .session-folder-new-label-text,
|
|
382
400
|
.session-folder-new-btn.is-label-changing .session-folder-new-label-previous { animation: none; }
|
|
401
|
+
.sidebar-creation-shimmer { animation: none; background: none; color: inherit; -webkit-text-fill-color: currentColor; }
|
|
383
402
|
}
|
package/lib/public/index.html
CHANGED
|
@@ -137,7 +137,16 @@
|
|
|
137
137
|
</aside>
|
|
138
138
|
<div id="home-workspace" class="home-workspace">
|
|
139
139
|
<button id="home-sidebar-expand" class="home-sidebar-expand" type="button" title="Show sidebar" aria-label="Show Home sidebar" aria-describedby="home-sidebar-expand-brand-label"><i data-lucide="panel-left-open"></i><span id="home-sidebar-expand-brand-label" class="home-sidebar-brand-wordmark home-sidebar-expand-wordmark">Clay Studio</span></button>
|
|
140
|
-
<button id="home-close-control" class="home-close-control" type="button" title="
|
|
140
|
+
<button id="home-close-control" class="home-close-control" type="button" title="Return to project" aria-label="Return to project"><i data-lucide="folder" aria-hidden="true"></i><span>Project</span></button>
|
|
141
|
+
<section id="home-project-empty" class="home-project-empty" aria-labelledby="home-project-empty-title" hidden>
|
|
142
|
+
<div class="home-project-empty-content">
|
|
143
|
+
<span class="home-project-empty-icon" aria-hidden="true"><i data-lucide="folder-plus"></i></span>
|
|
144
|
+
<p class="home-project-empty-eyebrow">PROJECT WORKSPACE</p>
|
|
145
|
+
<h1 id="home-project-empty-title" tabindex="-1">Start a new project</h1>
|
|
146
|
+
<p id="home-project-empty-status" class="home-project-empty-status" role="status">Create an empty project, then continue in its workspace.</p>
|
|
147
|
+
<button id="home-project-empty-create" type="button">Start a new project</button>
|
|
148
|
+
</div>
|
|
149
|
+
</section>
|
|
141
150
|
<div id="home-conversation-region" class="home-conversation-region">
|
|
142
151
|
<div id="home-mate-chat" class="home-mate-chat">
|
|
143
152
|
<div class="home-mate-chat-stage">
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
import { showProjectsHub, hideProjectsHub } from './projects-hub.js';
|
|
4
4
|
import { store } from './store.js';
|
|
5
5
|
import { getWs } from './ws-ref.js';
|
|
6
|
-
import { getCachedProjects, switchProject } from './app-projects.js';
|
|
7
|
-
import { chooseProjectActivationTarget } from './project-activation.js';
|
|
6
|
+
import { getCachedProjects, openAddProjectModal, switchProject } from './app-projects.js';
|
|
7
|
+
import { chooseProjectActivationTarget, homeProjectCreationState } from './project-activation.js';
|
|
8
8
|
import { mateAvatarUrl } from './avatar.js';
|
|
9
9
|
import { exitDmMode } from './app-dm.js';
|
|
10
10
|
import { closeHomeDock, initHomeDock, renderDock, requestHomeDockPreference } from './home-dock.js';
|
|
@@ -22,6 +22,7 @@ import { disposeHomeSessionActions } from './home-session-actions.js';
|
|
|
22
22
|
import { initHomeDebatesArchive, restoreHomeDebatesArchive } from './home-debates-archive.js';
|
|
23
23
|
import { isHomeDebatesSurface } from './home-sub-surface.js';
|
|
24
24
|
import { HOME_CAPSULE_CREATION_EVENT } from './home-capsule-creation-intent.js';
|
|
25
|
+
import { bindHomeProjectEmptyDismissal, setHomeProjectEmptyCoverage } from './home-project-empty-access.js';
|
|
25
26
|
|
|
26
27
|
var homeHub = null;
|
|
27
28
|
var homeHubVisible = false;
|
|
@@ -44,6 +45,13 @@ function syncHomeCloseControl() {
|
|
|
44
45
|
var close = document.getElementById("home-close-control");
|
|
45
46
|
if (!close) return;
|
|
46
47
|
close.classList.remove("hidden");
|
|
48
|
+
var loaded = store.get('projectListLoaded') === true;
|
|
49
|
+
var slug = loaded ? getHomeReturnSlug() : null;
|
|
50
|
+
var label = close.querySelector("span");
|
|
51
|
+
close.disabled = !loaded;
|
|
52
|
+
close.title = !loaded ? "Loading projects" : (slug ? "Return to project" : "Project options");
|
|
53
|
+
close.setAttribute("aria-label", close.title);
|
|
54
|
+
if (label) label.textContent = loaded ? "Project" : "Loading…";
|
|
47
55
|
}
|
|
48
56
|
|
|
49
57
|
function getHomeReturnSlug() {
|
|
@@ -53,6 +61,34 @@ function getHomeReturnSlug() {
|
|
|
53
61
|
);
|
|
54
62
|
}
|
|
55
63
|
|
|
64
|
+
function syncHomeProjectEmptyState() {
|
|
65
|
+
var empty = document.getElementById("home-project-empty");
|
|
66
|
+
if (!empty) return;
|
|
67
|
+
var requested = store.get('homeProjectEmptyVisible') === true;
|
|
68
|
+
var loaded = store.get('projectListLoaded') === true;
|
|
69
|
+
var returnSlug = loaded ? getHomeReturnSlug() : null;
|
|
70
|
+
var visible = homeHubVisible && requested && loaded && !returnSlug;
|
|
71
|
+
setHomeProjectEmptyCoverage(document, visible);
|
|
72
|
+
empty.hidden = !visible;
|
|
73
|
+
if (requested && loaded && returnSlug) store.set({ homeProjectEmptyVisible: false });
|
|
74
|
+
if (!visible) return;
|
|
75
|
+
var availability = homeProjectCreationState(store.snap());
|
|
76
|
+
var action = document.getElementById("home-project-empty-create");
|
|
77
|
+
var status = document.getElementById("home-project-empty-status");
|
|
78
|
+
action.disabled = !availability.enabled;
|
|
79
|
+
if (status) status.textContent = availability.message;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function dismissHomeProjectEmpty() {
|
|
83
|
+
if (store.get('homeProjectEmptyVisible')) store.set({ homeProjectEmptyVisible: false });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function openHomeProjectCreation() {
|
|
87
|
+
var availability = homeProjectCreationState(store.snap());
|
|
88
|
+
if (!availability.enabled || getHomeReturnSlug()) return;
|
|
89
|
+
openAddProjectModal("create");
|
|
90
|
+
}
|
|
91
|
+
|
|
56
92
|
function getVisibleMates() {
|
|
57
93
|
var mates = (store.get('cachedMatesList') || []).filter(function (mate) {
|
|
58
94
|
return !!mate && !mate.archived;
|
|
@@ -100,6 +136,7 @@ function isMateBusy(mate, projectStatus, mentionActive) {
|
|
|
100
136
|
}
|
|
101
137
|
|
|
102
138
|
function selectHomeMate(mateId) {
|
|
139
|
+
if (store.get('homeProjectEmptyVisible')) store.set({ homeProjectEmptyVisible: false });
|
|
103
140
|
if (mateId !== store.get('homeChatMateId') || isHomeDebatesSurface()) openHomeChat(mateId);
|
|
104
141
|
closeHomeSidebarAfterSelection();
|
|
105
142
|
}
|
|
@@ -246,14 +283,20 @@ export function initHomeHub() {
|
|
|
246
283
|
initHomeDock();
|
|
247
284
|
initHomeDebatesArchive();
|
|
248
285
|
document.getElementById("home-mate-list").addEventListener("keydown", handleMateListKeydown);
|
|
249
|
-
document.getElementById("home-
|
|
286
|
+
bindHomeProjectEmptyDismissal(document.getElementById("home-sidebar"), dismissHomeProjectEmpty);
|
|
287
|
+
document.getElementById("home-close-control").addEventListener("click", minimizeHomeHub);
|
|
288
|
+
document.getElementById("home-project-empty-create").addEventListener("click", openHomeProjectCreation);
|
|
250
289
|
document.getElementById("home-nav-projects").addEventListener("click", function () { showProjectsHub(); });
|
|
251
290
|
window.addEventListener("clay:home-debate", function () { hideHomeHub(); });
|
|
252
291
|
document.addEventListener("visibilitychange", syncHomePresentation);
|
|
253
292
|
|
|
254
293
|
store.subscribe(function (state, prev) {
|
|
255
294
|
if (state.connected !== prev.connected && state.connected && homeHubVisible) syncHomePresentation();
|
|
256
|
-
if (state.currentSlug !== prev.currentSlug
|
|
295
|
+
if (state.currentSlug !== prev.currentSlug || state.homeSurfaceProjectSlug !== prev.homeSurfaceProjectSlug ||
|
|
296
|
+
state.projectListLoaded !== prev.projectListLoaded || state.projectsHubList !== prev.projectsHubList) syncHomeCloseControl();
|
|
297
|
+
if (state.homeProjectEmptyVisible !== prev.homeProjectEmptyVisible || state.projectListLoaded !== prev.projectListLoaded ||
|
|
298
|
+
state.projectsHubList !== prev.projectsHubList || state.projectsAccessLoaded !== prev.projectsAccessLoaded ||
|
|
299
|
+
state.permissions !== prev.permissions || state.connected !== prev.connected) syncHomeProjectEmptyState();
|
|
257
300
|
if (state.homeChatMateId !== prev.homeChatMateId || state.cachedMatesList !== prev.cachedMatesList) renderHomeMateSwitcher();
|
|
258
301
|
if (state.homeSidebarCollapsed !== prev.homeSidebarCollapsed && !state.homeSidebarCollapsed) {
|
|
259
302
|
requestAnimationFrame(renderHomeMateSwitcher);
|
|
@@ -352,7 +395,7 @@ export function updateHomeIconBadge() {
|
|
|
352
395
|
|
|
353
396
|
export function showHomeHub(fromHistory) {
|
|
354
397
|
hideProjectsHub();
|
|
355
|
-
store.set({ pendingHomeProjectSlug: null });
|
|
398
|
+
store.set({ pendingHomeProjectSlug: null, homeProjectEmptyVisible: false });
|
|
356
399
|
if (store.get('dmMode')) exitDmMode();
|
|
357
400
|
var activeMateId = store.get('homeChatMateId');
|
|
358
401
|
var resume = homeHubSuspended || homeHubVisible;
|
|
@@ -363,6 +406,7 @@ export function showHomeHub(fromHistory) {
|
|
|
363
406
|
rememberHomePrimarySurface("home");
|
|
364
407
|
syncHomePresentation();
|
|
365
408
|
syncHomeCloseControl();
|
|
409
|
+
syncHomeProjectEmptyState();
|
|
366
410
|
renderHomeMateSwitcher();
|
|
367
411
|
if (activeMateId && !resume && !isHomeDebatesSurface()) openHomeChat(activeMateId);
|
|
368
412
|
if (isHomeDebatesSurface()) restoreHomeDebatesArchive();
|
|
@@ -392,6 +436,7 @@ export function hideHomeHub() {
|
|
|
392
436
|
disposeHomeSessionActions();
|
|
393
437
|
homeHubVisible = false;
|
|
394
438
|
homeHubSuspended = true;
|
|
439
|
+
if (store.get('homeProjectEmptyVisible')) store.set({ homeProjectEmptyVisible: false });
|
|
395
440
|
hideHomeShell();
|
|
396
441
|
homeHub.classList.add("hidden");
|
|
397
442
|
syncHomePresentation();
|
|
@@ -402,9 +447,17 @@ export function hideHomeHub() {
|
|
|
402
447
|
}
|
|
403
448
|
|
|
404
449
|
export function minimizeHomeHub() {
|
|
405
|
-
var slug = getHomeReturnSlug();
|
|
406
450
|
if (!homeHubVisible) return;
|
|
407
|
-
if (
|
|
451
|
+
if (store.get('projectListLoaded') !== true) return;
|
|
452
|
+
var slug = getHomeReturnSlug();
|
|
453
|
+
if (!slug) {
|
|
454
|
+
store.set({ homeProjectEmptyVisible: true });
|
|
455
|
+
syncHomeProjectEmptyState();
|
|
456
|
+
var title = document.getElementById("home-project-empty-title");
|
|
457
|
+
if (title) title.focus({ preventScroll: true });
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
if (store.get('homeProjectEmptyVisible')) store.set({ homeProjectEmptyVisible: false });
|
|
408
461
|
switchProject(slug);
|
|
409
462
|
var projectInput = document.getElementById("input");
|
|
410
463
|
if (projectInput && !projectInput.disabled) projectInput.focus({ preventScroll: true });
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Accessibility boundary for the Home no-project overlay.
|
|
2
|
+
|
|
3
|
+
var coveredIds = ["home-conversation-region", "home-dock-backdrop", "home-dock-divider", "home-tool-workbench"];
|
|
4
|
+
var priorStates = new WeakMap();
|
|
5
|
+
|
|
6
|
+
function remember(element) {
|
|
7
|
+
if (priorStates.has(element)) return;
|
|
8
|
+
priorStates.set(element, {
|
|
9
|
+
inert: element.inert === true,
|
|
10
|
+
inertAttribute: element.hasAttribute("inert"),
|
|
11
|
+
ariaHidden: element.getAttribute("aria-hidden"),
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function restore(element) {
|
|
16
|
+
var prior = priorStates.get(element);
|
|
17
|
+
if (!prior) return;
|
|
18
|
+
element.inert = prior.inert;
|
|
19
|
+
if (prior.inertAttribute) element.setAttribute("inert", "");
|
|
20
|
+
else element.removeAttribute("inert");
|
|
21
|
+
if (prior.ariaHidden === null) element.removeAttribute("aria-hidden");
|
|
22
|
+
else element.setAttribute("aria-hidden", prior.ariaHidden);
|
|
23
|
+
priorStates.delete(element);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function setHomeProjectEmptyCoverage(doc, covered) {
|
|
27
|
+
for (var i = 0; i < coveredIds.length; i++) {
|
|
28
|
+
var element = doc.getElementById(coveredIds[i]);
|
|
29
|
+
if (!element) continue;
|
|
30
|
+
if (!covered) { restore(element); continue; }
|
|
31
|
+
remember(element);
|
|
32
|
+
element.inert = true;
|
|
33
|
+
element.setAttribute("inert", "");
|
|
34
|
+
element.setAttribute("aria-hidden", "true");
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function bindHomeProjectEmptyDismissal(sidebar, dismiss) {
|
|
39
|
+
sidebar.addEventListener("click", function (event) {
|
|
40
|
+
var control = event.target.closest("#home-sidebar-new, #home-sidebar-all, #home-sidebar-debate, #home-tools-btn, .home-mate-list-row, .home-sidebar-recent-list button");
|
|
41
|
+
if (!control || control.disabled) return;
|
|
42
|
+
dismiss();
|
|
43
|
+
}, true);
|
|
44
|
+
}
|
|
@@ -19,6 +19,17 @@ export function chooseProjectActivationTarget(projects, candidates) {
|
|
|
19
19
|
return null;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
export function homeProjectCreationState(state) {
|
|
23
|
+
var current = state || {};
|
|
24
|
+
if (current.projectListLoaded !== true) return { ready: false, enabled: false, message: "Loading projects…" };
|
|
25
|
+
if (current.projectsAccessLoaded !== true) return { ready: false, enabled: false, message: "Checking project access…" };
|
|
26
|
+
if (current.permissions && current.permissions.createProject !== true) {
|
|
27
|
+
return { ready: true, enabled: false, message: "Ask an administrator to create or share a project with you." };
|
|
28
|
+
}
|
|
29
|
+
if (current.connected !== true) return { ready: true, enabled: false, message: "Reconnect to start a project." };
|
|
30
|
+
return { ready: true, enabled: true, message: "Create an empty project, then continue in its workspace." };
|
|
31
|
+
}
|
|
32
|
+
|
|
22
33
|
export function isProjectActivationPending(state, slug, socket) {
|
|
23
34
|
var target = projectWsPath(slug);
|
|
24
35
|
return !!target
|
|
@@ -17,6 +17,7 @@ import { getWs } from './ws-ref.js';
|
|
|
17
17
|
import { showToast } from './utils.js';
|
|
18
18
|
import { resolvePreferredVendor } from './default-vendor.js';
|
|
19
19
|
import { sendFolderOp, currentFolderState } from './session-folders.js';
|
|
20
|
+
import { queueSidebarCreationEffect } from './sidebar-creation-effect.js';
|
|
20
21
|
|
|
21
22
|
// Ids are unique without any module-level counter.
|
|
22
23
|
function nextId(prefix) {
|
|
@@ -207,6 +208,7 @@ export function handleNewSessionResult(msg) {
|
|
|
207
208
|
var destination = msg.folderPlaced === false ? "unfiled" : (msg.folderId || form.folderId || "unfiled");
|
|
208
209
|
if (msg.folderPlaced === false) showToast("The session was created but could not be filed in that folder. It is in Unfiled.", "warn");
|
|
209
210
|
store.set({ sessionCreate: null });
|
|
211
|
+
queueSidebarCreationEffect('session', msg.requestId, msg.sessionId, destination);
|
|
210
212
|
revealCreatedSession(destination, msg.sessionId);
|
|
211
213
|
return true;
|
|
212
214
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { store } from './store.js';
|
|
2
|
+
import { getWs } from './ws-ref.js';
|
|
3
|
+
import { showConfirm } from './app-misc.js';
|
|
4
|
+
import { describeGroupDelete } from './session-delete-scope.js';
|
|
5
|
+
import { showToast } from './utils.js';
|
|
6
|
+
|
|
7
|
+
function confirmClear(label, ids, sessions) {
|
|
8
|
+
if (!ids.length) return;
|
|
9
|
+
var requestedIds = ids.slice();
|
|
10
|
+
var originSlug = store.get('currentSlug');
|
|
11
|
+
var originWs = getWs();
|
|
12
|
+
showConfirm(describeGroupDelete(label, requestedIds, sessions).message, function () {
|
|
13
|
+
var ws = getWs();
|
|
14
|
+
var permissions = store.get('permissions');
|
|
15
|
+
var valid = store.get('currentSlug') === originSlug && ws === originWs && ws && ws.readyState === 1 &&
|
|
16
|
+
store.get('connected') && (!permissions || permissions.sessionDelete !== false);
|
|
17
|
+
if (!valid) {
|
|
18
|
+
showToast("Clear cancelled because the project, connection, or delete permission changed.", "warn");
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
ws.send(JSON.stringify({ type: "bulk_delete_sessions", sessionIds: requestedIds }));
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function renderDateGroupHeader(dateLabel, folderLabel, ids, sessions, surface, searching) {
|
|
26
|
+
var header = document.createElement("div");
|
|
27
|
+
header.className = "session-group-header session-date-group-header" + (surface === "mobile" ? " is-mobile" : "");
|
|
28
|
+
header.setAttribute("role", "heading");
|
|
29
|
+
header.setAttribute("aria-level", "3");
|
|
30
|
+
|
|
31
|
+
var label = document.createElement("span");
|
|
32
|
+
label.className = "session-group-header-label";
|
|
33
|
+
label.textContent = dateLabel;
|
|
34
|
+
header.appendChild(label);
|
|
35
|
+
|
|
36
|
+
var count = document.createElement("span");
|
|
37
|
+
count.className = "session-date-group-count";
|
|
38
|
+
count.textContent = String(ids.length);
|
|
39
|
+
count.setAttribute("aria-label", ids.length + (ids.length === 1 ? " session" : " sessions"));
|
|
40
|
+
header.appendChild(count);
|
|
41
|
+
|
|
42
|
+
if ((!store.get('permissions') || store.get('permissions').sessionDelete !== false) && ids.length) {
|
|
43
|
+
var clear = document.createElement("button");
|
|
44
|
+
clear.type = "button";
|
|
45
|
+
clear.className = "session-group-clear-btn";
|
|
46
|
+
clear.textContent = "Clear";
|
|
47
|
+
clear.setAttribute("aria-label", "Clear " + ids.length + " " + (searching ? "matching " : "") + (ids.length === 1 ? "session" : "sessions") + " from " + dateLabel + " in " + folderLabel);
|
|
48
|
+
clear.addEventListener("click", function (event) {
|
|
49
|
+
event.preventDefault();
|
|
50
|
+
event.stopPropagation();
|
|
51
|
+
var scope = dateLabel + " in " + folderLabel + (searching ? " (matching search)" : "");
|
|
52
|
+
confirmClear(scope, ids, sessions);
|
|
53
|
+
});
|
|
54
|
+
header.appendChild(clear);
|
|
55
|
+
}
|
|
56
|
+
return header;
|
|
57
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// Pure date subsection projection for units inside a real folder. Calendar
|
|
2
|
+
// boundaries are local-time Date constructors, so midnight and week/month
|
|
3
|
+
// transitions remain correct across DST changes.
|
|
4
|
+
|
|
5
|
+
var LABELS = ["Today", "Yesterday", "This week", "Earlier this month", "Older"];
|
|
6
|
+
|
|
7
|
+
export function unitTimestamp(unit, sort) {
|
|
8
|
+
var value = sort === "created" ? unit.createdAt : unit.lastActivity;
|
|
9
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
10
|
+
var parsed = value ? new Date(value).getTime() : 0;
|
|
11
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function localDateBoundaries(nowValue) {
|
|
15
|
+
var now = nowValue instanceof Date ? nowValue : new Date(nowValue === undefined ? Date.now() : nowValue);
|
|
16
|
+
var year = now.getFullYear();
|
|
17
|
+
var month = now.getMonth();
|
|
18
|
+
var day = now.getDate();
|
|
19
|
+
var today = new Date(year, month, day).getTime();
|
|
20
|
+
var yesterday = new Date(year, month, day - 1).getTime();
|
|
21
|
+
var mondayOffset = (now.getDay() + 6) % 7;
|
|
22
|
+
var week = new Date(year, month, day - mondayOffset).getTime();
|
|
23
|
+
var monthStart = new Date(year, month, 1).getTime();
|
|
24
|
+
return { today: today, yesterday: yesterday, week: week, month: monthStart };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function dateBucket(timestamp, boundaries) {
|
|
28
|
+
if (timestamp >= boundaries.today) return "Today";
|
|
29
|
+
if (timestamp >= boundaries.yesterday) return "Yesterday";
|
|
30
|
+
if (timestamp >= boundaries.week) return "This week";
|
|
31
|
+
if (timestamp >= boundaries.month) return "Earlier this month";
|
|
32
|
+
return "Older";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function groupUnitsByDate(units, sort, direction, nowValue) {
|
|
36
|
+
if (sort !== "activity" && sort !== "created") return null;
|
|
37
|
+
var source = Array.isArray(units) ? units : [];
|
|
38
|
+
var decorated = source.map(function (unit, index) {
|
|
39
|
+
return { unit: unit, index: index, timestamp: unitTimestamp(unit, sort) };
|
|
40
|
+
});
|
|
41
|
+
var sign = direction === "asc" ? 1 : -1;
|
|
42
|
+
decorated.sort(function (a, b) {
|
|
43
|
+
var delta = (a.timestamp - b.timestamp) * sign;
|
|
44
|
+
return delta || a.index - b.index;
|
|
45
|
+
});
|
|
46
|
+
var boundaries = localDateBoundaries(nowValue);
|
|
47
|
+
var byLabel = {};
|
|
48
|
+
for (var i = 0; i < decorated.length; i++) {
|
|
49
|
+
var label = dateBucket(decorated[i].timestamp, boundaries);
|
|
50
|
+
if (!byLabel[label]) byLabel[label] = [];
|
|
51
|
+
byLabel[label].push(decorated[i].unit);
|
|
52
|
+
}
|
|
53
|
+
var order = direction === "asc" ? LABELS.slice().reverse() : LABELS;
|
|
54
|
+
var groups = [];
|
|
55
|
+
for (var j = 0; j < order.length; j++) {
|
|
56
|
+
if (byLabel[order[j]]) groups.push({ label: order[j], units: byLabel[order[j]] });
|
|
57
|
+
}
|
|
58
|
+
return groups;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function unitRootSessionIds(unit) {
|
|
62
|
+
var item = unit && unit.item;
|
|
63
|
+
if (!item) return [];
|
|
64
|
+
if (item.type === "session" && item.data && typeof item.data.id === "number") return [item.data.id];
|
|
65
|
+
if (item.type === "driver-hierarchy" && item.root && item.root.driver && typeof item.root.driver.id === "number") return [item.root.driver.id];
|
|
66
|
+
if (item.type === "orphan-workers" && Array.isArray(item.workers)) return item.workers.map(function (session) { return session.id; });
|
|
67
|
+
if (item.type === "split-group" && Array.isArray(item.members)) return item.members.map(function (session) { return session.id; });
|
|
68
|
+
if (item.type === "loop" && Array.isArray(item.children)) return item.children.map(function (session) { return session.id; });
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function groupRootSessionIds(units) {
|
|
73
|
+
var result = [];
|
|
74
|
+
var seen = {};
|
|
75
|
+
for (var i = 0; i < units.length; i++) {
|
|
76
|
+
var ids = unitRootSessionIds(units[i]);
|
|
77
|
+
for (var j = 0; j < ids.length; j++) {
|
|
78
|
+
if (typeof ids[j] !== "number" || seen[ids[j]]) continue;
|
|
79
|
+
seen[ids[j]] = true;
|
|
80
|
+
result.push(ids[j]);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
@@ -11,6 +11,7 @@ import { getWs } from './ws-ref.js';
|
|
|
11
11
|
import { escapeHtml } from './utils.js';
|
|
12
12
|
import { currentFolderState } from './session-folders.js';
|
|
13
13
|
import { createModal, button, closeFolderModal } from './session-folder-dialogs.js';
|
|
14
|
+
import { spawnDustParticles, clearDustParticles } from './sidebar.js';
|
|
14
15
|
|
|
15
16
|
// The dialog's elements are found through the modal recorded in the store, so
|
|
16
17
|
// nothing here outlives the dialog: closing it drops the store state and the
|
|
@@ -52,6 +53,27 @@ function phrase(count, noun) {
|
|
|
52
53
|
return count + " " + noun + (count === 1 ? "" : "s");
|
|
53
54
|
}
|
|
54
55
|
|
|
56
|
+
function removalOrigin(folderId) {
|
|
57
|
+
var sections = document.querySelectorAll('.session-folder[data-folder-id]');
|
|
58
|
+
for (var i = 0; i < sections.length; i++) {
|
|
59
|
+
if (sections[i].dataset.folderId !== String(folderId) || sections[i].classList.contains('session-folder-favorites') || sections[i].classList.contains('session-folder-unfiled')) continue;
|
|
60
|
+
var header = sections[i].querySelector('.session-folder-header');
|
|
61
|
+
if (!header || !header.getClientRects().length) continue;
|
|
62
|
+
var icon = header.querySelector('.session-folder-icon') || header;
|
|
63
|
+
var rect = icon.getBoundingClientRect();
|
|
64
|
+
return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2, slug: store.get('currentSlug') };
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function playRemoval(origin) {
|
|
70
|
+
if (!origin || origin.slug !== store.get('currentSlug')) return;
|
|
71
|
+
spawnDustParticles(origin.x, origin.y, {
|
|
72
|
+
count: 12, minDistance: 18, distanceRange: 34, upwardBias: 10,
|
|
73
|
+
minDuration: 480, durationRange: 340, containerClass: "folder-delete-dust", maxContainers: 3,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
55
77
|
// Other folders a move can target: every custom folder except the source, and
|
|
56
78
|
// Unfiled is the first choice, not a destination here, and Favorites is a tag,
|
|
57
79
|
// never a destination.
|
|
@@ -136,7 +158,7 @@ function submit() {
|
|
|
136
158
|
var message = { type: "session_folders_delete", folderId: current.folderId, mode: current.preview.sessionCount === 0 ? "unfiled" : current.mode, token: current.preview.token, requestId: requestId };
|
|
137
159
|
if (message.mode === "move") message.destinationId = current.destinationId;
|
|
138
160
|
if (message.mode === "delete") message.confirmDelete = true;
|
|
139
|
-
patch({ phase: "pending", requestId: requestId, error: "" });
|
|
161
|
+
patch({ phase: "pending", requestId: requestId, error: "", removalOrigin: removalOrigin(current.folderId) });
|
|
140
162
|
if (!send(message)) patch({ phase: "ready", requestId: current.requestId, error: "Not connected. Try again." });
|
|
141
163
|
}
|
|
142
164
|
|
|
@@ -295,7 +317,10 @@ export function handleFolderDeleteState(msg) {
|
|
|
295
317
|
patch({ phase: current.preview ? "ready" : "error", error: msg.error, confirmDelete: false });
|
|
296
318
|
return true;
|
|
297
319
|
}
|
|
298
|
-
if (msg.folderDeleted === current.folderId)
|
|
320
|
+
if (msg.folderDeleted === current.folderId) {
|
|
321
|
+
playRemoval(current.removalOrigin);
|
|
322
|
+
closeFolderModal();
|
|
323
|
+
}
|
|
299
324
|
return true;
|
|
300
325
|
}
|
|
301
326
|
|
|
@@ -304,6 +329,7 @@ store.subscribe(function (next, previous) {
|
|
|
304
329
|
// Retry depends on the connection, so a change in it repaints the dialog.
|
|
305
330
|
else if (next.connected !== previous.connected && next.sessionFolderDelete) paint();
|
|
306
331
|
var current = next.sessionFolderDelete;
|
|
332
|
+
if (next.currentSlug !== previous.currentSlug || next.dmMode !== previous.dmMode) clearDustParticles("folder-delete-dust");
|
|
307
333
|
if (!current || !previous.connected || next.connected) return;
|
|
308
334
|
if (current.phase === "pending") {
|
|
309
335
|
patch({ phase: "ready", error: "Connection lost. Check the folder before trying again." });
|
|
@@ -8,6 +8,7 @@ import { store } from './store.js';
|
|
|
8
8
|
import { getWs } from './ws-ref.js';
|
|
9
9
|
import { showToast } from './utils.js';
|
|
10
10
|
import { defaultFolderState } from './session-folder-layout.js';
|
|
11
|
+
import { queueSidebarCreationEffect } from './sidebar-creation-effect.js';
|
|
11
12
|
|
|
12
13
|
export function handleSessionFoldersState(msg) {
|
|
13
14
|
var slug = store.get('currentSlug');
|
|
@@ -26,6 +27,7 @@ export function handleSessionFoldersState(msg) {
|
|
|
26
27
|
return;
|
|
27
28
|
}
|
|
28
29
|
store.set({ sessionFolderCreate: null });
|
|
30
|
+
if (msg.folderId) queueSidebarCreationEffect('folder', msg.requestId, msg.folderId, null);
|
|
29
31
|
if (create.sessionId !== null && msg.folderId) sendFolderOp({ op: "place_session", sessionId: create.sessionId, folderId: msg.folderId });
|
|
30
32
|
}
|
|
31
33
|
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// A small acknowledgement-only title shimmer for locally created sidebar
|
|
2
|
+
// folders and sessions. The queue lives in the store so list rendering and the
|
|
3
|
+
// acknowledgement may arrive in either order without replaying the effect.
|
|
4
|
+
|
|
5
|
+
import { store } from './store.js';
|
|
6
|
+
|
|
7
|
+
var MAX_EFFECTS = 4;
|
|
8
|
+
var MAX_ACTIVE_SHIMMERS = 4;
|
|
9
|
+
var MAX_ATTEMPTS = 40;
|
|
10
|
+
var RETRY_MS = 50;
|
|
11
|
+
var SHIMMER_MS = 5700;
|
|
12
|
+
|
|
13
|
+
function prefersReducedMotion() {
|
|
14
|
+
return window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function effectKey(effect) {
|
|
18
|
+
return effect.kind + ':' + effect.requestId;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function visibleElement(nodes) {
|
|
22
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
23
|
+
if (nodes[i].getClientRects().length > 0) return nodes[i];
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function folderTarget(effect) {
|
|
29
|
+
var sections = document.querySelectorAll('.session-folder[data-folder-id]');
|
|
30
|
+
var matches = [];
|
|
31
|
+
for (var i = 0; i < sections.length; i++) {
|
|
32
|
+
if (sections[i].dataset.folderId === String(effect.targetId) && !sections[i].classList.contains('session-folder-favorites')) matches.push(sections[i]);
|
|
33
|
+
}
|
|
34
|
+
var section = visibleElement(matches);
|
|
35
|
+
return section ? section.querySelector('.session-folder-label') : null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function sessionTarget(effect) {
|
|
39
|
+
var sections = document.querySelectorAll('.session-folder[data-folder-id]');
|
|
40
|
+
var matches = [];
|
|
41
|
+
for (var i = 0; i < sections.length; i++) {
|
|
42
|
+
if (sections[i].dataset.folderId !== String(effect.folderId) || sections[i].classList.contains('session-folder-favorites')) continue;
|
|
43
|
+
var units = sections[i].querySelectorAll('.session-folder-unit[data-unit-key]');
|
|
44
|
+
for (var j = 0; j < units.length; j++) {
|
|
45
|
+
if (units[j].dataset.unitKey !== String(effect.targetId)) continue;
|
|
46
|
+
var title = units[j].querySelector('.session-item-title, .mobile-session-title');
|
|
47
|
+
if (title) matches.push(title);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return visibleElement(matches);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function findTarget(effect) {
|
|
54
|
+
return effect.kind === 'folder' ? folderTarget(effect) : sessionTarget(effect);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function removeEffectDom() {
|
|
58
|
+
var targets = document.querySelectorAll('.sidebar-creation-shimmer');
|
|
59
|
+
for (var i = 0; i < targets.length; i++) {
|
|
60
|
+
targets[i].classList.remove('sidebar-creation-shimmer');
|
|
61
|
+
delete targets[i].dataset.creationEffect;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function playEffect(effect, target) {
|
|
66
|
+
if (!target.isConnected || prefersReducedMotion()) return;
|
|
67
|
+
var active = document.querySelectorAll('.sidebar-creation-shimmer');
|
|
68
|
+
for (var i = 0; i <= active.length - MAX_ACTIVE_SHIMMERS; i++) {
|
|
69
|
+
active[i].classList.remove('sidebar-creation-shimmer');
|
|
70
|
+
delete active[i].dataset.creationEffect;
|
|
71
|
+
}
|
|
72
|
+
target.classList.remove('sidebar-creation-shimmer');
|
|
73
|
+
void target.offsetWidth;
|
|
74
|
+
target.dataset.creationEffect = effectKey(effect);
|
|
75
|
+
target.classList.add('sidebar-creation-shimmer');
|
|
76
|
+
setTimeout(function () {
|
|
77
|
+
if (target.dataset.creationEffect !== effectKey(effect)) return;
|
|
78
|
+
target.classList.remove('sidebar-creation-shimmer');
|
|
79
|
+
delete target.dataset.creationEffect;
|
|
80
|
+
}, SHIMMER_MS);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function clearSidebarCreationEffects() {
|
|
84
|
+
if ((store.get('sidebarCreationEffects') || []).length) store.set({ sidebarCreationEffects: [] });
|
|
85
|
+
removeEffectDom();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function flushSidebarCreationEffects() {
|
|
89
|
+
var queue = store.get('sidebarCreationEffects') || [];
|
|
90
|
+
if (!queue.length) return;
|
|
91
|
+
if (prefersReducedMotion()) {
|
|
92
|
+
clearSidebarCreationEffects();
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
var slug = store.get('currentSlug');
|
|
96
|
+
var pending = [];
|
|
97
|
+
var ready = [];
|
|
98
|
+
for (var i = 0; i < queue.length; i++) {
|
|
99
|
+
var effect = queue[i];
|
|
100
|
+
if (effect.slug !== slug) continue;
|
|
101
|
+
var target = findTarget(effect);
|
|
102
|
+
if (target) {
|
|
103
|
+
ready.push({ effect: effect, target: target });
|
|
104
|
+
} else if (effect.attempts < MAX_ATTEMPTS) {
|
|
105
|
+
pending.push(Object.assign({}, effect, { attempts: effect.attempts + 1 }));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// Consume before touching the DOM. A render or duplicate acknowledgement can
|
|
109
|
+
// therefore never play an already resolved effect a second time.
|
|
110
|
+
store.set({ sidebarCreationEffects: pending });
|
|
111
|
+
for (var j = 0; j < ready.length; j++) playEffect(ready[j].effect, ready[j].target);
|
|
112
|
+
if (pending.length) setTimeout(flushSidebarCreationEffects, RETRY_MS);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function queueSidebarCreationEffect(kind, requestId, targetId, folderId) {
|
|
116
|
+
if (!requestId || targetId === undefined || targetId === null || prefersReducedMotion()) return false;
|
|
117
|
+
var queue = (store.get('sidebarCreationEffects') || []).slice();
|
|
118
|
+
var next = { kind: kind, requestId: requestId, targetId: targetId, folderId: folderId || null, slug: store.get('currentSlug'), attempts: 0 };
|
|
119
|
+
var key = effectKey(next);
|
|
120
|
+
for (var i = 0; i < queue.length; i++) if (effectKey(queue[i]) === key) return false;
|
|
121
|
+
queue.push(next);
|
|
122
|
+
if (queue.length > MAX_EFFECTS) queue = queue.slice(queue.length - MAX_EFFECTS);
|
|
123
|
+
store.set({ sidebarCreationEffects: queue });
|
|
124
|
+
setTimeout(flushSidebarCreationEffects, 0);
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
store.subscribe(function (state, previous) {
|
|
129
|
+
if (state.currentSlug !== previous.currentSlug || state.dmMode !== previous.dmMode || (previous.connected && !state.connected)) clearSidebarCreationEffects();
|
|
130
|
+
});
|
|
@@ -744,6 +744,7 @@ export function renderMobileSessionsInto(container) {
|
|
|
744
744
|
var folderCtx = {
|
|
745
745
|
surface: "mobile",
|
|
746
746
|
searching: matchIds !== null,
|
|
747
|
+
sessions: getCachedSessions(),
|
|
747
748
|
renderItem: function (it) {
|
|
748
749
|
if (it.type === "loop") return createMobileLoopGroup(it.loopId, it.children, it.groupKey);
|
|
749
750
|
if (it.type === "driver-hierarchy") return renderMobileDriverHierarchy(it.root, createMobileSessionItem, refreshMobileChatSheet);
|
|
@@ -20,6 +20,8 @@ import { openFolderDelete } from './session-folder-delete.js';
|
|
|
20
20
|
import { openSessionCreate } from './session-create.js';
|
|
21
21
|
import { renderNewSessionButton, renderCreateRow } from './session-create-form.js';
|
|
22
22
|
import { restoreFolderCollapse, toggleFolderCollapse } from './session-folder-collapse.js';
|
|
23
|
+
import { groupUnitsByDate, groupRootSessionIds } from './session-date-groups.js';
|
|
24
|
+
import { renderDateGroupHeader } from './session-date-group-header.js';
|
|
23
25
|
|
|
24
26
|
// Mutable UI state (open menu, drag in flight, the layout each surface last
|
|
25
27
|
// rendered) lives in the store; this module only owns the functions.
|
|
@@ -65,7 +67,7 @@ export function unitsFromItems(items) {
|
|
|
65
67
|
key: data && typeof data.id === "number" ? data.id : null,
|
|
66
68
|
title: data ? data.title : (item.type === "split-group" ? (item.group && item.group.name) : (item.type === "loop" ? item.loopId : "")),
|
|
67
69
|
createdAt: data ? data.createdAt || 0 : 0,
|
|
68
|
-
lastActivity: item.lastActivity || 0,
|
|
70
|
+
lastActivity: data ? (data.lastActivity || 0) : (item.lastActivity || 0),
|
|
69
71
|
item: item,
|
|
70
72
|
});
|
|
71
73
|
}
|
|
@@ -355,7 +357,17 @@ function renderSection(section, ctx, kind) {
|
|
|
355
357
|
: (kind === "folder" ? "Empty folder" : "Nothing unfiled");
|
|
356
358
|
body.appendChild(empty);
|
|
357
359
|
}
|
|
358
|
-
|
|
360
|
+
var view = currentFolderState().view;
|
|
361
|
+
var dateGroups = isFavorites ? null : groupUnitsByDate(section.units, view.sort, view.direction);
|
|
362
|
+
if (dateGroups) {
|
|
363
|
+
for (var g = 0; g < dateGroups.length; g++) {
|
|
364
|
+
var ids = groupRootSessionIds(dateGroups[g].units);
|
|
365
|
+
body.appendChild(renderDateGroupHeader(dateGroups[g].label, section.label, ids, ctx.sessions || [], surface, !!ctx.searching));
|
|
366
|
+
for (var gu = 0; gu < dateGroups[g].units.length; gu++) body.appendChild(renderUnit(dateGroups[g].units[gu], section.id, ctx));
|
|
367
|
+
}
|
|
368
|
+
} else {
|
|
369
|
+
for (var i = 0; i < section.units.length; i++) body.appendChild(renderUnit(section.units[i], section.id, ctx));
|
|
370
|
+
}
|
|
359
371
|
root.appendChild(body);
|
|
360
372
|
restoreFolderCollapse(root, body, toggle, surface, section.id, section.collapsed, !!ctx.searching);
|
|
361
373
|
if (surface === "desktop") wireContainerDrop(root, section.id);
|
|
@@ -879,7 +879,7 @@ function renderSessionItem(s, options) {
|
|
|
879
879
|
if (store.get('isMultiUserMode') && s.sessionVisibility === "shared") {
|
|
880
880
|
textHtml += '<span class="session-shared-icon" title="Shared session" aria-label="Shared session">' + iconHtml("users") + '</span>';
|
|
881
881
|
}
|
|
882
|
-
textHtml += highlightMatch(s.title || "New Session", getListSearchQuery());
|
|
882
|
+
textHtml += '<span class="session-item-title">' + highlightMatch(s.title || "New Session", getListSearchQuery()) + '</span>';
|
|
883
883
|
textSpan.innerHTML = textHtml;
|
|
884
884
|
el.appendChild(textSpan);
|
|
885
885
|
appendGithubWork(el, s, textSpan);
|
|
@@ -1118,6 +1118,7 @@ export function renderSessionList(sessions) {
|
|
|
1118
1118
|
var folderCtx = {
|
|
1119
1119
|
surface: "desktop",
|
|
1120
1120
|
searching: matchIds !== null,
|
|
1121
|
+
sessions: cachedSessions,
|
|
1121
1122
|
renderItem: function (it) {
|
|
1122
1123
|
if (it.type === "loop") return renderLoopGroup(it.loopId, it.children, it.groupKey) || document.createElement("div");
|
|
1123
1124
|
if (it.type === "split-group") return renderSplitGroupItem(it.group, it.members);
|
|
@@ -460,10 +460,24 @@ export function initSidebar(_ctx) {
|
|
|
460
460
|
|
|
461
461
|
}
|
|
462
462
|
|
|
463
|
-
export function
|
|
463
|
+
export function clearDustParticles(containerClass) {
|
|
464
|
+
var selector = ".dust-particle-container" + (containerClass ? "." + containerClass : "");
|
|
465
|
+
var containers = document.querySelectorAll(selector);
|
|
466
|
+
for (var i = 0; i < containers.length; i++) containers[i].remove();
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
export function spawnDustParticles(cx, cy, options) {
|
|
470
|
+
var opts = options || {};
|
|
471
|
+
if (window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches) return null;
|
|
464
472
|
var colors = ["#8B7355", "#A0522D", "#D2B48C", "#C4A882", "#9E9E9E", "#B8860B", "#BC8F8F"];
|
|
465
|
-
var count = 24;
|
|
473
|
+
var count = Math.max(1, Math.min(opts.count || 24, 24));
|
|
474
|
+
var containerClass = opts.containerClass || "";
|
|
475
|
+
var maxContainers = Math.max(1, Math.min(opts.maxContainers || 6, 6));
|
|
476
|
+
var active = document.querySelectorAll(".dust-particle-container" + (containerClass ? "." + containerClass : ""));
|
|
477
|
+
for (var ai = 0; ai <= active.length - maxContainers; ai++) active[ai].remove();
|
|
466
478
|
var container = document.createElement("div");
|
|
479
|
+
container.className = "dust-particle-container" + (containerClass ? " " + containerClass : "");
|
|
480
|
+
container.setAttribute("aria-hidden", "true");
|
|
467
481
|
container.style.position = "fixed";
|
|
468
482
|
container.style.top = "0";
|
|
469
483
|
container.style.left = "0";
|
|
@@ -478,10 +492,10 @@ export function spawnDustParticles(cx, cy) {
|
|
|
478
492
|
dot.className = "dust-particle";
|
|
479
493
|
var size = 3 + Math.random() * 5;
|
|
480
494
|
var angle = Math.random() * Math.PI * 2;
|
|
481
|
-
var dist = 30 + Math.random() * 60;
|
|
495
|
+
var dist = (opts.minDistance || 30) + Math.random() * (opts.distanceRange || 60);
|
|
482
496
|
var dx = Math.cos(angle) * dist;
|
|
483
|
-
var dy = Math.sin(angle) * dist - 20
|
|
484
|
-
var duration = 600 + Math.random() * 500;
|
|
497
|
+
var dy = Math.sin(angle) * dist - (opts.upwardBias === undefined ? 20 : opts.upwardBias);
|
|
498
|
+
var duration = (opts.minDuration || 600) + Math.random() * (opts.durationRange || 500);
|
|
485
499
|
|
|
486
500
|
dot.style.width = size + "px";
|
|
487
501
|
dot.style.height = size + "px";
|
|
@@ -496,4 +510,5 @@ export function spawnDustParticles(cx, cy) {
|
|
|
496
510
|
}
|
|
497
511
|
|
|
498
512
|
setTimeout(function () { container.remove(); }, 1200);
|
|
513
|
+
return container;
|
|
499
514
|
}
|
package/package.json
CHANGED