PageSourceSearch

https://roxima.io/blocks/projects/script.js?v=49

js roxima.io collected 2026-10-02 01:58:02 UTC 261,757 bytes, 5,478 lines download raw bytes

1/**
2 * Objectifs Block (Mini-Notion)
3 * Remplaces original "Projects" block
4 */
5
6import { objectifsStore } from '../../shared/objectifs_store.js';
7import { pomodoroStore } from '../../shared/store_pomodoro.js';
8import { i18n } from '../../shared/i18n.js';
9import { supabase } from '../../shared/supabase.js?v=4';
10import { NotionRichText } from '../../shared/notion_rich_text.js';
11import { assertCompleteNotionSnapshot } from '../../shared/notion_sync_guard.mjs';
12import { collectStaleNotionPageIds } from '../../shared/notion_page_cleanup.mjs';
13import {
14    authenticatedNotionRequest,
15    disconnectNotionConnection,
16    loadNotionConnections,
17    sanitizeLegacyNotionStorage,
18    startNotionOAuth
19} from '../../shared/notion_oauth.mjs';
20import {
21    getTodayPriorities,
22    addTaskToToday,
23    removeTaskFromToday,
24    reorderTodayPriorities,
25    setTaskGeneralPriority,
26    updateTaskEstimatedMinutes,
27    resolveTaskReference
28} from '../../shared/daily_priorities.js';
29import { findPlannedTaskEvent } from '../../shared/task_calendar_planning.mjs';
30import { getProjectActivationStep, PROJECT_ACTIVATION_STEP } from '../../shared/project_activation.mjs';
31import {
32    clearActivationDraft,
33    consumeActivationDraft,
34    readActivationDraft,
35    readActivationDraftConsumption
36} from '../../shared/activation_draft.mjs?v=2';
37import {
38    PLANNING_SESSION_DURATION_MS,
39    getPlanningSessionRemainingMs,
40    getPlanningSessionStorageKey,
41    sanitizePlanningSession,
42    startOrResumePlanningSession
43} from '../../shared/planning_session.mjs?v=1';
44import {
45    clientPointToSelectionSpace,
46    clientRectToSelectionSpace,
47    copySelectedBlocks,
48    createSelectionRect,
49    createProjectDragItems,
50    formatSelectedBlocksAsText,
51    getRangeSelection,
52    getSelectionWithDescendants,
53    rectanglesIntersect
54} from '../../shared/project_multi_selection.mjs';
55import { openAiPlanner } from './ai_planner.js';
56
57let selectedPageId = localStorage.getItem('roxima_selected_project_page_id') || null;
58let projectBlockClipboard = [];
59
60function setSelectedPageId(id) {
61    selectedPageId = id;
62    if (id) {
63        localStorage.setItem('roxima_selected_project_page_id', id);
64    } else {
65        localStorage.removeItem('roxima_selected_project_page_id');
66    }
67}
68
69const selectedPageIds = new Set();
70const expandedPages = new Set();
71
72// Slash Menu State
73let slashMenuOpen = false;
74let slashMenuIndex = 0;
75let slashMenuBlockId = null; // Block ID where / was typed
76let isPlusMenuMode = false; // New flag to distinguish context
77
78const SLASH_OPTIONS = [
79    { label: 'Texte', labelKey: 'projects_block_text', type: 'p', icon: '¶' },
80    { label: 'Titre 1', labelKey: 'projects_block_h1', type: 'h1', icon: 'H1' },
81    { label: 'Titre 2', labelKey: 'projects_block_h2', type: 'h2', icon: 'H2' },
82    { label: 'Action', labelKey: 'projects_block_todo', type: 'todo', icon: '☑' },
83    { label: 'Liste', labelKey: 'projects_block_list', type: 'bulleted_list_item', icon: '•' },
84    { label: 'Liste numérotée', labelKey: 'projects_block_numbered_list', type: 'numbered_list_item', icon: '1.' },
85    { label: 'Séparateur', labelKey: 'projects_block_divider', type: 'divider', icon: '—' },
86    { label: 'Page liée', labelKey: 'projects_block_link', type: 'page_link', icon: '↗' },
87];
88
89const optionLabel = (opt) => opt.labelKey ? i18n.t(opt.labelKey) : opt.label;
90
91function escapeHtml(value = '') {
92    const span = document.createElement('span');
93    span.textContent = String(value);
94    return span.innerHTML;
95}
96
97function escapeAttribute(value = '') {
98    return String(value ?? '')
99        .replace(/&/g, '&')
100        .replace(/"/g, '"')
101        .replace(/'/g, ''')
102        .replace(/</g, '&lt;')
103        .replace(/>/g, '&gt;');
104}
105
106function inlineEventString(value = '') {
107    return escapeAttribute(JSON.stringify(String(value)));
108}
109
110function renderProjectCreationActivation(draft = {}) {
111    const examples = [1, 2, 3].map((index) => ({
112        index,
113        title: i18n.t(`project_activation_example_${index}_project`),
114        label: i18n.t(`project_activation_example_${index}_label`)
115    }));
116    const draftTitle = String(draft.title || '');
117    const helpExpanded = draft.helpExpanded === true;
118
119    return `
120        <section class="project-activation-card project-activation-card--create" aria-labelledby="project-activation-create-title">
121            <div class="project-activation-kicker" data-i18n="project_activation_create_step">${i18n.t('project_activation_create_step')}</div>
122            <h3 id="project-activation-create-title" data-i18n="project_activation_create_title">${i18n.t('project_activation_create_title')}</h3>
123            <p data-i18n="project_activation_create_copy">${i18n.t('project_activation_create_copy')}</p>
124            <form class="project-activation-form" id="project-activation-form">
125                <label class="projects-visually-hidden" for="project-activation-title" data-i18n="project_activation_start_label">${i18n.t('project_activation_start_label')}</label>
126                <input id="project-activation-title" name="project-title" type="text" maxlength="120"
127                       value="${escapeAttribute(draftTitle)}"
128                       placeholder="${escapeAttribute(i18n.t('project_activation_create_placeholder'))}"
129                       data-i18n-placeholder="project_activation_create_placeholder" autocomplete="off" required>
130                <button class="project-activation-primary" type="submit" data-i18n="project_activation_create_cta">${i18n.t('project_activation_create_cta')}</button>
131            </form>
132            <small data-i18n="project_activation_create_hint">${i18n.t('project_activation_create_hint')}</small>
133            <button class="project-activation-help-toggle" id="project-activation-help-toggle" type="button"
134                    aria-expanded="${helpExpanded}" aria-controls="project-activation-help">
135                <span data-i18n="project_activation_help_toggle">${i18n.t('project_activation_help_toggle')}</span>
136                <span class="project-activation-help-chevron" aria-hidden="true">⌄</span>
137            </button>
138            <div class="project-activation-help" id="project-activation-help"${helpExpanded ? '' : ' hidden'}>
139                <p data-i18n="project_activation_help_intro">${i18n.t('project_activation_help_intro')}</p>
140                <div class="project-activation-examples">
141                    ${examples.map((example) => `
142                        <button class="project-activation-example${draft.selectedExampleIndex === example.index ? ' is-selected' : ''}" type="button"
143                                data-project-example-index="${example.index}"
144                                data-project-example-title="${escapeAttribute(example.title)}"
145                                aria-pressed="${draft.selectedExampleIndex === example.index}">
146                            ${escapeHtml(example.label)}
147                        </button>
148                    `).join('')}
149                </div>
150            </div>
151        </section>
152    `;
153}
154
155function renderFirstTaskActivation() {
156    return `
157        <section class="project-activation-card project-activation-card--task" aria-labelledby="project-activation-task-title">
158            <div class="project-activation-kicker" data-i18n="project_activation_task_step">${i18n.t('project_activation_task_step')}</div>
159            <h3 id="project-activation-task-title" data-i18n="project_activation_task_title">${i18n.t('project_activation_task_title')}</h3>
160            <p data-i18n="project_activation_task_copy">${i18n.t('project_activation_task_copy')}</p>
161            <button class="project-activation-primary" id="btn-add-first-task" type="button" data-i18n="project_activation_task_cta">${i18n.t('project_activation_task_cta')}</button>
162            <small data-i18n="project_activation_task_hint">${i18n.t('project_activation_task_hint')}</small>
163        </section>
164    `;
165}
166
167// Sidebar State (Local UI state)
168let sidebarForcedState = null; // 'open', 'closed', or null (default)
169let formattingToolbar = null;
170let currentColorDropdown = null;
171
172export function mount(container, ctx) {
173    const projectActivationDraft = {
174        title: '',
175        helpExpanded: false,
176        selectedExampleIndex: null
177    };
178    let firstFocusActivationTaskId = null;
179    let activationDraftConsumptionPending = false;
180    let ignoredSelectionClick = null;
181    let notionOAuthStartPending = false;
182    let selectionPageId = selectedPageId;
183    const editorsCommittedBeforeFocusout = new WeakSet();
184    let planningOwnerId = String(ctx.currentUser?.id || 'guest');
185    let planningSession = loadPlanningSession(planningOwnerId);
186    let planningTimerId = null;
187
188    // Attach capturing click listener to container to intercept click event after drag-selection
189    container.addEventListener('click', (e) => {
190        if (!ignoredSelectionClick) return;
191
192        const clickToIgnore = ignoredSelectionClick;
193        ignoredSelectionClick = null;
194        const isSyntheticSelectionClick = performance.now() <= clickToIgnore.expiresAt
195            && Math.hypot(e.clientX - clickToIgnore.clientX, e.clientY - clickToIgnore.clientY) <= 6;
196        if (isSyntheticSelectionClick) {
197            e.preventDefault();
198            e.stopImmediatePropagation();
199        }
200    }, true); // useCapture = true!
201
202    // Inject Styles
203    if (!document.querySelector('link[href*="blocks/projects/styles.css"]')) {
204        const link = document.createElement('link');
205        link.rel = 'stylesheet';
206        link.href = '../blocks/projects/styles.css?v=22';
207        document.head.appendChild(link);
208    }
209    
210    monitorSelection();
211
212    // Auth Helper
213    const store = objectifsStore;
214    const checkAuth = () => {
215        // Disabled auth block for onboarding and local usage
216        return true;
217    };
218    const isTouchLayout = () => window.matchMedia('(max-width: 768px), (pointer: coarse)').matches;
219    const cleanTaskText = (block) => (block?.text || '').trim() || i18n.t('common_new_task');
220
221    sanitizeLegacyNotionStorage(localStorage);
222
223    function getNotionWorkspaces() {
224        try {
225            return JSON.parse(localStorage.getItem('roxima_notion_workspaces') || '[]');
226        } catch (e) {
227            return [];
228        }
229    }
230
231    function getActiveNotionWorkspaceId() {
232        return localStorage.getItem('roxima_notion_active_workspace_id');
233    }
234
235    function getActiveNotionWorkspace() {
236        const list = getNotionWorkspaces();
237        const activeId = getActiveNotionWorkspaceId();
238        return list.find(w => w.id === activeId) || list[0] || null;
239    }
240
241    function isDemoNotionWorkspace(workspace) {
242        return workspace?.isDemo === true || workspace?.id === 'demo-workspace-id';
243    }
244
245    async function refreshNotionConnections() {
246        if (!ctx.currentUser) return getNotionWorkspaces();
247
248        const previous = getNotionWorkspaces();
249        const connections = await loadNotionConnections({
250            supabaseClient: supabase,
251            storage: localStorage
252        });
253
254        // Les anciennes versions utilisaient l'identifiant de l'espace Notion
255        // comme identifiant local. Le serveur utilise désormais un connectionId.
256        // Réassocier les pages locales évite de les masquer après la migration.
257        const connectionByWorkspaceId = new Map(
258            connections.map(connection => [connection.workspaceId, connection])
259        );
260        const idMigration = new Map();
261        previous.forEach(workspace => {
262            const replacement = connectionByWorkspaceId.get(workspace.workspaceId || workspace.id);
263            if (replacement && replacement.id !== workspace.id) {
264                idMigration.set(workspace.id, replacement.id);
265            }
266        });
267
268        if (idMigration.size > 0) {
269            store.getPages().forEach(page => {
270                const nextId = idMigration.get(page.notionWorkspaceId);
271                if (nextId) page.notionWorkspaceId = nextId;
272            });
273            store.save();
274        }
275
276        return connections;
277    }
278
279    function notionRequest(path, connectionId, body = {}, method = 'POST') {
280        return authenticatedNotionRequest(path, {
281            supabaseClient: supabase,
282            method,
283            body: {
284                ...body,
285                connectionId
286            }
287        });
288    }
289
290    function setupNotionSyncInterval(ws) {
291        if (window.__notionSyncInterval) clearInterval(window.__notionSyncInterval);
292        if (!ws) return;
293        window.__notionSyncInterval = setInterval(() => {
294            if (!document.hidden && document.body.contains(container)) {
295                const currentWs = getActiveNotionWorkspace();
296                if (currentWs) {
297                    syncNotion(currentWs);
298                }
299            }
300        }, 30000);
301    }
302
303    function setActiveNotionWorkspace(workspaceId) {
304        localStorage.setItem('roxima_notion_active_workspace_id', workspaceId);
305        const active = getActiveNotionWorkspace();
306        if (active) {
307            if (!isDemoNotionWorkspace(active)) {
308                syncNotion(active);
309                setupNotionSyncInterval(active);
310            }
311        } else {
312            if (window.__notionSyncInterval) {
313                clearInterval(window.__notionSyncInterval);
314                window.__notionSyncInterval = null;
315            }
316        }
317    }
318
319    async function removeNotionWorkspace(workspaceId) {
320        const workspace = getNotionWorkspaces().find(item => item.id === workspaceId);
321        if (!workspace) return;
322
323        if (!isDemoNotionWorkspace(workspace)) {
324            await disconnectNotionConnection(workspaceId, { supabaseClient: supabase });
325        }
326
327        // Delete all Notion pages associated with this workspace
328        const store = objectifsStore;
329        const pagesToDelete = store.getPages().filter(p => (p.isNotion || p.source === 'notion') && p.notionWorkspaceId === workspaceId);
330        pagesToDelete.forEach(p => store.deletePage(p.id));
331
332        let list = getNotionWorkspaces();
333        list = list.filter(w => w.id !== workspaceId);
334        localStorage.setItem('roxima_notion_workspaces', JSON.stringify(list));
335        
336        const activeId = getActiveNotionWorkspaceId();
337        if (activeId === workspaceId) {
338            if (list.length > 0) {
339                setActiveNotionWorkspace(list[0].id);
340            } else {
341                localStorage.removeItem('roxima_notion_active_workspace_id');
342            }
343        }
344    }
345
346    async function syncBlockToNotion(pageId, blockId, overrides = null) {
347        const store = objectifsStore;
348        const page = store.getPage(pageId);
349        if (!page) return false;
350        if (!page.isNotion && page.source !== 'notion') return true;
351
352        // A daily priority can come from an inactive Notion workspace.
353        const notionWorkspace = page.notionWorkspaceId
354            ? getNotionWorkspaces().find(workspace => workspace.id === page.notionWorkspaceId)
355            : getActiveNotionWorkspace();
356        if (!notionWorkspace) return false;
357        if (isDemoNotionWorkspace(notionWorkspace)) return true;
358
359        const storedBlock = store.getBlocks(pageId).find(b => b.id === blockId);
360        if (!storedBlock) return false;
361        const block = overrides ? { ...storedBlock, ...overrides } : storedBlock;
362
363        // If block has no notionBlockId yet, append it
364        if (!block.notionBlockId) {
365            try {
366                const parentId = page.notionPageId;
367                if (!parentId) return false;
368
369                const blocks = store.getBlocks(pageId) || [];
370                const index = blocks.findIndex(b => b.id === blockId);
371                const prevBlock = index > 0 ? blocks[index - 1] : null;
372                const afterBlockId = prevBlock && prevBlock.notionBlockId ? prevBlock.notionBlockId : null;
373
374                const data = await notionRequest('/api/notion/append-block', notionWorkspace.id, {
375                    parentId: parentId,
376                    type: block.type,
377                    text: block.text,
378                    rich_text: block.notionData ? block.notionData.rich_text : null,
379                    checked: block.checked,
380                    afterBlockId: afterBlockId
381                });
382                
383                if (data.newBlockId) {
384                    storedBlock.notionBlockId = data.newBlockId;
385                    storedBlock.isNotion = true;
386                    store.save();
387                }
388                return true;
389            } catch (error) {
390                console.error("Erreur syncBlockToNotion (append):", error);
391                return false;
392            }
393        }
394
395        try {
396            const blocks = store.getBlocks(pageId) || [];
397            const index = blocks.findIndex(b => b.id === blockId);
398            const prevBlock = index > 0 ? blocks[index - 1] : null;
399            const afterBlockId = prevBlock && prevBlock.notionBlockId ? prevBlock.notionBlockId : null;
400
401            const data = await notionRequest('/api/notion/update-block', notionWorkspace.id, {
402                blockId: block.notionBlockId,
403                type: block.type,
404                text: block.text,
405                rich_text: block.notionData ? block.notionData.rich_text : null,
406                checked: block.checked,
407                parentId: page.notionPageId,
408                afterBlockId: afterBlockId
409            });
410            
411            if (data.recreated && data.newBlockId) {
412                storedBlock.notionBlockId = data.newBlockId;
413                store.save();
414                console.log(`✅ Recreated block in Notion due to type change: ${data.newBlockId}`);
415            }
416            return true;
417        } catch (error) {
418            console.error("Erreur syncBlockToNotion (update):", error);
419            ctx.bus?.emit('ui:toast', 'Erreur sauvegarde Notion');
420            return false;
421        }
422    }
423
424    const GENERAL_PRIORITY_META = {
425        p1: { code: 'P1', labelKey: 'daily_priority_level_p1' },
426        p2: { code: 'P2', labelKey: 'daily_priority_level_p2' },
427        p3: { code: 'P3', labelKey: 'daily_priority_level_p3' },
428        none: { code: '—', labelKey: 'daily_priority_level_none' }
429    };
430
431    function translateProject(key, variables = {}) {
432        return Object.entries(variables).reduce((text, [name, value]) => (
433            text.replaceAll(`{${name}}`, String(value))
434        ), i18n.t(key));
435    }
436
437    function loadPlanningSession(ownerId) {
438        const storageKey = getPlanningSessionStorageKey(ownerId);
439        try {
440            const parsed = JSON.parse(localStorage.getItem(storageKey) || 'null');
441            const normalized = sanitizePlanningSession(parsed, { ownerId });
442            if (!normalized && parsed) localStorage.removeItem(storageKey);
443            return normalized;
444        } catch {
445            localStorage.removeItem(storageKey);
446            return null;
447        }
448    }
449
450    function savePlanningSession() {
451        const storageKey = getPlanningSessionStorageKey(planningOwnerId);
452        if (!planningSession) {
453            localStorage.removeItem(storageKey);
454            return;
455        }
456        localStorage.setItem(storageKey, JSON.stringify(planningSession));
457    }
458
459    function formatPlanningTime(remainingMs) {
460        const totalSeconds = Math.max(0, Math.ceil(remainingMs / 1000));
461        const minutes = Math.floor(totalSeconds / 60);
462        const seconds = totalSeconds % 60;
463        return `${String(minutes).padStart(2, '0')}
463:${String(seconds).padStart(2, '0')}`;
464    }
465
466    function renderPlanningSessionPanel() {
467        if (!planningSession) return '';
468        const remainingMs = getPlanningSessionRemainingMs(planningSession);
469        if (remainingMs <= 0) return '';
470        const durationMs = Date.parse(planningSession.endsAt) - Date.parse(planningSession.startedAt);
471        const progress = Math.max(0, Math.min(100, (remainingMs / durationMs) * 100));
472        const time = formatPlanningTime(remainingMs);
473
474        return `
475            <section class="planning-session-card" role="timer" aria-labelledby="planning-session-title"
476                     aria-label="${escapeAttribute(translateProject('planning_session_time_remaining', { time }))}"
477                     style="--planning-progress:${progress}%;">
478                <span class="planning-session-card__mark" aria-hidden="true">
479                    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
480                        <circle cx="12" cy="12" r="9"></circle>
481                        <path d="M12 7v5l3 2"></path>
482                    </svg>
483                </span>
484                <span class="planning-session-card__copy">
485                    <small>${escapeHtml(i18n.t('planning_session_kicker'))}</small>
486                    <strong id="planning-session-title">${escapeHtml(i18n.t('planning_session_title'))}</strong>
487                    <span>${escapeHtml(i18n.t('planning_session_hint'))}</span>
488                </span>
489                <time class="planning-session-card__time" data-planning-session-time datetime="PT${Math.ceil(remainingMs / 1000)}S">${time}</time>
490                <button class="planning-session-card__finish" type="button" data-planning-session-finish>
491                    ${escapeHtml(i18n.t('planning_session_finish'))}
492                </button>
493                <span class="planning-session-card__progress" aria-hidden="true"></span>
494            </section>
495        `;
496    }
497
498    function stopPlanningTimer() {
499        if (planningTimerId === null) return;
500        window.clearInterval(planningTimerId);
501        planningTimerId = null;
502    }
503
504    function completePlanningSession(reason = 'elapsed') {
505        if (!planningSession) return;
506        const completedSession = planningSession;
507        planningSession = null;
508        stopPlanningTimer();
509        savePlanningSession();
510        container.querySelector('.planning-session-card')?.remove();
511        showDailyToast(i18n.t('planning_session_completed'));
512
513        queueMicrotask(() => {
514            ctx.bus?.emit('planning-session:completed', {
515                ownerId: completedSession.ownerId,
516                completedAt: new Date().toISOString(),
517                reason
518            });
519        });
520    }
521
522    function updatePlanningSessionUi() {
523        if (!planningSession) {
524            stopPlanningTimer();
525            return;
526        }
527
528        const remainingMs = getPlanningSessionRemainingMs(planningSession);
529        if (remainingMs <= 0) {
530            completePlanningSession('elapsed');
531            return;
532        }
533
534        const card = container.querySelector('.planning-session-card');
535        const timeElement = card?.querySelector('[data-planning-session-time]');
536        if (!card || !timeElement) return;
537
538        const durationMs = Date.parse(planningSession.endsAt) - Date.parse(planningSession.startedAt);
539        const progress = Math.max(0, Math.min(100, (remainingMs / durationMs) * 100));
540        const time = formatPlanningTime(remainingMs);
541        card.style.setProperty('--planning-progress', `${progress}%`);
542        card.setAttribute('aria-label', translateProject('planning_session_time_remaining', { time }));
543        timeElement.textContent = time;
544        timeElement.setAttribute('datetime', `PT${Math.ceil(remainingMs / 1000)}S`);
545    }
546
547    function syncPlanningTimer() {
548        updatePlanningSessionUi();
549        if (!planningSession || planningTimerId !== null) return;
550        planningTimerId = window.setInterval(updatePlanningSessionUi, 1000);
551    }
552
553    function startPlanningSession(payload = {}) {
554        const activeOwnerId = String(ctx.currentUser?.id || 'guest');
555        const requestedOwnerId = String(payload.ownerId || activeOwnerId);
556        if (requestedOwnerId !== activeOwnerId) return;
557
558        planningOwnerId = activeOwnerId;
559        planningSession = startOrResumePlanningSession(planningSession, {
560            ownerId: planningOwnerId,
561            durationMs: PLANNING_SESSION_DURATION_MS
562        });
563        savePlanningSession();
564        render();
565        syncPlanningTimer();
566    }
567
568    function setPlanningOwner(user) {
569        const nextOwnerId = String(user?.id || 'guest');
570        if (nextOwnerId === planningOwnerId) return;
571        stopPlanningTimer();
572        planningOwnerId = nextOwnerId;
573        planningSession = loadPlanningSession(planningOwnerId);
574        if (container.closest('body')) {
575            render();
576            syncPlanningTimer();
577        }
578    }
579
580    function getGeneralPriorityLabel(meta) {
581        return i18n.t(meta.labelKey);
582    }
583
584    const dailyPriorityBusy = new Set();
585    let priorityMenuElement = null;
586    let priorityMenuTrigger = null;
587    let priorityMenuCleanup = null;
588    let pendingDailyRender = false;
589    let pendingActivationRender = false;
590
591    function normalizeGeneralPriority(value) {
592        const normalized = String(value ?? '').trim().toLowerCase();
593        if (normalized === '3' || normalized === 'high' || normalized === 'p1') return 'p1';
594        if (normalized === '2' || normalized === 'medium' || normalized === 'p2') return 'p2';
595        if (normalized === '1' || normalized === 'low' || normalized === 'p3') return 'p3';
596        return 'none';
597    }
598
599    function getDailyTaskId(item) {
600        return String(item?.queueTaskId ?? item?.queue_task_id ?? item?.taskId ?? item?.task_id ?? item?.sourceBlockId ?? item?.id ?? '');
601    }
602
603    function normalizedIdentifier(value) {
604        return String(value ?? '').replace(/-/g, '').toLowerCase();
605    }
606
607    function getDailyItems() {
608        try {
609            return [...(getTodayPriorities() || [])].sort((a, b) => Number(a.position || 0) - Number(b.position || 0));
610        } catch (error) {
611            console.warn('Impossible de lire les priorités du jour :', error);
612            return [];
613        }
614    }
615
616    function isBlockInToday(block, items = getDailyItems()) {
617        const resolved = resolveTaskReference(block?.id);
618        const candidates = [block?.id, block?.notionBlockId, resolved?.taskId, resolved?.task_id]
619            .map(normalizedIdentifier)
620            .filter(Boolean);
621        return items.some(item => candidates.includes(normalizedIdentifier(getDailyTaskId(item))));
622    }
623
624    function showDailyToast(message) {
625        ctx.bus?.emit('ui:toast', message);
626    }
627
628    function getDailyPlanningStatus(taskId) {
629        const events = ctx.store.get('rox_calendar_v1');
630        const event = findPlannedTaskEvent(Array.isArray(events) ? events : [], taskId);
631        if (!event) return null;
632        const start = new Date(event.startISO || event.start);
633        const end = new Date(event.endISO || event.end);
634        if (Number.isNaN(start.getTime())) return null;
635        const time = start.toLocaleTimeString(i18n.locale(), { hour: '2-digit', minute: '2-digit' });
636        const today = new Date();
637        const sameDay = start.getFullYear() === today.getFullYear()
638            && start.getMonth() === today.getMonth()
639            && start.getDate() === today.getDate();
640        const date = sameDay ? '' : `${start.toLocaleDateString(i18n.locale(), { day: 'numeric', month: 'short' })} · `;
641        const duration = Number.isNaN(end.getTime()) ? null : Math.max(1, Math.round((end - start) / 60000));
642        return {
643            label: `${date}${time}${duration ? ` · ${duration} min` : ''}`,
644            eventId: event.id
645        };
646    }
647
648    function renderDailyPrioritiesPanel() {
649        const items = getDailyItems();
650        const listHtml = items.length ? items.map((item, index) => {
651            const taskId = getDailyTaskId(item);
652            const title = String(item.title || item.text || i18n.t('daily_priority_untitled_task'));
653            const projectName = String(item.projectName || item.project_name || i18n.t('common_project'));
654            const projectIcon = String(item.projectIcon || item.project_icon || '');
655            const pomoSettings = (typeof pomodoroStore !== 'undefined' ? pomodoroStore.getSettings() : null) || {};
656            const focusTime = Number(pomoSettings.focusTime) || 25;
657            const duration = Number(item.estimatedMinutes || item.estimated_minutes || item.duration_minutes || focusTime);
658            
659            // Calculate base session duration and session multiplier
660            let sessionMinutes = focusTime;
661            let multiplier = 1;
662            if (duration % focusTime === 0) {
663                multiplier = Math.max(1, Math.floor(duration / focusTime));
664            } else {
665                multiplier = Math.max(1, Math.round(duration / focusTime));
666                sessionMinutes = Math.max(1, Math.round(duration / multiplier));
667            }
668            const totalEstimated = sessionMinutes * multiplier;
669
670            const priority = normalizeGeneralPriority(item.generalPriority ?? item.general_priority ?? item.priority);
671            const meta = GENERAL_PRIORITY_META[priority];
672            const priorityLabel = getGeneralPriorityLabel(meta);
673            const missing = Boolean(item.missing);
674            const planning = getDailyPlanningStatus(taskId);
675            const planningState = planning
676                ? `${i18n.t('daily_priority_planned')} ${planning.label}`
677                : i18n.t('daily_priority_unplanned');
678            const totalDurationTitle = translateProject(
679                multiplier === 1 ? 'daily_priority_total_duration_one' : 'daily_priority_total_duration_many',
680                { total: totalEstimated, count: multiplier, minutes: sessionMinutes }
681            );
682            return `
683                <li class="daily-priority-item${missing ? ' is-missing' : ''}" data-daily-task-id="${escapeAttribute(taskId)}">
684                    <button class="daily-priority-drag" type="button" draggable="true" aria-label="${escapeAttribute(translateProject('daily_priority_drag', { task: title }))}" title="${escapeAttribute(i18n.t('daily_priority_drag_title'))}">⋮⋮</button>
685                    <button class="daily-priority-position is-${priority}" type="button" data-daily-action="change-priority" aria-label="${escapeAttribute(translateProject('daily_priority_position', { position: index + 1, priority: priorityLabel }))}" title="${escapeAttribute(translateProject('daily_priority_edit_priority', { priority: priorityLabel }))}">${index + 1}</button>
686                    <button class="daily-priority-open" type="button" data-daily-action="open" ${missing ? 'disabled' : ''}>
687                        <span class="daily-priority-title">${escapeHtml(title)}</span>
688                        <span class="daily-priority-project">
689                            ${escapeHtml(projectIcon)} ${escapeHtml(projectName)}
690                            <span class="daily-priority-calendar-state${planning ? ' is-planned' : ''}">
691                                · ${escapeHtml(planningState)}
692                            </span>
693                        </span>
694                    </button>
695                    <div class="daily-priority-duration" title="${escapeAttribute(totalDurationTitle)}">
696                        <label class="daily-priority-duration-session" title="${escapeAttribute(i18n.t('daily_priority_focus_duration'))}">
697                            <span class="projects-visually-hidden">${escapeHtml(translateProject('daily_priority_focus_duration_for', { task: title }))}</span>
698                            <input type="number" min="1" max="480" step="5" value="${sessionMinutes}" data-daily-session-duration ${missing ? 'disabled' : ''}>
699                            <span>min</span>
700                        </label>
701                        <span class="daily-priority-duration-multiply" aria-hidden="true">×</span>
702                        <label class="daily-priority-duration-multiplier" title="${escapeAttribute(i18n.t('daily_priority_session_count'))}">
703                            <span class="projects-visually-hidden">${escapeHtml(translateProject('daily_priority_session_count_for', { task: title }))}</span>
704                            <input type="number" min="1" max="50" step="1" value="${multiplier}" data-daily-multiplier ${missing ? 'disabled' : ''}>
705                        </label>
706                    </div>
707                    <div class="daily-priority-actions" aria-label="${escapeAttribute(translateProject('daily_priority_actions_for', { task: title }))}">
708                        <button type="button" data-daily-action="remove" aria-label="${escapeAttribute(translateProject('daily_priority_remove_from_today', { task: title }))}" title="${escapeAttribute(i18n.t('daily_priority_remove'))}">×</button>
709                    </div>
710                </li>
711            `;
712        }).join('') : `
713            <li class="daily-priorities-empty">
714                ${escapeHtml(i18n.t('daily_priority_empty'))}
715            </li>
716        `;
717
718        return `
719            <section class="daily-priorities-panel" aria-labelledby="daily-priorities-title">
720                <header class="daily-priorities-header">
721                    <div>
722                        <h3 id="daily-priorities-title">${escapeHtml(i18n.t('daily_priority_title'))} <span aria-hidden="true">·</span> ${items.length}/3</h3>
723                    </div>
724                    <button type="button" class="daily-priorities-plan-btn" data-action="plan-all-priorities" ${items.length === 0 ? 'disabled' : ''} title="${escapeAttribute(i18n.t('daily_priority_plan_all_title'))}">
725                        <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-sparkles" aria-hidden="true"><path d="m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z"/><path d="m5 3 1 2.5L8.5 6 6 7 5 9.5 4 7 1.5 6 4 5.5z"/><path d="m19 17 1 2.5 2.5.5-2.5 1-1 2.5-1-2.5-2.5-1 2.5-1z"/></svg>
726                        ${escapeHtml(i18n.t('daily_priority_plan_all'))}
727                    </button>
728                </header>
729                <ol class="daily-priorities-list">${listHtml}</ol>
730            </section>
731        `;
732    }
733
734    async function addBlockToToday(blockId) {
735        const busyKey = `add:${blockId}`;
736        if (dailyPriorityBusy.has(busyKey)) return null;
737        dailyPriorityBusy.add(busyKey);
738        try {
739            const result = await addTaskToToday(blockId);
740            showDailyToast(result?.status === 'already-added'
741                ? i18n.t('daily_priority_already_added_toast')
742                : i18n.t('daily_priority_added_toast'));
743            render();
744            return result;
745        } catch (error) {
746            if (error?.code === 'MAX_PRIORITIES') {
747                showDailyToast(i18n.t('daily_priority_limit_toast'));
748            } else {
749                console.error('Erreur ajout priorité du jour :', error);
750                showDailyToast(i18n.t('daily_priority_add_error'));
751            }
752            return null;
753        } finally {
754            dailyPriorityBusy.delete(busyKey);
755        }
756    }
757
758    async function removeBlockFromToday(blockId) {
759        const busyKey = `remove:${blockId}`;
760        if (dailyPriorityBusy.has(busyKey)) return null;
761        dailyPriorityBusy.add(busyKey);
762        try {
763            const result = await removeTaskFromToday(blockId);
764            showDailyToast(i18n.t('daily_priority_removed_toast'));
765            render();
766            return result;
767        } catch (error) {
768            console.error('Erreur retrait priorité du jour :', error);
769            showDailyToast(i18n.t('daily_priority_remove_error'));
770            return null;
771        } finally {
772            dailyPriorityBusy.delete(busyKey);
773        }
774    }
775
776    function readAndSaveEditableTask(blockId) {
777        const blockElement = container.querySelector(`.editor-block[data-id="${CSS.escape(String(blockId))}"]`);
778        const editor = blockElement?.querySelector('.editable-text');
779        if (!editor) return '';
780
781        const text = convertHtmlToMarkdown(editor.innerHTML).trim();
782        if (!text) {
783            editor.focus();
784            return '';
785        }
786
787        const block = (store.getBlocks(selectedPageId) || []).find(item => item.id === blockId);
788        const notionData = { ...(block?.notionData || {}) };
789        notionData.rich_text = NotionRichText.fromHtml(editor.innerHTML);
790        debouncedUpdateBlock.cancel();
791        store.updateBlock(selectedPageId, blockId, { text, notionData });
792        return text;
793    }
794
795    async function continueFirstTaskToFocus(button) {
796        const blockId = button.dataset.blockId;
797        const text = readAndSaveEditableTask(blockId);
798        if (!text) return;
799
800        button.disabled = true;
801        button.setAttribute('aria-busy', 'true');
802
803        const alreadyInToday = isBlockInToday({ id: blockId });
804        const result = alreadyInToday ? { status: 'already-added' } : await addBlockToToday(blockId);
805        if (!result) {
806            if (button.isConnected) {
807                button.disabled = false;
808                button.removeAttribute('aria-busy');
809            }
810            return;
811        }
812
813        firstFocusActivationTaskId = null;
814        render();
815        ctx.bus?.emit('app:push-task', { blockId, blockText: text });
816    }
817
818    async function moveDailyPriority(taskId, delta) {
819        const items = getDailyItems();
820        const orderedIds = items.map(getDailyTaskId);
821        const index = orderedIds.findIndex(id => normalizedIdentifier(id) === normalizedIdentifier(taskId));
822        const nextIndex = index + delta;
823        if (index < 0 || nextIndex < 0 || nextIndex >= orderedIds.length) return;
824        [orderedIds[index], orderedIds[nextIndex]] = [orderedIds[nextIndex], orderedIds[index]];
825        await reorderTodayPriorities(orderedIds);
826        render();
827    }
828
829    function closeTaskPriorityMenu(restoreFocus = true) {
830        priorityMenuCleanup?.();
831        priorityMenuCleanup = null;
832        priorityMenuTrigger?.setAttribute('aria-expanded', 'false');
833        priorityMenuElement?.remove();
834        priorityMenuElement = null;
835        if (restoreFocus && priorityMenuTrigger?.isConnected) priorityMenuTrigger.focus();
836        priorityMenuTrigger = null;
837    }
838
839    function openTaskPriorityMenu(trigger, blockId) {
840        closeTaskPriorityMenu(false);
841        priorityMenuTrigger = trigger;
842        let current = 'none';
843        const allPages = store.getPages ? store.getPages() : [];
844        let foundBlock = null;
845        for (const page of allPages) {
846            const blocks = store.getBlocks(page.id) || [];
847            foundBlock = blocks.find(item => String(item.id) === String(blockId));
848            if (foundBlock) break;
849        }
850        if (foundBlock) {
851            current = normalizeGeneralPriority(foundBlock.generalPriority ?? foundBlock.general_priority ?? foundBlock.priority);
852        } else {
853            const dailyItems = getDailyItems() || [];
854            const dItem = dailyItems.find(it => String(it.taskId || it.id) === String(blockId));
855            if (dItem) {
856                current = normalizeGeneralPriority(dItem.generalPriority ?? dItem.general_priority ?? dItem.priority);
857            }
858        }
859        const menu = document.createElement('div');
860        menu.className = 'task-priority-menu';
861        menu.setAttribute('role', 'menu');
862        menu.setAttribute('aria-label', i18n.t('daily_priority_general'));
863        menu.innerHTML = Object.entries(GENERAL_PRIORITY_META).map(([value, meta]) => `
864            <button type="button" role="menuitemradio" aria-checked="${current === value}" data-priority-value="${value}" class="is-${value}">
865                <span class="task-priority-menu-flag" aria-hidden="true">⚑</span>
866                <span><strong>${meta.code === '—' ? escapeHtml(getGeneralPriorityLabel(meta)) : meta.code}</strong>${value === 'none' ? '' : ` — ${escapeHtml(getGeneralPriorityLabel(meta))}`}</span>
867            </button>
868        `).join('');
869        document.body.appendChild(menu);
870        priorityMenuElement = menu;
871        trigger.setAttribute('aria-expanded', 'true');
872
873        const rect = trigger.getBoundingClientRect();
874        const left = Math.min(rect.left, window.innerWidth - menu.offsetWidth - 12);
875        const top = Math.min(rect.bottom + 6, window.innerHeight - menu.offsetHeight - 12);
876        menu.style.left = `${Math.max(12, left)}px`;
877        menu.style.top = `${Math.max(12, top)}px`;
878
879        menu.querySelectorAll('[data-priority-value]').forEach(button => {
880            button.addEventListener('click', async () => {
881                try {
882                    await setTaskGeneralPriority(blockId, button.dataset.priorityValue);
883                    ctx.bus?.emit('objectifs:refresh');
884                    closeTaskPriorityMenu(false);
885                    render();
886                } catch (error) {
887                    console.error('Erreur priorité générale :', error);
888                    showDailyToast(i18n.t('daily_priority_change_error'));
889                }
890            });
891        });
892
893        const closeOnPointer = event => {
894            if (!menu.contains(event.target) && event.target !== trigger) {
895                closeTaskPriorityMenu(false);
896            }
897        };
898        const closeOnKey = event => {
899            if (event.key === 'Escape') {
900                event.preventDefault();
901                closeTaskPriorityMenu(true);
902                return;
903            }
904            if (!['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)) return;
905            const options = [...menu.querySelectorAll('[role="menuitemradio"]')];
906            const currentIndex = options.indexOf(document.activeElement);
907            let nextIndex = currentIndex;
908            if (event.key === 'Home') nextIndex = 0;
909            if (event.key === 'End') nextIndex = options.length - 1;
910            if (event.key === 'ArrowDown') nextIndex = (currentIndex + 1 + options.length) % options.length;
911            if (event.key === 'ArrowUp') nextIndex = (currentIndex - 1 + options.length) % options.length;
912            event.preventDefault();
913            options[nextIndex]?.focus();
914        };
915        priorityMenuCleanup = () => {
916            document.removeEventListener('pointerdown', closeOnPointer, true);
917            document.removeEventListener('keydown', closeOnKey, true);
918        };
919        document.addEventListener('pointerdown', closeOnPointer, true);
920        document.addEventListener('keydown', closeOnKey, true);
921        menu.querySelector('button')?.focus();
922    }
923
924    function bindDailyPriorityUI() {
925        container.querySelectorAll('.first-focus-activation-button').forEach(button => {
926            button.addEventListener('click', event => {
927                event.preventDefault();
928                event.stopPropagation();
929                continueFirstTaskToFocus(button);
930            });
931        });
932
933        container.querySelectorAll('.add-to-today-button:not([disabled])').forEach(button => {
934            button.addEventListener('click', event => {
935                event.preventDefault();
936                event.stopPropagation();
937                button.disabled = true;
938                button.setAttribute('aria-busy', 'true');
939                const isAdded = button.classList.contains('is-added');
940                const action = isAdded
941                    ? removeBlockFromToday(button.dataset.blockId)
942                    : addBlockToToday(button.dataset.blockId);
943                action.finally(() => {
944                    if (button.isConnected) {
945                        button.disabled = false;
946                        button.removeAttribute('aria-busy');
947                    }
948                });
949            });
950        });
951
952        container.querySelectorAll('.task-priority-flag-button').forEach(button => {
953            button.addEventListener('click', event => {
954                event.preventDefault();
955                event.stopPropagation();
956                openTaskPriorityMenu(button, button.dataset.blockId);
957            });
958        });
959
960        const planAllBtn = container.querySelector('[data-action="plan-all-priorities"]');
961        if (planAllBtn) {
962            planAllBtn.addEventListener('click', (e) => {
963                e.preventDefault();
964                const request = { handled: false };
965                ctx.bus?.emit('calendar:plan-all-priorities', request);
966
967                if (!request.handled) {
968                    showDailyToast(i18n.t('daily_priority_calendar_unavailable'));
969                } else if (request.plannedCount > 0) {
970                    showDailyToast(translateProject(
971                        request.plannedCount === 1 ? 'daily_priority_slots_added_one' : 'daily_priority_slots_added_many',
972                        { count: request.plannedCount }
973                    ));
974                } else if (request.missingSlotCount > 0) {
975                    showDailyToast(i18n.t('daily_priority_no_slots'));
976                } else if (request.removedCount > 0) {
977                    showDailyToast(i18n.t('daily_priority_calendar_adjusted'));
978                } else {
979                    showDailyToast(i18n.t('daily_priority_already_planned'));
980                }
981            });
982        }
983
984        container.querySelectorAll('[data-daily-action]').forEach(button => {
985            button.addEventListener('click', async event => {
986                event.preventDefault();
987                const row = button.closest('.daily-priority-item');
988                const taskId = row?.dataset.dailyTaskId;
989                if (!taskId) return;
990                const busyKey = `${button.dataset.dailyAction}:${taskId}`;
991                if (dailyPriorityBusy.has(busyKey)) return;
992                dailyPriorityBusy.add(busyKey);
993                button.disabled = true;
994                try {
995                    if (button.dataset.dailyAction === 'change-priority') {
996                        openTaskPriorityMenu(button, taskId);
997                        button.disabled = false;
998                        dailyPriorityBusy.delete(busyKey);
999                        return;
1000                    } else if (button.dataset.dailyAction === 'remove') {
1001                        await removeTaskFromToday(taskId);
1002                        showDailyToast(i18n.t('daily_priority_removed_toast'));
1003                        render();
1004                    } else if (button.dataset.dailyAction === 'up') {
1005                        await moveDailyPriority(taskId, -1);
1006                    } else if (button.dataset.dailyAction === 'down') {
1007                        await moveDailyPriority(taskId, 1);
1008                    } else if (button.dataset.dailyAction === 'open') {
1009                        openDailyPrioritySource({ taskId });
1010                    }
1011                } catch (error) {
1012                    console.error('Erreur priorité du jour :', error);
1013                    showDailyToast(i18n.t('daily_priority_update_error'));
1014                    render();
1015                } finally {
1016                    dailyPriorityBusy.delete(busyKey);
1017                }
1018            });
1019        });
1020
1021        container.querySelectorAll('[data-daily-session-duration], [data-daily-multiplier]').forEach(input => {
1022            input.addEventListener('change', async () => {
1023                const row = input.closest('.daily-priority-item');
1024                const taskId = row?.dataset.dailyTaskId;
1025                if (!taskId) return;
1026                
1027                const sessInput = row.querySelector('[data-daily-session-duration]');
1028                const multInput = row.querySelector('[data-daily-multiplier]');
1029                if (!sessInput || !multInput) return;
1030                
1031                const sessVal = Math.max(1, Math.min(480, Number(sessInput.value) || 25));
1032                const multVal = Math.max(1, Math.min(50, Number(multInput.value) || 1));
1033                const totalMinutes = Math.min(1440, sessVal * multVal);
1034                
1035                sessInput.disabled = true;
1036                multInput.disabled = true;
1037                try {
1038                    await updateTaskEstimatedMinutes(taskId, totalMinutes);
1039                    showDailyToast(translateProject(
1040                        multVal === 1 ? 'daily_priority_duration_updated_one' : 'daily_priority_duration_updated_many',
1041                        { count: multVal, minutes: sessVal, total: totalMinutes }
1042                    ));
1043                    render();
1044                } catch (error) {
1045                    console.error('Erreur durée estimée :', error);
1046                    showDailyToast(i18n.t('daily_priority_duration_range_error'));
1047                    render();
1048                }
1049            });
1050        });
1051
1052        const list = container.querySelector('.daily-priorities-list');
1053        if (!list) return;
1054        let draggedTaskId = null;
1055        list.querySelectorAll('.daily-priority-drag').forEach(handle => {
1056            handle.addEventListener('dragstart', event => {
1057                const row = handle.closest('.daily-priority-item');
1058                draggedTaskId = row?.dataset.dailyTaskId || null;
1059                if (!draggedTaskId) {
1060                    event.preventDefault();
1061                    return;
1062                }
1063                row.classList.add('is-dragging');
1064                event.dataTransfer.effectAllowed = 'move';
1065                event.dataTransfer.setData('text/plain', draggedTaskId);
1066
1067                const items = getDailyItems();
1068                const item = items.find(it => getDailyTaskId(it) === draggedTaskId);
1069                if (item) {
1070                    event.dataTransfer.setData('application/json', JSON.stringify({
1071                        type: 'todo-item',
1072                        id: draggedTaskId,
1073                        sourceId: draggedTaskId,
1074                        text: item.title || item.text || i18n.t('daily_priority_untitled_task'),
1075                        estimatedMinutes: Number(item.estimatedMinutes || item.estimated_minutes || 25)
1076                    }));
1077                    window.__roximaDraggingTask = {
1078                        text: item.title || item.text || i18n.t('daily_priority_untitled_task'),
1079                        id: draggedTaskId,
1080                        origin: 'daily-priorities'
1081                    };
1082                }
1083            });
1084            handle.addEventListener('dragend', () => {
1085                list.querySelectorAll('.daily-priority-item').forEach(row => row.classList.remove('is-dragging', 'is-drop-target'));
1086                draggedTaskId = null;
1087                window.__roximaDraggingTask = null;
1088            });
1089        });
1090        list.addEventListener('dragover', event => {
1091            if (draggedTaskId) {
1092                const target = event.target.closest('.daily-priority-item');
1093                if (!target || target.dataset.dailyTaskId === draggedTaskId) return;
1094                event.preventDefault();
1095                event.dataTransfer.dropEffect = 'move';
1096                list.querySelectorAll('.daily-priority-item').forEach(row => row.classList.toggle('is-drop-target', row === target));
1097            } else {
1098                // Dragging from editor
1099                const isProjectDrag = event.dataTransfer.types.includes('application/json') || window.__roximaDraggingTask;
1100                if (isProjectDrag) {
1101                    event.preventDefault();
1102                    event.dataTransfer.dropEffect = 'copy';
1103                    list.classList.add('is-drag-over');
1104                }
1105            }
1106        });
1107        list.addEventListener('dragleave', () => {
1108            list.classList.remove('is-drag-over');
1109        });
1110        list.addEventListener('drop', async event => {
1111            event.preventDefault();
1112            list.classList.remove('is-drag-over');
1113            if (draggedTaskId) {
1114                const target = event.target.closest('.daily-priority-item');
1115                if (!target) return;
1116                const targetId = target.dataset.dailyTaskId;
1117                const orderedIds = getDailyItems().map(getDailyTaskId).filter(id => normalizedIdentifier(id) !== normalizedIdentifier(draggedTaskId));
1118                let insertIndex = orderedIds.findIndex(id => normalizedIdentifier(id) === normalizedIdentifier(targetId));
1119                if (insertIndex < 0) return;
1120                if (event.clientY > target.getBoundingClientRect().top + target.offsetHeight / 2) insertIndex += 1;
1121                orderedIds.splice(insertIndex, 0, draggedTaskId);
1122                try {
1123                    await reorderTodayPriorities(orderedIds);
1124                    render();
1125                } catch (error) {
1126                    console.error('Erreur réorganisation priorités :', error);
1127                    showDailyToast(i18n.t('daily_priority_reorder_error'));
1128                    render();
1129                } finally {
1130                    draggedTaskId = null;
1131                }
1132            } else {
1133                // Drop from editor
1134                let blockId = null;
1135                if (window.__roximaDraggingTask && window.__roximaDraggingTask.id) {
1136                    blockId = window.__roximaDraggingTask.id;
1137                } else {
1138                    const dragData = event.dataTransfer.getData('text/plain');
1139                    if (dragData && dragData.length > 20) {
1140                        blockId = dragData;
1141                    }
1142                }
1143                if (blockId) {
1144                    await addBlockToToday(blockId);
1145                }
1146            }
1147        });
1148    }
1149
1150    function findTaskSource(task) {
1151        const pageId = task?.sourcePageId || task?.source_page_id || task?.projectId || task?.project_id;
1152        const blockId = task?.sourceBlockId || task?.source_block_id || task?.blockId || task?.block_id;
1153        if (pageId && blockId) {
1154            const block = (store.getBlocks(pageId) || []).find(item =>
1155                normalizedIdentifier(item.id) === normalizedIdentifier(blockId)
1156                || normalizedIdentifier(item.notionBlockId) === normalizedIdentifier(blockId)
1157            );
1158            if (block) return { pageId, block };
1159        }
1160
1161        const candidateId = task?.taskId || task?.task_id || task?.id;
1162        for (const page of store.getPages()) {
1163            const block = (store.getBlocks(page.id) || []).find(item =>
1164                normalizedIdentifier(item.id) === normalizedIdentifier(candidateId)
1165                || normalizedIdentifier(item.notionBlockId) === normalizedIdentifier(candidateId)
1166            );
1167            if (block) return { pageId: page.id, block };
1168        }
1169        return null;
1170    }
1171
1172    function openDailyPrioritySource(payload = {}) {
1173        const request = payload?.task || payload;
1174        const requestedId = request?.taskId || request?.task_id || request?.sourceBlockId || request?.id;
1175        const task = resolveTaskReference(requestedId) || request;
1176        const source = findTaskSource(task);
1177        if (!source) {
1178            showDailyToast(i18n.t('daily_priority_source_missing'));
1179            return;
1180        }
1181
1182        const page = store.getPage(source.pageId);
1183        if (page?.notionWorkspaceId && page.notionWorkspaceId !== getActiveNotionWorkspaceId()) {
1184            const workspace = getNotionWorkspaces().find(item => item.id === page.notionWorkspaceId);
1185            localStorage.setItem('roxima_notion_active_workspace_id', page.notionWorkspaceId);
1186            if (workspace && !isDemoNotionWorkspace(workspace)) setupNotionSyncInterval(workspace);
1187        }
1188        setSelectedPageId(source.pageId);
1189        ctx.bus?.emit('app:show-panel', { panelId: 'region-projects' });
1190        render({ preserveCaret: false });
1191        requestAnimationFrame(() => focusBlock(source.block.id, 'end'));
1192    }
1193
1194    async function handleDailyTaskExecutionRequest(payload, executionStatus) {
1195        const request = payload?.task || payload || {};
1196        const requestedId = request.taskId || request.task_id || request.sourceBlockId || request.id;
1197        let source = null;
1198        try {
1199            const task = resolveTaskReference(requestedId) || request;
1200            source = findTaskSource(task);
1201            if (!source) throw new Error('La tâche source est introuvable.');
1202            const patch = executionStatus === 'done'
1203                ? { checked: true, executionStatus: 'done', execution_status: 'done' }
1204                : { checked: false, executionStatus: 'blocked', execution_status: 'blocked' };
1205            // Sync a detached payload: the shared store must stay untouched
1206            // until Notion acknowledges the update, even if another save runs
1207            // while the request is in flight.
1208            const synced = await syncBlockToNotion(source.pageId, source.block.id, patch);
1209            if (!synced) throw new Error('La synchronisation de la tâche a échoué.');
1210            store.updateBlock(source.pageId, source.block.id, patch);
1211            ctx.bus?.emit('objectifs:refresh');
1212            const result = { status: executionStatus, task: resolveTaskReference(requestedId) || task };
1213            payload?.resolve?.(result);
1214            payload?.onSuccess?.(result);
1215            payload?.callback?.(null, result);
1216        } catch (error) {
1217            console.error(`Erreur passage de tâche à l’état ${executionStatus} :`, error);
1218            showDailyToast(i18n.t('daily_priority_task_update_error'));
1219            payload?.reject?.(error);
1220            payload?.onError?.(error);
1221            payload?.callback?.(error);
1222        }
1223    }
1224
1225    async function deleteBlockFromNotion(pageId, notionBlockId) {
1226        const page = store.getPage(pageId);
1227        const notionWorkspace = page?.notionWorkspaceId
1228            ? getNotionWorkspaces().find(workspace => workspace.id === page.notionWorkspaceId)
1229            : getActiveNotionWorkspace();
1230        if (!notionWorkspace || isDemoNotionWorkspace(notionWorkspace)) return;
1231
1232        try {
1233            await notionRequest('/api/notion/delete-block', notionWorkspace.id, {
1234                blockId: notionBlockId
1235            });
1236        } catch (error) {
1237            console.error("Erreur deleteBlockFromNotion:", error);
1238        }
1239    }
1240
1241    async function syncNotion(activeWorkspace, { revealImportedPage = false } = {}) {
1242        if (!activeWorkspace) {
1243            window.Roxima?.showToast?.("Aucun environnement Notion connecté.");
1244            return;
1245        }
1246
1247        if (isDemoNotionWorkspace(activeWorkspace)) return;
1248
1249        try {
1250            window.Roxima?.showToast?.(`Synchronisation de ${activeWorkspace.name}...`);
1251
1252            const store = objectifsStore;
1253            // The automatic Notion refresh can run while a task is still being
1254            // edited. Push that live value first so the following pull cannot
1255            // replace it with the older remote title.
1256            const focusedEditor = container.querySelector('.editable-text:focus');
1257            if (focusedEditor && selectedPageId) {
1258                const committed = commitEditableBlockNow(store, selectedPageId, focusedEditor);
1259                if (committed) {
1260                    debouncedSyncBlockToNotion.cancel();
1261                    const savedRemotely = await syncBlockToNotion(selectedPageId, committed.id);
1262                    if (!savedRemotely) {
1263                        throw new Error('La tâche en cours ne peut pas être synchronisée sans risque de perte.');
1264                    }
1265                }
1266            }
1267
1268            // Wait for any pending focusout / local debounced changes to save
1269            await new Promise(resolve => setTimeout(resolve, 350));
1270
1271            const previousNotionPageIds = new Set(
1272                store.getPages()
1273                    .filter(page => page.notionWorkspaceId === activeWorkspace.id && page.notionPageId)
1274                    .map(page => page.notionPageId)
1275            );
1276            const pages = store.getPages();
1277            
1278            // Sync any newly created blocks that haven't been pushed to Notion yet
1279            for (const p of pages) {
1280                if (p.isNotion && p.notionWorkspaceId === activeWorkspace.id) {
1281                    const blocks = store.getBlocks(p.id) || [];
1282                    for (const block of blocks) {
1283                        if (!block.notionBlockId) {
1284                            await syncBlockToNotion(p.id, block.id);
1285                        }
1286                    }
1287                }
1288            }
1289
1290            const data = await notionRequest('/api/notion/sync-pages', activeWorkspace.id);
1291
1292            assertCompleteNotionSnapshot(data);
1293            
1294            if (data.pages && Array.isArray(data.pages)) {
1295                const idMap = {}; // notionPageId -> roximaPageId
1296                
1297                const existingPages = store.getPages();
1298                existingPages.forEach(p => {
1299                    if (p.notionPageId) {
1300                        idMap[p.notionPageId] = p.id;
1301                    }
1302                });
1303
1304                data.pages.forEach(item => {
1305                    if (idMap[item.id]) {
1306                        const page = store.getPage(idMap[item.id]);
1307                        if (page) {
1308                            page.title = item.title;
1309                            page.icon = item.icon;
1310                            page.notionLastEditedTime = item.lastEditedTime;
1311                            page.isNotion = true;
1312                            page.source = 'notion';
1313                            page.notionWorkspaceId = activeWorkspace.id;
1314                        }
1315                    } else {
1316                        const newPage = store.createPage(item.title, null, item.icon);
1317                        newPage.notionPageId = item.id;
1318                        newPage.notionLastEditedTime = item.lastEditedTime;
1319                        newPage.isNotion = true;
1320                        newPage.source = 'notion';
1321                        newPage.notionWorkspaceId = activeWorkspace.id;
1322                        idMap[item.id] = newPage.id;
1323                    }
1324                });
1325
1326                data.pages.forEach(item => {
1327                    const roximaPageId = idMap[item.id];
1328                    if (roximaPageId) {
1329                        const page = store.getPage(roximaPageId);
1330                        if (page) {
1331                            if (item.parentId && idMap[item.parentId]) {
1332                                page.parentId = idMap[item.parentId];
1333                            } else {
1334                                page.parentId = null;
1335                            }
1336                        }
1337                    }
1338                });
1339
1340                // Clean up only pages that are positively identified as stale
1341                // Notion pages for this workspace. A manual project may share a
1342                // title and parent with a Notion page; that is valid user data
1343                // and must never be treated as a duplicate.
1344                const syncedNotionPageIds = new Set(data.pages.map(item => item.id));
1345                const pagesToDelete = new Set(collectStaleNotionPageIds(
1346                    store.getPages(),
1347                    syncedNotionPageIds,
1348                    activeWorkspace.id
1349                ));
1350
1351                pagesToDelete.forEach(id => store.deletePage(id));
1352                store.save();
1353
1354                // Group incoming Notion blocks by page
1355                const notionBlocksByPage = {};
1356                data.pages.forEach(item => {
1357                    const roximaPageId = idMap[item.id];
1358                    if (roximaPageId) notionBlocksByPage[roximaPageId] = [];
1359                });
1360                if (data.todoBlocks && Array.isArray(data.todoBlocks)) {
1361                    data.todoBlocks.forEach(block => {
1362                        const roximaPageId = idMap[block.pageId];
1363                        if (!roximaPageId) return;
1364                        if (!notionBlocksByPage[roximaPageId]) {
1365                            notionBlocksByPage[roximaPageId] = [];
1366                        }
1367                        notionBlocksByPage[roximaPageId].push(block);
1368                    });
1369                }
1370
1371                let localTodos = ctx.store.get('todos') || [];
1372                let localTodosChanged = false;
1373                const deletedNotionBlocks = [];
1374
1375                // Sync blocks page by page
1376                for (const roximaPageId in notionBlocksByPage) {
1377                    const incomingBlocks = notionBlocksByPage[roximaPageId];
1378                    const page = store.getPage(roximaPageId);
1379                    const pageTitle = page ? store.getPagePath(page.id) : '';
1380                    const pageIcon = page ? page.icon : '📄';
1381
1382                    const currentBlocks = store.getBlocks(roximaPageId) || [];
1383                    const newBlocksList = [];
1384                    const incomingNotionIds = new Set(incomingBlocks.map(block => normalizedIdentifier(block.id)));
1385
1386                    currentBlocks.forEach(block => {
1387                        if ((block.isNotion || block.notionBlockId) && block.notionBlockId &&
1388                            !incomingNotionIds.has(normalizedIdentifier(block.notionBlockId))) {
1389                            deletedNotionBlocks.push({
1390                                type: 'block-deleted',
1391                                pageId: roximaPageId,
1392                                blockId: block.id,
1393                                notionBlockId: block.notionBlockId
1394                            });
1395                        }
1396                    });
1397
1398                    incomingBlocks.forEach(incoming => {
1399                        const blockType = incoming.type || 'todo';
1400                        let blockText = incoming.text;
1401                        if (blockType === 'page_link') {
1402                            blockText = idMap[incoming.text] || incoming.text;
1403                        }
1404
1405                        // Try to find existing block by notionBlockId
1406                        let existing = currentBlocks.find(b => b.notionBlockId === incoming.id);
1407                        
1408                        if (existing) {
1409                            existing.text = blockText;
1410                            existing.checked = incoming.completed;
1411                            existing.type = blockType;
1412                            existing.isNotion = true;
1413                            newBlocksList.push(existing);
1414                        } else {
1415                            // Create new block
1416                            const newId = 'block_' + Math.random().toString(36).substring(2, 9);
1417                            const newBlock = {
1418                                id: newId,
1419                                type: blockType,
1420                                text: blockText,
1421                                checked: incoming.completed,
1422                                level: 0,
1423                                notionBlockId: incoming.id,
1424                                isNotion: true
1425                            };
1426                            newBlocksList.push(newBlock);
1427                        }
1428
1429                        const blockIdForTodo = existing ? existing.id : newBlocksList[newBlocksList.length - 1].id;
1430
1431                        // Sync to To-Do block list if it's a todo type
1432                        if (blockType === 'todo') {
1433                            let existingLocalTodo = localTodos.find(t => t.sourceId === blockIdForTodo || t.notionBlockId === incoming.id);
1434                            if (existingLocalTodo) {
1435                                if (existingLocalTodo.text !== incoming.text || existingLocalTodo.completed !== incoming.completed || existingLocalTodo.projectName !== pageTitle || existingLocalTodo.projectIcon !== pageIcon || existingLocalTodo.sourceId !== blockIdForTodo) {
1436                                    existingLocalTodo.text = incoming.text;
1437                                    existingLocalTodo.completed = incoming.completed;
1438                                    existingLocalTodo.projectName = pageTitle;
1439                                    existingLocalTodo.projectIcon = pageIcon;
1440                                    existingLocalTodo.sourceId = blockIdForTodo;
1441                                    existingLocalTodo.notionBlockId = incoming.id;
1442                                    localTodosChanged = true;
1443                                }
1444                            } else {
1445                                localTodos.push({
1446                                    id: 'todo_' + Math.random().toString(36).substring(2, 9),
1447                                    text: incoming.text,
1448                                    completed: incoming.completed,
1449                                    sourceId: blockIdForTodo,
1450                                    projectId: roximaPageId,
1451                                    projectName: pageTitle,
1452                                    projectIcon: pageIcon,
1453                                    level: 0,
1454                                    source: 'notion',
1455                                    notionBlockId: incoming.id
1456                                });
1457                                localTodosChanged = true;
1458                            }
1459                        }
1460                    });
1461
1462                    // Keep local blocks that are NOT Notion blocks (not yet synced)
1463                    currentBlocks.forEach(b => {
1464                        if (!b.isNotion && !b.notionBlockId) {
1465                            newBlocksList.push(b);
1466                        }
1467                    });
1468
1469                    // Overwrite the blocks list in the store for this page
1470                    store.state.blocksByPageId[roximaPageId] = newBlocksList;
1471                }
1472
1473                if (localTodosChanged) {
1474                    ctx.store.set('todos', localTodos);
1475                }
1476
1477                store.save();
1478
1479                let revealedPage = null;
1480                if (revealImportedPage && data.pages.length > 0) {
1481                    const preferredNotionPage = data.pages.find(item => !previousNotionPageIds.has(item.id)) || data.pages[0];
1482                    const preferredPageId = idMap[preferredNotionPage.id];
1483                    if (preferredPageId) {
1484                        setSelectedPageId(preferredPageId);
1485                        selectedPageIds.clear();
1486                        selectedPageIds.add(String(preferredPageId));
1487                        sidebarForcedState = 'open';
1488                        revealedPage = store.getPage(preferredPageId);
1489                    }
1490                }
1491
1492                deletedNotionBlocks.forEach(change => {
1493                    ctx.bus.emit('objectifs:changed', { state: store.state, change });
1494                });
1495                render();
1496                ctx.bus.emit('objectifs:refresh');
1497                if (revealImportedPage) {
1498                    window.Roxima.notionOAuthReturn = false;
1499                    ctx.bus.emit('notion:sync:complete', {
1500                        pageCount: data.pages.length,
1501                        pageTitle: revealedPage?.title || ''
1502                    });
1503                }
1504                window.Roxima?.showToast?.(translateProject(
1505                    data.pages.length === 1 ? 'notion_sync_complete_one' : 'notion_sync_complete_many',
1506                    { count: data.pages.length }
1507                ));
1508                return { pageCount: data.pages.length, pageId: revealedPage?.id || null };
1509            }
1510        } catch (err) {
1511            console.error(err);
1512            const message = err.code === 'NOTION_AUTH_FAILED'
1513                ? 'Connexion Notion a renouveler. Les donnees locales ont ete conservees.'
1514                : err.message;
1515            window.Roxima?.showToast?.('Erreur Sync Notion: ' + message);
1516            if (revealImportedPage) {
1517                window.Roxima.notionOAuthReturn = false;
1518                ctx.bus.emit('notion:sync:error', { message });
1519            }
1520            return { pageCount: 0, pageId: null, error: err };
1521        }
1522    }
1523
1524    function showNotionConfigModal() {
1525        const workspaces = getNotionWorkspaces();
1526        const activeWorkspaceId = getActiveNotionWorkspaceId();
1527        
1528        let workspacesHtml = '';
1529        if (workspaces.length === 0) {
1530            workspacesHtml = `
1531            <div style="text-align:center; padding:20px; border:1px dashed var(--border-color, #eee); border-radius:8px; background:rgba(0,0,0,0.01);">
1532                <p style="margin:0 0 10px 0; font-size:0.9rem; color:var(--text-muted);">Aucun environnement Notion connecté.</p>
1533            </div>
1534            `;
1535        } else {
1536            workspacesHtml = workspaces.map(w => {
1537                const isActive = w.id === activeWorkspaceId;
1538                const safeWorkspaceId = escapeHtml(w.id);
1539                return `
1540                <div class="workspace-item" style="display:flex; align-items:center; justify-content:space-between; padding:10px 12px; background:rgba(0,0,0,0.02); border-radius:8px; border:1px solid ${isActive ? 'var(--text-main, #000)' : 'rgba(0,0,0,0.06)'}; margin-bottom:8px;">
1541                    <label style="display:flex; align-items:center; gap:10px; cursor:pointer; flex:1; margin:0;">
1542                        <input type="radio" name="notion-workspace-radio" value="${safeWorkspaceId}" ${isActive ? 'checked' : ''} style="margin:0; width:16px; height:16px;">
1543                        <img src="assets/img/Logo_Notion.png" width="16" height="16" alt="Notion" style="object-fit:contain; flex-shrink:0;">
1544                        <span style="font-weight:600; font-size:0.9rem; color:var(--text-main);">${escapeHtml(w.name)}</span>
1545                    </label>
1546                    <button class="mini-btn btn-delete-workspace" data-id="${safeWorkspaceId}" style="background:none; border:none; color:var(--text-muted, #888); padding:6px; cursor:pointer; display:inline-flex; align-items:center; justify-content:center; border-radius:4px;" title="Déconnecter" aria-label="Déconnecter">
1547                        <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg>
1548                    </button>
1549                </div>
1550                `;
1551            }).join('');
1552        }
1553
1554        const content = `
1555        <div class="notion-modal-config" style="display:flex; flex-direction:column; gap:16px; padding:10px 0;">
1556            <p style="font-size:0.9rem; color:var(--text-muted); margin:0;">
1557                Choisissez l'environnement Notion à afficher et à synchroniser avec vos projets ROXIMA.
1558            </p>
1559            
1560            <div class="notion-workspaces-list">
1561                ${workspacesHtml}
1562            </div>
1563            
1564            <div style="display:flex; justify-content:space-between; align-items:center; margin-top:16px; gap:10px;">
1565                <button id="modal-btn-add-notion-env" class="cal-btn" style="display:flex; align-items:center; gap:8px; padding:8px 14px; background:none; border:1px solid var(--border-color, #eee); border-radius:8px; font-size:0.85rem; font-weight:600; cursor:pointer; color:var(--text-main);">
1566                    <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>
1567                    <span>Ajouter un environnement</span>
1568                </button>
1569                ${workspaces.length > 0 ? `
1570                <button id="modal-btn-sync-active-notion" class="cal-btn" style="display:flex; align-items:center; gap:8px; padding:8px 16px; background:var(--text-main, #000); color:var(--bg-card, #fff); border:none; border-radius:8px; font-size:0.85rem; font-weight:600; cursor:pointer;">
1571                    <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21.5 2v6h-6M2.5 22v-6h6"></path><path d="M2 11.5a10 10 0 0 1 18.8-4.3L21.5 8M22 12.5a10 10 0 0 1-18.8 4.3L2.5 16"></path></svg>
1572                    <span>Synchroniser</span>
1573                </button>
1574                ` : ''}
1575            </div>
1576        </div>
1577        `;
1578
1579        ctx.bus.emit('ui:modal:open', {
1580            title: "Configuration Notion",
1581            content: content
1582        });
1583
1584        const modalContainer = document.querySelector('.modal');
1585        if (modalContainer) {
1586            modalContainer.querySelectorAll('input[name="notion-workspace-radio"]').forEach(radio => {
1587                radio.addEventListener('change', (e) => {
1588                    const wsId = e.target.value;
1589                    setActiveNotionWorkspace(wsId);
1590                    render();
1591                    showNotionConfigModal();
1592                });
1593            });
1594
1595            modalContainer.querySelectorAll('.btn-delete-workspace').forEach(btn => {
1596                btn.addEventListener('click', async (e) => {
1597                    e.stopPropagation();
1598                    const wsId = e.currentTarget.dataset.id;
1599                    const ws = workspaces.find(w => w.id === wsId);
1600                    if (ws && confirm(`Déconnecter l'environnement Notion "${ws.name}" ?`)) {
1601                        try {
1602                            await removeNotionWorkspace(wsId);
1603                            render();
1604                            showNotionConfigModal();
1605                        } catch (error) {
1606                            console.error('Erreur deconnexion Notion :', error);
1607                            window.Roxima?.showToast?.(error?.message || 'Impossible de deconnecter Notion.');
1608                        }
1609                    }
1610                });
1611            });
1612
1613            const btnAddEnv = modalContainer.querySelector('#modal-btn-add-notion-env');
1614            if (btnAddEnv) {
1615                btnAddEnv.addEventListener('click', async () => {
1616                    if (notionOAuthStartPending) return;
1617                    notionOAuthStartPending = true;
1618                    btnAddEnv.disabled = true;
1619                    btnAddEnv.setAttribute('aria-busy', 'true');
1620                    const closeBtn = document.querySelector('#modal-close-btn');
1621                    if (closeBtn) closeBtn.click();
1622                    try {
1623                        await startNotionOAuth({ supabaseClient: supabase });
1624                    } catch (error) {
1625                        console.error('Erreur demarrage OAuth Notion :', error);
1626                        window.Roxima?.showToast?.(error?.message || 'Impossible de connecter Notion.');
1627                    } finally {
1628                        notionOAuthStartPending = false;
1629                        if (btnAddEnv.isConnected) {
1630                            btnAddEnv.disabled = false;
1631                            btnAddEnv.removeAttribute('aria-busy');
1632                        }
1633                    }
1634                });
1635            }
1636
1637            const btnSync = modalContainer.querySelector('#modal-btn-sync-active-notion');
1638            if (btnSync) {
1639                btnSync.addEventListener('click', async () => {
1640                    const closeBtn = document.querySelector('#modal-close-btn');
1641                    if (closeBtn) closeBtn.click();
1642
1643                    const activeWorkspace = getActiveNotionWorkspace();
1644                    if (activeWorkspace) {
1645                        await syncNotion(activeWorkspace);
1646                    }
1647                });
1648            }
1649        }
1650    }
1651
1652    const openMobileProjectActions = (blockId) => {
1653        document.activeElement?.blur();
1654        const pageId = selectedPageId;
1655        if (!pageId) return;
1656
1657        const block = store.getBlocks(pageId).find(item => String(item.id) === String(blockId));
1658        if (!block) return;
1659
1660        const text = cleanTaskText(block);
1661
1662        let priorityHtml = '';
1663        if (block.type === 'todo') {
1664            const currentPriority = normalizeGeneralPriority(block.generalPriority ?? block.general_priority ?? block.priority);
1665            priorityHtml = `
1666                <div class="mobile-general-priority" role="group" aria-label="${escapeAttribute(i18n.t('daily_priority_general'))}">
1667                    <p>${escapeHtml(i18n.t('daily_priority_general'))}</p>
1668                    ${Object.entries(GENERAL_PRIORITY_META).map(([value, meta]) => `
1669                        <button class="priority-flag-btn is-${value}" data-priority="${value}" type="button" aria-pressed="${currentPriority === value}">
1670                            <span aria-hidden="true">⚑</span>
1671                            <span>${meta.code === '—' ? escapeHtml(getGeneralPriorityLabel(meta)) : `${meta.code} · ${escapeHtml(getGeneralPriorityLabel(meta))}`}</span>
1672                        </button>
1673                    `).join('')}
1674                </div>
1675            `;
1676        }
1677
1678        ctx.bus.emit('ui:action-sheet:open', {
1679            title: text,
1680            subtitleKey: 'common_actions',
1681            customHtml: priorityHtml,
1682            actions: [
1683                ...(block.type === 'todo' && !block.checked ? [{
1684                    icon: isBlockInToday(block) ? '✓' : '☀',
1685                    labelKey: isBlockInToday(block) ? 'daily_priority_already_selected' : 'daily_priority_add_action',
1686                    detailKey: isBlockInToday(block) ? 'daily_priority_already_selected_detail' : 'daily_priority_max_tasks_detail',
1687                    handler: () => addBlockToToday(block.id)
1688                }] : []),
1689                {
1690                    icon: '🎯',
1691                    labelKey: 'projects_menu_focus',
1692                    handler: () => ctx.bus.emit('app:push-task', { blockId: block.id, blockText: text })
1693                },
1694                {
1695                    icon: '→',
1696                    labelKey: 'projects_action_send_todo',
1697                    detailKey: 'projects_action_send_todo_detail',
1698                    handler: () => {
1699                        const project = store.getPage(pageId);
1700                        ctx.bus.emit('quick:add', { text, sourceId: block.id, projectId: pageId, projectName: project ? store.getPagePath(project.id) : '', projectIcon: project ? project.icon : '' });
1701                        ctx.bus.emit('app:show-panel', { panelId: 'region-todo' });
1702                        ctx.bus.emit('ui:toast', i18n.t('projects_toast_sent_todo'));
1703                    }
1704                },
1705                {
1706                    icon: '↗',
1707                    labelKey: 'projects_menu_plan',
1708                    handler: () => {
1709                        const resolvedTask = resolveTaskReference(block.id);
1710                        ctx.bus.emit('calendar:plan-task', {
1711                            text,
1712                            sourceId: resolvedTask?.taskId || resolvedTask?.task_id || block.notionBlockId || block.id,
1713                            durationMinutes: Number(block.estimatedMinutes || block.estimated_minutes || 25)
1714                        });
1715                    }
1716                },
1717                {
1718                    icon: '✎',
1719                    labelKey: 'common_edit',
1720                    handler: () => focusBlock(block.id, 'end')
1721                },
1722                {
1723                    icon: '×',
1724                    labelKey: 'common_delete',
1725                    danger: true,
1726                    handler: () => {
1727                        store.deleteBlock(pageId, block.id);
1728                        render();
1729                        if (ctx.bus) ctx.bus.emit('objectifs:refresh');
1730                    }
1731                }
1732            ]
1733        });
1734
1735        if (block.type === 'todo') {
1736            setTimeout(() => {
1737                const modalEl = document.getElementById('modal-layer');
1738                if (modalEl) {
1739                    modalEl.querySelectorAll('.priority-flag-btn').forEach(btn => {
1740                        const btnPriority = btn.dataset.priority;
1741                        if (btnPriority === normalizeGeneralPriority(block.generalPriority ?? block.general_priority ?? block.priority)) {
1742                            btn.classList.add('active');
1743                        }
1744                        
1745                        btn.addEventListener('click', async () => {
1746                            try {
1747                                await setTaskGeneralPriority(block.id, btnPriority);
1748                                if (ctx.bus) ctx.bus.emit('objectifs:refresh');
1749                                document.getElementById('modal-close-btn')?.click();
1750                                render();
1751                            } catch (error) {
1752                                console.error('Erreur priorité générale :', error);
1753                                showDailyToast(i18n.t('daily_priority_change_error'));
1754                            }
1755                        });
1756                    });
1757                }
1758            }, 80);
1759        }
1760    };
1761
1762    // Expose Bridge and Handlers
1763    window.Roxima = window.Roxima || {};
1764
1765    // Global handler for direct access
1766    window.Roxima.handlePlusClick = (e, blockId) => {
1767        if (e) e.stopPropagation();
1768        if (!checkAuth()) return; // Guard Plus Menu
1769
1770        const btn = e.target.closest('.block-plus'); // Ensure we get the div if clicked on text
1771        if (btn) {
1772            console.log("Direct Plus Click:", blockId);
1773            openMenu(btn, blockId); // Unified openMenu
1774        }
1775    };
1776
1777
1778    // Global Mousedown to handle Menu Interaction preventing Blur
1779    if (!window.Roxima._menuMousedownListenerAdded) {
1780        document.addEventListener('mousedown', (e) => {
1781            const menuItem = e.target.closest('.slash-item');
1782            if (menuItem) {
1783                e.preventDefault(); // Prevent input blur
1784                e.stopPropagation();
1785                const type = menuItem.dataset.type;
1786                if (isPlusMenuMode) {
1787                    handlePlusAction(type);
1788                } else {
1789                    applySlashType(selectedPageId, type);
1790                }
1791            }
1792        });
1793        window.Roxima._menuMousedownListenerAdded = true;
1794    }
1795
1796    function handleContainerClick(e) {
1797        // 1. Menu Item Click (Handled by Mousedown mainly, but keep for fallback if needed?)
1798        // If mousedown handled it, we don't need it here.
1799        if (e.target.closest('.slash-item')) return;
1800
1801        const btnAddFirstTask = e.target.closest('#btn-add-first-task');
1802        if (btnAddFirstTask) {
1803            e.preventDefault();
1804            if (!checkAuth() || !selectedPageId) return;
1805
1806            const firstTask = store.addBlock(selectedPageId, { type: 'todo', text: '' });
1807            firstFocusActivationTaskId = firstTask.id;
1808            render();
1809            window.setTimeout(() => {
1810                const taskInput = Array.from(container.querySelectorAll('.editable-text'))
1811                    .find(input => input.dataset.id === firstTask.id);
1812                taskInput?.focus();
1813            }, 50);
1814            return;
1815        }
1816
1817        // 1.5 Clear Selection
1818        if (!e.target.closest('.editor-block') && !e.target.closest('.block-handle') && !e.target.closest('.block-plus')) {
1819            if (selection.ids.size > 0) selection.clear();
1820        }
1821
1822        // 2. Empty State / Bottom Trigger
1823        if (e.target.closest('.editor-empty-state') ||
1824            e.target.closest('#bottom-add-trigger') ||
1825            e.target.id === 'editor-scroller' ||
1826            e.target.id === 'editor-blocks') {
1827
1828            if (!checkAuth()) return; // Guard
1829
1830            store.addBlock(selectedPageId, { type: 'p', text: '' });
1831            render();
1832            setTimeout(() => {
1833                const inputs = container.querySelectorAll('.editable-text');
1834                if (inputs.length) inputs[inputs.length - 1].focus();
1835            }, 50);
1836            return;
1837        }
1838
1839        // 3. Toggle Sidebar
1840        const toggleBtn = e.target.closest('#btn-toggle-sidebar');
1841        if (toggleBtn) {
1842            const layoutEl = container.querySelector('.obj-layout');
1843            if (layoutEl) {
1844                const isVisible = layoutEl.classList.contains('show-sidebar') || 
1845                                  (!layoutEl.classList.contains('hide-sidebar') && !!container.closest('.card-expanded'));
1846                if (isVisible) {
1847                    layoutEl.classList.remove('show-sidebar');
1848                    layoutEl.classList.add('hide-sidebar');
1849                    sidebarForcedState = 'closed';
1850                    if (toggleBtn) {
1851                        toggleBtn.innerHTML = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" focusable="false"><line x1="3" y1="12" x2="21" y2="12"></line><line x1="3" y1="6" x2="21" y2="6"></line><line x1="3" y1="18" x2="21" y2="18"></line></svg>`;
1852                    }
1853                } else {
1854                    layoutEl.classList.remove('hide-sidebar');
1855                    layoutEl.classList.add('show-sidebar');
1856                    sidebarForcedState = 'open';
1857                    if (toggleBtn) {
1858                        toggleBtn.innerHTML = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" focusable="false"><polyline points="15 18 9 12 15 6"></polyline></svg>`;
1859                    }
1860                }
1861            }
1862            return;
1863        }
1864
1865        // 4. Block Handle Click -> Context Menu
1866        const mobileTaskAction = e.target.closest('.mobile-task-action');
1867        if (mobileTaskAction) {
1868            e.preventDefault();
1869            e.stopPropagation();
1870            if (!checkAuth()) return;
1871            openMobileProjectActions(mobileTaskAction.dataset.id);
1872            return;
1873        }
1874
1875        // 4. Block Handle Click -> Context Menu
1876        const handle = e.target.closest('.block-handle');
1877        if (handle) {
1878            e.stopPropagation();
1879            if (!checkAuth()) return; // Guard
1880            const blockId = handle.closest('.editor-block').dataset.id;
1881            openContextMenu(handle, blockId);
1882            return;
1883        }
1884
1885        // The deletion logic is now handled in the modal confirmation click listener.
1886
1887        // 5. Sidebar Actions (Delete/Add) - Delegated from Container
1888        const btnDel = e.target.closest('.action-del');
1889        if (btnDel) {
1890            e.stopPropagation();
1891            if (!checkAuth()) return; // Guard
1892            const id = btnDel.dataset.id;
1893
1894            const isMulti = selectedPageIds.has(id) && selectedPageIds.size > 1;
1895            const hasSubProjects = store.getPages().some(p => p.parentId === id);
1896            
1897            const warningText = isMulti
1898                ? `Vous allez supprimer <b>${selectedPageIds.size} projets</b> et tous leurs sous-projets. Continuer ?`
1899                : (hasSubProjects
1900                    ? "Ce projet contient des sous-projets. Tu veux vraiment le supprimer ?"
1901                    : "Voulez-vous vraiment supprimer cette page et tout son contenu ?");
1902
1903            // Custom Modal for Confirmation
1904            ctx.bus.emit('ui:modal:open', {
1905                title: 'Supprimer la page ?',
1906                content: `
1907                    <div style="text-align:center; padding:10px;">
1908                        <p style="margin-bottom:20px;">${warningText}</p>
1909                        <div style="display:flex; justify-content:center; gap:10px;">
1910                            <button id="modal-btn-cancel-del" style="padding:8px 16px; border:1px solid #ddd; background:white; border-radius:6px; cursor:pointer;">Annuler</button>
1911                            <button id="modal-btn-confirm-del" style="padding:8px 16px; border:none; background:#ff4d4f; color:white; border-radius:6px; cursor:pointer;">Supprimer</button>
1912                        </div>
1913                    </div>
1914                `
1915            });
1916            
1917            setTimeout(() => {
1918                const btnCancel = document.getElementById('modal-btn-cancel-del');
1919                const btnConfirm = document.getElementById('modal-btn-confirm-del');
1920                
1921                if (btnCancel) {
1922                    btnCancel.addEventListener('click', () => {
1923                        document.getElementById('modal-close-btn')?.click();
1924                    });
1925                }
1926                
1927                if (btnConfirm) {
1928                    btnConfirm.addEventListener('click', () => {
1929                        if (selectedPageIds.has(id) && selectedPageIds.size > 1) {
1930                            const idsToDelete = Array.from(selectedPageIds);
1931                            idsToDelete.forEach(pId => store.deletePage(pId));
1932                            selectedPageIds.clear();
1933                            setSelectedPageId(null);
1934                        } else {
1935                            store.deletePage(id);
1936                            selectedPageIds.delete(id);
1937                            if (selectedPageId === id) setSelectedPageId(null);
1938                        }
1939                        render();
1940                        document.getElementById('modal-close-btn')?.click();
1941                    });
1942                }
1943            }, 10);
1944            return;
1945        }
1946
1947        const btnAddSub = e.target.closest('.action-add-sub');
1948        if (btnAddSub) {
1949            e.stopPropagation();
1950            if (!checkAuth()) return; // Guard
1951            const id = btnAddSub.dataset.id;
1952            const newPage = store.createPage(i18n.t('common_untitled'), id);
1953            expandedPages.add(id);
1954            setSelectedPageId(newPage.id);
1955            selectedPageIds.clear();
1956            selectedPageIds.add(selectedPageId);
1957            render();
1958            return;
1959        }
1960
1961        const toggleBtnFolder = e.target.closest('.toggle-folder-btn');
1962        if (toggleBtnFolder) {
1963            e.stopPropagation();
1964            const id = toggleBtnFolder.dataset.id;
1965            if (expandedPages.has(id)) {
1966                expandedPages.delete(id);
1967            } else {
1968                expandedPages.add(id);
1969            }
1970            render();
1971            return;
1972        }
1973
1974
1975        // 5.5 NEW: Sidebar Main Add Button
1976        const btnAddPage = e.target.closest('#btn-add-page') || e.target.closest('#btn-add-page-empty');
1977        if (btnAddPage) {
1978            if (!checkAuth()) return; // Guard
1979            const newPage = store.createPage(i18n.t('common_untitled'), null);
1980            setSelectedPageId(newPage.id);
1981            render();
1982            window.setTimeout(() => {
1983                const titleInput = container.querySelector('.page-title-editor');
1984                if (!titleInput || titleInput.dataset.id !== newPage.id) return;
1985                titleInput.focus();
1986                titleInput.select();
1987            }, 50);
1988            return;
1989        }
1990
1991        // Select Page (Last priority in sidebar)
1992        const pageItem = e.target.closest('.page-item');
1993        if (pageItem) {
1994            // Avoid selecting if we clicked an action button (already handled, but good to be safe)
1995            if (!e.target.closest('.mini-btn')) {
1996                const id = pageItem.dataset.id;
1997
1998                if (e.ctrlKey || e.metaKey) {
1999                    if (selectedPageIds.has(id)) {
2000                        selectedPageIds.delete(id);
2001                        if (selectedPageId === id) {
2002                            setSelectedPageId(selectedPageIds.size > 0 ? Array.from(selectedPageIds).pop() : null);
2003                        }
2004                    } else {
2005                        selectedPageIds.add(id);
2006                        setSelectedPageId(id);
2007                    }
2008                } else if (e.shiftKey && selectedPageId) {
2009                    const allItems = Array.from(document.querySelectorAll('.page-item'));
2010                    const startIdx = allItems.findIndex(el => el.dataset.id === selectedPageId);
2011                    const endIdx = allItems.findIndex(el => el.dataset.id === id);
2012                    if (startIdx !== -1 && endIdx !== -1) {
2013                        const min = Math.min(startIdx, endIdx);
2014                        const max = Math.max(startIdx, endIdx);
2015                        selectedPageIds.clear();
2016                        for (let i = min; i <= max; i++) {
2017                            selectedPageIds.add(allItems[i].dataset.id);
2018                        }
2019                        setSelectedPageId(id);
2020                    }
2021                } else {
2022                    selectedPageIds.clear();
2023                    selectedPageIds.add(id);
2024                    setSelectedPageId(id);
2025                }
2026                render();
2027            }
2028            return;
2029        }
2030
2031        // 6. Close Menus on outside click
2032        if (slashMenuOpen) {
2033            closeSlashMenu();
2034        }
2035    }
2036
2037    // Attach global listener once to container
2038    container.removeEventListener('click', handleContainerClick);
2039    container.addEventListener('click', handleContainerClick);
2040
2041    // Attach global listener to document for menus (since they are now on body)
2042    if (!window.Roxima._menuClickListenerAdded) {
2043        document.addEventListener('click', (e) => {
2044            const slashItem = e.target.closest('.slash-item');
2045            if (slashItem) {
2046                e.stopPropagation();
2047                const type = slashItem.dataset.type;
2048                if (isPlusMenuMode) {
2049                    handlePlusAction(type);
2050                } else {
2051                    applySlashType(selectedPageId, type);
2052                }
2053            }
2054        });
2055        window.Roxima._menuClickListenerAdded = true;
2056    }
2057
2058    // Checkbox Change Listener (for Todo sync)
2059    container.addEventListener('change', (e) => {
2060        if (e.target.classList.contains('todo-check')) {
2061            const blockEl = e.target.closest('.editor-block');
2062            const blockId = blockEl.dataset.id;
2063            const checked = e.target.checked;
2064            store.updateBlock(selectedPageId, blockId, { checked });
2065
2066            // Visual update
2067            const textEl = blockEl.querySelector('.editable-text');
2068            if (checked) textEl.classList.add('checked-text');
2069            else textEl.classList.remove('checked-text');
2070
2071            // Emit Update Signal
2072            if (ctx.bus) ctx.bus.emit('objectifs:refresh');
2073        }
2074    });
2075
2076    function saveProjectScrollState() {
2077        const saved = [];
2078        if (!container) return saved;
2079        const selectors = ['.obj-content', '.obj-sidebar', '.v2-section-content', '.region-projects', '.editor-block-list'];
2080        selectors.forEach(sel => {
2081            const el = container.querySelector(sel) || container.closest(sel);
2082            if (el && (el.scrollTop > 0 || el.scrollLeft > 0)) {
2083                saved.push({ selector: sel, top: el.scrollTop, left: el.scrollLeft });
2084            }
2085        });
2086        return saved;
2087    }
2088
2089    function restoreProjectScrollState(saved) {
2090        if (!saved || !saved.length || !container) return;
2091        saved.forEach(item => {
2092            const el = container.querySelector(item.selector) || container.closest(item.selector);
2093            if (el) {
2094                el.scrollTop = item.top;
2095                el.scrollLeft = item.left;
2096            }
2097        });
2098    }
2099
2100    function findActivationTask(taskId) {
2101        const normalizedTaskId = String(taskId || '');
2102        for (const page of store.getPages()) {
2103            const block = store.getBlocks(page.id).find((item) => String(item.id) === normalizedTaskId);
2104            if (block) return { page, block };
2105        }
2106        return null;
2107    }
2108
2109    function ensureActivationDraftAction({ actionText, pageId, taskId }) {
2110        const existing = findActivationTask(taskId);
2111        if (existing) {
2112            if (String(existing.block.text || '').trim() !== actionText) {
2113                throw new Error('ACTIVATION_TASK_ID_CONFLICT');
2114            }
2115            return { pageId: existing.page.id, taskId: existing.block.id };
2116        }
2117
2118        let page = pageId ? store.getPage(pageId) : null;
2119        if (!page) {
2120            page = store.getPages().find((candidate) => (
2121                String(candidate?._activationDraftTaskId || '') === String(taskId)
2122            )) || null;
2123        }
2124        if (!page) {
2125            const reusableEmptyPage = store.getPages().find((candidate) => (
2126                !candidate.isNotion
2127                && candidate.source !== 'notion'
2128                && String(candidate.title || candidate.name || '') === i18n.t('project_activation_default_project')
2129                && store.getBlocks(candidate.id).length === 0
2130            ));
2131            page = reusableEmptyPage || store.createPage(i18n.t('project_activation_default_project'), null);
2132        }
2133
2134        if (String(page._activationDraftTaskId || '') !== String(taskId)) {
2135            page._activationDraftTaskId = taskId;
2136            store.save();
2137        }
2138        const task = store.addBlock(page.id, { id: taskId, type: 'todo', text: actionText });
2139        return { pageId: page.id, taskId: task.id };
2140    }
2141
2142    function hasResumableActivationDraftClaim(ownerId, actionText) {
2143        const claim = readActivationDraftConsumption(window.sessionStorage);
2144        return Boolean(
2145            claim
2146            && claim.ownerId === String(ownerId || '')
2147            && claim.actionText === actionText
2148        );
2149    }
2150
2151    async function consumeAuthenticatedActivationDraft(ownerId = ctx.currentUser?.id) {
2152        const normalizedOwnerId = String(ownerId || '').trim();
2153        const actionText = readActivationDraft(window.sessionStorage);
2154        if (!normalizedOwnerId || !actionText) return { status: 'no-draft' };
2155
2156        const manualPages = store.getPages().filter((page) => !page.isNotion && page.source !== 'notion');
2157        if (manualPages.length > 0 && !hasResumableActivationDraftClaim(normalizedOwnerId, actionText)) {
2158            return { status: 'existing-account' };
2159        }
2160
2161        activationDraftConsumptionPending = true;
2162        const activationForm = container.querySelector('#project-activation-form');
2163        activationForm?.setAttribute('aria-busy', 'true');
2164        activationForm?.querySelectorAll('input, button').forEach((element) => {
2165            element.disabled = true;
2166        });
2167
2168        try {
2169            return await consumeActivationDraft({
2170                storage: window.sessionStorage,
2171                ownerId: normalizedOwnerId,
2172                ensureAction: ensureActivationDraftAction,
2173                addToToday: ({ taskId }) => (
2174                    isBlockInToday({ id: taskId })
2175                        ? { status: 'already-added' }
2176                        : addBlockToToday(taskId)
2177                ),
2178                openFocus: ({ actionText: savedAction, pageId, taskId }) => {
2179                    setSelectedPageId(pageId);
2180                    selectedPageIds.clear();
2181                    selectedPageIds.add(pageId);
2182                    firstFocusActivationTaskId = null;
2183                    projectActivationDraft.title = '';
2184                    projectActivationDraft.helpExpanded = false;
2185                    projectActivationDraft.selectedExampleIndex = null;
2186                    render();
2187                    ctx.bus?.emit('app:push-task', {
2188                        blockId: taskId,
2189                        blockText: savedAction
2190                    });
2191                }
2192            });
2193        } catch (error) {
2194            const claim = readActivationDraftConsumption(window.sessionStorage);
2195            firstFocusActivationTaskId = claim?.taskId || firstFocusActivationTaskId;
2196            console.warn('Impossible de terminer automatiquement la première action après inscription.', error);
2197            render();
2198            return { status: 'incomplete', error };
2199        } finally {
2200            activationDraftConsumptionPending = false;
2201        }
2202    }
2203
2204    function captureFocusedBlockCaret() {
2205        const editor = container.querySelector('.editable-text:focus');
2206        if (!editor?.dataset.id) return null;
2207
2208        const selection = window.getSelection();
2209        let offset = editor.innerText.length;
2210        if (selection?.rangeCount) {
2211            const range = selection.getRangeAt(0);
2212            if (editor.contains(range.endContainer)) {
2213                const beforeCaret = range.cloneRange();
2214                beforeCaret.selectNodeContents(editor);
2215                beforeCaret.setEnd(range.endContainer, range.endOffset);
2216                offset = beforeCaret.toString().length;
2217            }
2218        }
2219
2220        return { blockId: editor.dataset.id, offset };
2221    }
2222
2223    function restoreFocusedBlockCaret(savedCaret) {
2224        if (!savedCaret) return;
2225        focusBlock(savedCaret.blockId, savedCaret.offset);
2226    }
2227
2228    // -- RENDER FUNCTION --
2229    function render(options) {
2230        const savedCaret = options?.preserveCaret === false
2231            ? null
2232            : captureFocusedBlockCaret();
2233        const savedScrolls = saveProjectScrollState();
2234        closeTaskPriorityMenu(false);
2235        selectedPageId = localStorage.getItem('roxima_selected_project_page_id') || null;
2236        if (selectionPageId !== selectedPageId) {
2237            selection.ids.clear();
2238            selection.anchorBlockId = null;
2239            selection.extentBlockId = null;
2240            selectionPageId = selectedPageId;
2241        }
2242        const activeWorkspaceId = getActiveNotionWorkspaceId();
2243        const pages = store.getPages().filter(p => {
2244            if ((p.source === 'manual' || !p.source) && !p.isNotion) return true;
2245            if ((p.source === 'notion' || p.isNotion) && p.notionWorkspaceId === activeWorkspaceId) return true;
2246            return false;
2247        });
2248
2249        // Resolve orphans: if page parentId is not in the filtered pages list, treat parentId as null
2250        const pageIds = new Set(pages.map(p => p.id));
2251        const resolvedPages = pages.map(p => {
2252            if (p.parentId && !pageIds.has(p.parentId)) {
2253                return { ...p, parentId: null };
2254            }
2255            return p;
2256        });
2257
2258        if (!selectedPageId && resolvedPages.length > 0) {
2259            setSelectedPageId(resolvedPages[0].id);
2260            selectedPageIds.add(selectedPageId);
2261        } else if (selectedPageId && !pageIds.has(selectedPageId)) {
2262            setSelectedPageId(resolvedPages.length > 0 ? resolvedPages[0].id : null);
2263            if (selectedPageId) selectedPageIds.add(selectedPageId);
2264        }
2265
2266        if (selectedPageId) {
2267            expandedPages.add(selectedPageId);
2268            let curr = store.getPage(selectedPageId);
2269            while (curr && curr.parentId) {
2270                expandedPages.add(curr.parentId);
2271                curr = store.getPage(curr.parentId);
2272            }
2273        }
2274
2275        const currentPage = selectedPageId ? store.getPage(selectedPageId) : null;
2276        const blocks = currentPage ? store.getBlocks(currentPage.id) : [];
2277        const activationStep = getProjectActivationStep({
2278            projectCount: resolvedPages.length,
2279            hasCurrentPage: Boolean(currentPage),
2280            blockCount: blocks.length
2281        });
2282        if (activationStep === PROJECT_ACTIVATION_STEP.CREATE_PROJECT && !projectActivationDraft.title) {
2283            projectActivationDraft.title = readActivationDraft(window.sessionStorage);
2284        }
2285
2286        // Determine Layout Classes
2287        let layoutClasses = 'obj-layout';
2288        if (sidebarForcedState === 'open') layoutClasses += ' show-sidebar';
2289        if (sidebarForcedState === 'closed') layoutClasses += ' hide-sidebar';
2290        const isSidebarOpen = sidebarForcedState === 'open' || (sidebarForcedState !== 'closed' && Boolean(container.closest('.card-expanded')));
2291
2292        const activeWorkspace = getActiveNotionWorkspace();
2293        const isNotionConnected = Boolean(activeWorkspace && !isDemoNotionWorkspace(activeWorkspace));
2294        const notionBtnTitle = isNotionConnected
2295            ? translateProject('projects_notion_connected', { workspace: activeWorkspace.name })
2296            : i18n.t('projects_notion_disconnected');
2297
2298        // Main Template
2299        container.innerHTML = `
2300            <div class="card" style="height: 100%; display: flex; flex-direction: column; position: relative;">
2301                <div class="card-header" style="display: flex; align-items: center; justify-content: space-between; gap: 12px;">
2302                    <div style="display: flex; align-items: center; gap: 8px; flex: 1; min-width: 0;">
2303                        <button class="btn-toggle-sidebar" id="bt
2303n-toggle-sidebar" type="button" data-i18n-title="projects_menu" data-i18n-aria-label="projects_menu" title="${escapeAttribute(i18n.t('projects_menu'))}" aria-label="${escapeAttribute(i18n.t('projects_menu'))}" style="margin: 0; flex-shrink: 0;">
2304                            ${isSidebarOpen
2305                                ? `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" focusable="false"><polyline points="15 18 9 12 15 6"></polyline></svg>`
2306                                : `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" focusable="false"><line x1="3" y1="12" x2="21" y2="12"></line><line x1="3" y1="6" x2="21" y2="6"></line><line x1="3" y1="18" x2="21" y2="18"></line></svg>`
2307                            }
2308                        </button>
2309                        <h2 class="card-title" data-i18n="projects_title" style="margin: 0; flex-shrink: 0;">Projects</h2>
2310                        ${currentPage ? `
2311                            <div class="project-header-inline" style="display: flex; align-items: center; gap: 6px; flex: 1; min-width: 0; margin-left: 6px; padding-left: 4px;">
2312                                <button id="header-icon-btn" type="button" data-i18n-title="projects_change_icon" data-i18n-aria-label="projects_change_icon" title="${escapeAttribute(i18n.t('projects_change_icon'))}" aria-label="${escapeAttribute(i18n.t('projects_change_icon'))}" style="font-size:1.4rem; background:none; border:none; cursor:pointer; display:flex; align-items:center; justify-content:center; border-radius:4px; width:30px; height:30px; flex-shrink:0;">
2313                                    ${escapeHtml(currentPage.icon || '📄')}
2314                                </button>
2315                                <input type="text" class="page-title-editor" value="${escapeAttribute(currentPage.title)}" data-id="${escapeAttribute(currentPage.id)}" placeholder="Sans titre" data-i18n-placeholder="common_untitled" style="flex: 1; min-width: 0; font-size: 1.15rem; font-weight: 700; color: var(--text-main);">
2316                                ${currentPage.isNotion ? `<span class="notion-header-badge" style="background: #000; padding: 4px 6px; border-radius: 4px; flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center;" title="${escapeAttribute(i18n.t('projects_notion_page'))}"><img src="assets/img/Logo_Notion.png" width="12" height="12" style="filter: invert(1);"></span>` : ''}
2317                            </div>
2318                        ` : ''}
2319                    </div>
2320                    <button class="icon-btn project-mobile-actions-toggle" id="btn-project-actions-mobile" type="button"
2321                            aria-expanded="false" aria-controls="project-header-actions"
2322                            data-i18n-title="common_actions" data-i18n-aria-label="common_actions"
2323                            title="${escapeAttribute(i18n.t('common_actions'))}" aria-label="${escapeAttribute(i18n.t('common_actions'))}">
2324                        <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true" focusable="false">
2325                            <circle cx="12" cy="5" r="1"></circle><circle cx="12" cy="12" r="1"></circle><circle cx="12" cy="19" r="1"></circle>
2326                        </svg>
2327                    </button>
2328                    <div class="card-actions project-header-actions" id="project-header-actions">
2329                        <button class="icon-btn" id="btn-sync-notion" type="button" title="${escapeAttribute(notionBtnTitle)}" aria-label="${escapeAttribute(notionBtnTitle)}" style="position: relative; display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; padding: 0;">
2330                            <img src="assets/img/Logo_Notion.png" width="16" height="16" alt="Notion" style="filter: drop-shadow(0 1px 1px rgba(0,0,0,0.15));">
2331                            <span class="notion-status-dot" style="position: absolute; top: 3px; right: 3px; width: 7px; height: 7px; border-radius: 50%; background: ${isNotionConnected ? '#22c55e' : '#ef4444'}; box-shadow: 0 0 5px ${isNotionConnected ? 'rgba(34, 197, 94, 0.7)' : 'rgba(239, 68, 68, 0.7)'}; border: 1.5px solid var(--bg-card, #fff);"></span>
2332                        </button>
2333                        ${currentPage ? `
2334                        <button class="icon-btn btn-ai-planner" id="bt
2334n-ai-planner" type="button" data-i18n-title="projects_ai_planner" data-i18n-aria-label="projects_ai_planner" title="${escapeAttribute(i18n.t('projects_ai_planner'))}" aria-label="${escapeAttribute(i18n.t('projects_ai_planner'))}">
2335                            <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-sparkles"><path d="m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z"/><path d="m5 3 1 2.5L8.5 6 6 7 5 9.5 4 7 1.5 6 4 5.5z"/><path d="m19 17 1 2.5 2.5.5-2.5 1-1 2.5-1-2.5-2.5-1 2.5-1z"/></svg>
2336                        </button>
2337                        ` : ''}
2338                        <button class="icon-btn" id="btn-paste-tasks" type="button" data-i18n-title="projects_paste_tasks" data-i18n-aria-label="projects_paste_tasks" title="${escapeAttribute(i18n.t('projects_paste_tasks'))}" aria-label="${escapeAttribute(i18n.t('projects_paste_tasks'))}">
2339                            <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>
2340                        </button>
2341                        <button class="icon-btn" id="btn-zoom" type="button" data-i18n-title="projects_expand" data-i18n-aria-label="projects_expand" title="${escapeAttribute(i18n.t('projects_expand'))}" aria-label="${escapeAttribute(i18n.t('projects_expand'))}"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" focusable="false"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" y1="3" x2="14" y2="10"></line><line x1="3" y1="21" x2="10" y2="14"></line></svg></button>
2342                    </div>
2343                </div>
2344                ${renderPlanningSessionPanel()}
2345                
2346                <div class="${layoutClasses}">
2347                    <!-- SIDEBAR -->
2348                    <div class="obj-sidebar">
2349                        <div class="obj-actions">
2350                            <button class="obj-btn-add" id="btn-add-page">
2351                                <span style="font-size:1.2em; line-height:1;">+</span> <span data-i18n="common_project">Project</span>
2352                            </button>
2353                        </div>
2354                        <div class="obj-tree" id="page-tree"></div>
2355                    </div>
2356
2357                    <!-- CONTENT PAGE / EDITOR -->
2358                    <div class="obj-content" id="editor-scroller">
2359                        ${currentPage ? `
2360                            <div class="editor-container" id="editor-blocks" role="listbox" aria-multiselectable="true" aria-label="${escapeAttribute(i18n.t('projects_blocks_aria'))}">
2361                                ${activationStep === PROJECT_ACTIVATION_STEP.ADD_FIRST_TASK
2362                                    ? renderFirstTaskActivation()
2363                                    : renderBlocks(blocks)}
2364                            </div>
2365
2366                        ` : renderProjectCreationActivation(projectActivationDraft)}
2367                    </div>
2368                </div>
2369                ${renderDailyPrioritiesPanel()}
2370                <!-- Floating Selection Action Bar -->
2371                <div class="selection-action-bar" id="project-selection-bar">
2372                    <span class="selection-count">0 ${i18n.t('selection_items_selected')}</span>
2373                    <button class="selection-btn btn-copy" type="button">
2374                        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>
2375                        <span data-i18n="common_copy">${i18n.t('common_copy')}</span>
2376                    </button>
2377                    <button class="selection-btn btn-paste" type="button" disabled>
2378                        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2"></path><rect x="9" y="3" width="6" height="4" rx="1"></rect></svg>
2379                        <span data-i18n="common_paste">${i18n.t('common_paste')}</span>
2380                    </button>
2381                    <button class="selection-btn btn-duplicate" type="button">
2382                        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="8" y="8" width="12" height="12" rx="2"></rect><path d="M16 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2"></path></svg>
2383                        <span data-i18n="common_duplicate">${i18n.t('common_duplicate')}</span>
2384                    </button>
2385                    <button class="selection-btn danger-btn btn-delete" type="button">
2386                        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></svg>
2387                        <span data-i18n="common_delete">${i18n.t('common_delete')}</span>
2388                    </button>
2389                    <button class="selection-btn cancel-btn btn-cancel" type="button" data-i18n-title="common_close" data-i18n-aria-label="common_close" title="${escapeAttribute(i18n.t('common_close'))}" aria-label="${escapeAttribute(i18n.t('common_close'))}">
2390                        <span aria-hidden="true">&times;</span>
2391                    </button>
2392                </div>
2393            </div>
2394        `;
2395
2396        container.querySelector('[data-planning-session-finish]')?.addEventListener('click', () => {
2397            completePlanningSession('confirmed');
2398        });
2399        updatePlanningSessionUi();
2400
2401        const activationForm = container.querySelector('#project-activation-form');
2402        if (activationForm) {
2403            const helpToggle = container.querySelector('#project-activation-help-toggle');
2404            const helpPanel = container.querySelector('#project-activation-help');
2405            const titleInput = activationForm.querySelector('#project-activation-title');
2406
2407            helpToggle?.addEventListener('click', () => {
2408                const expanded = helpToggle.getAttribute('aria-expanded') === 'true';
2409                projectActivationDraft.helpExpanded = !expanded;
2410                helpToggle.setAttribute('aria-expanded', String(!expanded));
2411                if (helpPanel) helpPanel.hidden = expanded;
2412            });
2413
2414            titleInput?.addEventListener('input', () => {
2415                projectActivationDraft.title = titleInput.value;
2416                const selectedButton = projectActivationDraft.selectedExampleIndex
2417                    ? container.querySelector(`[data-project-example-index="${projectActivationDraft.selectedExampleIndex}"]`)
2418                    : null;
2419                if (selectedButton?.dataset.projectExampleTitle !== titleInput.value) {
2420                    projectActivationDraft.selectedExampleIndex = null;
2421                    container.querySelectorAll('[data-project-example-title]').forEach((button) => {
2422                        button.classList.remove('is-selected');
2423                        button.setAttribute('aria-pressed', 'false');
2424                    });
2425                }
2426            });
2427
2428            container.querySelectorAll('[data-project-example-title]').forEach((exampleButton) => {
2429                exampleButton.addEventListener('click', () => {
2430                    const exampleTitle = String(exampleButton.dataset.projectExampleTitle || '').trim();
2431                    if (!exampleTitle || !titleInput) return;
2432
2433                    projectActivationDraft.title = exampleTitle;
2434                    projectActivationDraft.selectedExampleIndex = Number(exampleButton.dataset.projectExampleIndex) || null;
2435                    titleInput.value = exampleTitle;
2436                    titleInput.dispatchEvent(new Event('input', { bubbles: true }));
2437                    container.querySelectorAll('[data-project-example-title]').forEach((button) => {
2438                        const selected = button === exampleButton;
2439                        button.classList.toggle('is-selected', selected);
2440                        button.setAttribute('aria-pressed', String(selected));
2441                    });
2442                    titleInput.focus();
2443                });
2444            });
2445
2446            activationForm.addEventListener('submit', async (event) => {
2447                event.preventDefault();
2448                if (!checkAuth()) return;
2449
2450                if (readActivationDraft(window.sessionStorage) && ctx.currentUser?.id) {
2451                    await consumeAuthenticatedActivationDraft(ctx.currentUser.id);
2452                    return;
2453                }
2454                if (activationDraftConsumptionPending) return;
2455
2456                const actionText = String(titleInput?.value || '').trim();
2457                if (!actionText) {
2458                    titleInput?.focus();
2459                    return;
2460                }
2461
2462                const submitButton = activationForm.querySelector('button[type="submit"]');
2463                if (submitButton?.disabled) return;
2464                if (submitButton) submitButton.disabled = true;
2465
2466                const newPage = store.createPage(i18n.t('project_activation_default_project'), null);
2467                const firstTask = store.addBlock(newPage.id, { type: 'todo', text: actionText });
2468                clearActivationDraft(window.sessionStorage);
2469                firstFocusActivationTaskId = firstTask.id;
2470                projectActivationDraft.title = '';
2471                projectActivationDraft.helpExpanded = false;
2472                projectActivationDraft.selectedExampleIndex = null;
2473                setSelectedPageId(newPage.id);
2474                selectedPageIds.clear();
2475                selectedPageIds.add(newPage.id);
2476
2477                const addedToToday = await addBlockToToday(firstTask.id);
2478                if (addedToToday) {
2479                    firstFocusActivationTaskId = null;
2480                    render();
2481                    window.setTimeout(() => {
2482                        ctx.bus?.emit('app:push-task', {
2483                            blockId: firstTask.id,
2484                            blockText: actionText
2485                        });
2486                    }, 0);
2487                    return;
2488                }
2489
2490                // The action is already saved even if Today cannot be updated.
2491                // Keep the existing recovery CTA visible instead of losing work.
2492                render();
2493                window.setTimeout(() => {
2494                    const taskInput = Array.from(container.querySelectorAll('.editable-text'))
2495                        .find(input => input.dataset.id === firstTask.id);
2496                    taskInput?.focus();
2497                }, 50);
2498            });
2499        }
2500
2501        renderTree(container.querySelector('#page-tree'), resolvedPages, null);
2502
2503        // Ensure global menus exist on body
2504        if (!document.getElementById('slash-menu')) {
2505            const sm = document.createElement('div');
2506            sm.id = 'slash-menu';
2507            sm.style.display = 'none';
2508            document.body.appendChild(sm);
2509        }
2510        if (!document.getElementById('block-plus-menu')) {
2511            const bpm = document.createElement('div');
2512            bpm.id = 'block-plus-menu';
2513            bpm.style.display = 'none';
2514            document.body.appendChild(bpm);
2515        }
2516
2517        if (currentPage) {
2518            initEditor(container.querySelector('#editor-blocks'), currentPage.id, ctx.bus);
2519            initSlashMenu(document.getElementById('slash-menu'));
2520            initPlusMenu(document.getElementById('block-plus-menu'));
2521
2522            // Listeners for inputs are kept here because elements are recreated
2523            const titleInput = container.querySelector('.page-title-editor');
2524            if (titleInput) {
2525                titleInput.addEventListener('input', (e) => store.renamePage(e.target.dataset.id, e.target.value));
2526                titleInput.addEventListener('blur', render);
2527            }
2528
2529            // Bind Icon Picker
2530            const iconBtn = container.querySelector('#header-icon-btn');
2531            if (iconBtn) {
2532                iconBtn.addEventListener('click', (e) => {
2533                    const rect = iconBtn.getBoundingClientRect();
2534                    showEmojiPicker(rect.left, rect.bottom + 5, (icon) => {
2535                        store.setPageIcon(currentPage.id, icon);
2536                        render();
2537                    });
2538                });
2539            }
2540
2541            // Bind AI Planner
2542            const btnAiPlanner = container.querySelector('#btn-ai-planner');
2543            if (btnAiPlanner) {
2544                btnAiPlanner.addEventListener('click', () => {
2545                    openAiPlanner(currentPage.id, ctx, render);
2546                });
2547            }
2548
2549            // Bind Page Links
2550            container.querySelectorAll('.page-link-block').forEach(el => {
2551                el.addEventListener('click', (e) => {
2552                    setSelectedPageId(e.currentTarget.dataset.pageId);
2553                    render();
2554                });
2555            });
2556        }
2557
2558        const btnSyncNotion = container.querySelector('#btn-sync-notion');
2559        if (btnSyncNotion) {
2560            btnSyncNotion.addEventListener('click', () => {
2561                showNotionConfigModal();
2562            });
2563        }
2564
2565        container.querySelector('#btn-paste-tasks')?.addEventListener('click', () => {
2566            ctx.bus?.emit('todo:open-import');
2567        });
2568
2569        const mobileProjectActionsToggle = container.querySelector('#btn-project-actions-mobile');
2570        const projectHeaderActions = container.querySelector('#project-header-actions');
2571        const closeMobileProjectActions = () => {
2572            projectHeaderActions?.classList.remove('is-mobile-open');
2573            mobileProjectActionsToggle?.setAttribute('aria-expanded', 'false');
2574        };
2575
2576        mobileProjectActionsToggle?.addEventListener('click', (event) => {
2577            event.stopPropagation();
2578            const willOpen = !projectHeaderActions?.classList.contains('is-mobile-open');
2579            projectHeaderActions?.classList.toggle('is-mobile-open', willOpen);
2580            mobileProjectActionsToggle.setAttribute('aria-expanded', String(willOpen));
2581        });
2582        projectHeaderActions?.addEventListener('click', (event) => {
2583            event.stopPropagation();
2584            if (event.target.closest('button')) closeMobileProjectActions();
2585        });
2586        container.querySelector('.obj-layout')?.addEventListener('click', closeMobileProjectActions);
2587        container.querySelector('.daily-priorities-panel')?.addEventListener('click', closeMobileProjectActions);
2588
2589        bindDailyPriorityUI();
2590
2591        bindGlobalEvents(container, store, container.querySelector('#page-tree'), ctx.bus);
2592
2593        // Init Drag & Drop for Editor
2594        // Init Drag & Drop for Editor
2595        initDragDropSystem(container.querySelector('#editor-blocks'), store);
2596
2597        // Restore selection visuals after render
2598        selection.updateVisuals();
2599
2600        // Restore scroll position
2601        restoreProjectScrollState(savedScrolls);
2602        requestAnimationFrame(() => restoreProjectScrollState(savedScrolls));
2603        restoreFocusedBlockCaret(savedCaret);
2604    }
2605
2606    // SETUP BUS LISTENER (Once per mount logic, but render might be recalled?)
2607    // Actually render is internal. 
2608    // We should attach the bus listener ONCE in mount, not in render.
2609    // But render is defined in mount.
2610    // So we add it here (inside mount, after render def).
2611    if (ctx.bus) {
2612        if (container.__roximaProjectsBusHandlers) {
2613            ctx.bus.off('objectifs:refresh', container.__roximaProjectsBusHandlers.objectifsRefresh);
2614            ctx.bus.off('i18n:changed', container.__roximaProjectsBusHandlers.i18nChanged);
2615            ctx.bus.off('daily-priorities:changed', container.__roximaProjectsBusHandlers.dailyPrioritiesChanged);
2616            ctx.bus.off('daily-priority:open-source', container.__roximaProjectsBusHandlers.dailyPriorityOpenSource);
2617            ctx.bus.off('daily-priority:complete-request', container.__roximaProjectsBusHandlers.dailyPriorityCompleteRequest);
2618            ctx.bus.off('daily-priority:block-request', container.__roximaProjectsBusHandlers.dailyPriorityBlockRequest);
2619            ctx.bus.off('calendar:changed', container.__roximaProjectsBusHandlers.calendarChanged);
2620            ctx.bus.off('planning-session:start', container.__roximaProjectsBusHandlers.planningSessionStart);
2621            ctx.bus.off('auth:state', container.__roximaProjectsBusHandlers.authState);
2622            ctx.bus.off('sync:account-hydrated', container.__roximaProjectsBusHandlers.accountHydrated);
2623        }
2624
2625        const deferRenderWhileActivationIsEdited = () => {
2626            const activationInput = container.querySelector('#project-activation-title:focus');
2627            if (!activationInput) return false;
2628            if (!pendingActivationRender) {
2629                pendingActivationRender = true;
2630                activationInput.addEventListener('blur', () => {
2631                    pendingActivationRender = false;
2632                    if (container.closest('body')) render();
2633                }, { once: true });
2634            }
2635            return true;
2636        };
2637        const objectifsRefresh = () => {
2638            if (!container.closest('body') || deferRenderWhileActivationIsEdited()) return;
2639            render();
2640        };
2641        const i18nChanged = () => {
2642            if (!container.closest('body') || deferRenderWhileActivationIsEdited()) return;
2643            render();
2644        };
2645        const dailyPrioritiesChanged = () => {
2646            if (!container.closest('body')) return;
2647            const activeEditor = container.querySelector('.editable-text:focus, .page-title-editor:focus');
2648            if (activeEditor) {
2649                if (!pendingDailyRender) {
2650                    pendingDailyRender = true;
2651                    activeEditor.addEventListener('blur', () => {
2652                        pendingDailyRender = false;
2653                        // Let the editor's 200 ms local-save debounce commit
2654                        // the final keystroke before rebuilding the DOM.
2655                        window.setTimeout(() => {
2656                            if (container.closest('body')) render();
2657                        }, 225);
2658                    }, { once: true });
2659                }
2660                return;
2661            }
2662            render();
2663        };
2664        const dailyPriorityOpenSource = payload => openDailyPrioritySource(payload);
2665        const dailyPriorityCompleteRequest = payload => handleDailyTaskExecutionRequest(payload, 'done');
2666        const dailyPriorityBlockRequest = payload => handleDailyTaskExecutionRequest(payload, 'blocked');
2667        const calendarChanged = () => {
2668            if (!container.closest('body') || deferRenderWhileActivationIsEdited()) return;
2669            render();
2670        };
2671        const planningSessionStart = payload => startPlanningSession(payload);
2672        const authState = ({ user } = {}) => setPlanningOwner(user);
2673        const accountHydrated = ({ userId } = {}) => {
2674            const activeOwnerId = String(ctx.currentUser?.id || '');
2675            if (!activeOwnerId || String(userId || '') !== activeOwnerId) return;
2676            void consumeAuthenticatedActivationDraft(activeOwnerId);
2677        };
2678
2679        container.__roximaProjectsBusHandlers = {
2680            objectifsRefresh,
2681            i18nChanged,
2682            dailyPrioritiesChanged,
2683            dailyPriorityOpenSource,
2684            dailyPriorityCompleteRequest,
2685            dailyPriorityBlockRequest,
2686            calendarChanged,
2687            planningSessionStart,
2688            authState,
2689            accountHydrated
2690        };
2691
2692        ctx.bus.on('objectifs:refresh', objectifsRefresh);
2693        ctx.bus.on('i18n:changed', i18nChanged);
2694        ctx.bus.on('daily-priorities:changed', dailyPrioritiesChanged);
2695        ctx.bus.on('daily-priority:open-source', dailyPriorityOpenSource);
2696        ctx.bus.on('daily-priority:complete-request', dailyPriorityCompleteRequest);
2697        ctx.bus.on('daily-priority:block-request', dailyPriorityBlockRequest);
2698        ctx.bus.on('calendar:changed', calendarChanged);
2699        ctx.bus.on('planning-session:start', planningSessionStart);
2700        ctx.bus.on('auth:state', authState);
2701        ctx.bus.on('sync:account-hydrated', accountHydrated);
2702    }
2703
2704    // --- SELECTION MANAGER ---
2705    const selection = {
2706        ids: new Set(),
2707        anchorBlockId: null,
2708        extentBlockId: null,
2709
2710        clear() {
2711            this.ids.clear();
2712            this.anchorBlockId = null;
2713            this.extentBlockId = null;
2714            this.updateVisuals();
2715        },
2716
2717        replace(ids, { anchorId = null, extentId = null } = {}) {
2718            this.ids = new Set(Array.from(ids || [], id => String(id)));
2719            this.anchorBlockId = anchorId ? String(anchorId) : (this.ids.values().next().value || null);
2720            this.extentBlockId = extentId ? String(extentId) : this.anchorBlockId;
2721            this.updateVisuals();
2722        },
2723
2724        add(id, { preserveAnchor = false } = {}) {
2725            const strId = String(id);
2726            this.ids.add(strId);
2727            if (!preserveAnchor || !this.anchorBlockId) this.anchorBlockId = strId;
2728            this.extentBlockId = strId;
2729            this.updateVisuals();
2730        },
2731
2732        toggle(id, { preserveAnchor = false } = {}) {
2733            const strId = String(id);
2734            if (this.ids.has(strId)) {
2735                this.ids.delete(strId);
2736                if (this.anchorBlockId === strId) this.anchorBlockId = this.ids.values().next().value || null;
2737            } else {
2738                this.ids.add(strId);
2739                if (!preserveAnchor || !this.anchorBlockId) this.anchorBlockId = strId;
2740            }
2741            this.extentBlockId = strId;
2742            this.updateVisuals();
2743        },
2744
2745        setSingle(id) {
2746            const strId = String(id);
2747            this.replace([strId], { anchorId: strId, extentId: strId });
2748        },
2749
2750        setRange(targetId, allBlockIds, { additive = false } = {}) {
2751            const orderedIds = allBlockIds.map(id => String(id));
2752            if (!this.anchorBlockId || !orderedIds.includes(this.anchorBlockId)) {
2753                this.setSingle(targetId);
2754                return;
2755            }
2756            const nextIds = getRangeSelection(
2757                orderedIds,
2758                this.anchorBlockId,
2759                targetId,
2760                additive ? [...this.ids] : []
2761            );
2762            this.replace(nextIds, { anchorId: this.anchorBlockId, extentId: targetId });
2763        },
2764
2765        updateVisuals() {
2766            const allBlocks = container.querySelectorAll('.editor-block');
2767            allBlocks.forEach(el => {
2768                const selected = this.ids.has(el.dataset.id);
2769                el.classList.toggle('selected', selected);
2770                el.setAttribute('aria-selected', String(selected));
2771            });
2772
2773            // Update selection state in topbar capsule
2774            window.Roxima?.setSelectionState?.({
2775                count: this.ids.size,
2776                canPaste: projectBlockClipboard.length > 0,
2777                onCopy: () => copyCurrentBlockSelection(),
2778                onPaste: () => insertProjectClipboard(),
2779                onDuplicate: () => insertProjectClipboard({ duplicate: true }),
2780                onDelete: () => deleteCurrentBlockSelection(),
2781                onClear: () => selection.clear()
2782            });
2783
2784            const bar = container.querySelector('#project-selection-bar');
2785            if (bar) {
2786                if (this.ids.size > 0) {
2787                    const countEl = bar.querySelector('.selection-count');
2788                    if (countEl) countEl.textContent = `${this.ids.size} ${i18n.t('selection_items_selected')}`;
2789                    bar.classList.add('visible');
2790                } else {
2791                    bar.classList.remove('visible');
2792                }
2793                const pasteButton = bar.querySelector('.btn-paste');
2794                if (pasteButton) pasteButton.disabled = projectBlockClipboard.length === 0;
2795            }
2796        },
2797
2798        getSortedIds(allBlockIds) {
2799            // Return IDs in order of appearance
2800            return allBlockIds.filter(id => this.ids.has(id));
2801        }
2802    };
2803
2804    const getEditorBlockIds = (editorEl = container) => Array.from(
2805        editorEl.querySelectorAll('.editor-block'),
2806        el => el.dataset.id
2807    );
2808
2809    function getSelectedBlocks({ includeDescendants = true } = {}) {
2810        if (!selectedPageId) return [];
2811        const blocks = store.getBlocks(selectedPageId);
2812        if (includeDescendants) return getSelectionWithDescendants(blocks, selection.ids);
2813        const selectedIds = new Set(selection.ids);
2814        return blocks.filter(block => selectedIds.has(String(block.id)));
2815    }
2816
2817    function copyCurrentBlockSelection({ showToast = true } = {}) {
2818        if (!selectedPageId || selection.ids.size === 0) return false;
2819        const blocks = store.getBlocks(selectedPageId);
2820        projectBlockClipboard = copySelectedBlocks(blocks, selection.ids);
2821        const plainText = formatSelectedBlocksAsText(blocks, selection.ids);
2822        if (!projectBlockClipboard.length) return false;
2823
2824        navigator.clipboard?.writeText(plainText).catch(() => {});
2825        selection.updateVisuals();
2826        if (showToast) window.Roxima.showToast?.(i18n.t('projects_toast_copied'));
2827        return true;
2828    }
2829
2830    function insertProjectClipboard({ duplicate = false } = {}) {
2831        if (!selectedPageId || selection.ids.size === 0) return [];
2832        const sourceBlocks = duplicate
2833            ? copySelectedBlocks(store.getBlocks(selectedPageId), selection.ids)
2834            : projectBlockClipboard;
2835        if (!sourceBlocks.length) return [];
2836
2837        const selectedBlocks = getSelectedBlocks();
2838        const afterBlockId = selectedBlocks.at(-1)?.id || null;
2839        const insertedBlocks = store.insertBlocks(selectedPageId, sourceBlocks, afterBlockId);
2840        if (!insertedBlocks.length) return [];
2841
2842        const insertedIds = insertedBlocks.map(block => String(block.id));
2843        selection.replace(insertedIds, { anchorId: insertedIds[0], extentId: insertedIds.at(-1) });
2844        render();
2845        window.Roxima.showToast?.(i18n.t(duplicate ? 'projects_toast_duplicated' : 'projects_toast_pasted'));
2846        return insertedBlocks;
2847    }
2848
2849    function deleteCurrentBlockSelection() {
2850        if (!selectedPageId || selection.ids.size === 0) return [];
2851        const pageId = selectedPageId;
2852        const blocksToDelete = getSelectedBlocks();
2853        blocksToDelete.forEach(block => {
2854            if (block.isNotion && block.notionBlockId) deleteBlockFromNotion(pageId, block.notionBlockId);
2855        });
2856        const deletedBlocks = store.deleteBlocks(pageId, blocksToDelete.map(block => block.id));
2857        selection.clear();
2858        if (store.getBlocks(pageId).length === 0) store.addBlock(pageId, { type: 'p', text: '' });
2859        render();
2860        return deletedBlocks;
2861    }
2862
2863    function moveCurrentBlockSelection(direction) {
2864        if (!selectedPageId || selection.ids.size === 0) return [];
2865        const blocks = store.getBlocks(selectedPageId);
2866        const movingBlocks = getSelectionWithDescendants(blocks, selection.ids);
2867        if (!movingBlocks.length) return [];
2868
2869        const movingIds = new Set(movingBlocks.map(block => String(block.id)));
2870        const firstIndex = blocks.findIndex(block => movingIds.has(String(block.id)));
2871        let lastIndex = -1;
2872        blocks.forEach((block, index) => {
2873            if (movingIds.has(String(block.id))) lastIndex = index;
2874        });
2875
2876        const targetIndex = direction === 'up' ? firstIndex - 1 : lastIndex + 1;
2877        if (targetIndex < 0 || targetIndex >= blocks.length) return [];
2878
2879        const targetBlock = blocks[targetIndex];
2880        const movedIds = store.moveBlocks(
2881            selectedPageId,
2882            selection.getSortedIds(getEditorBlockIds()),
2883            targetBlock.id,
2884            direction === 'up' ? 'before' : 'after'
2885        );
2886        if (movedIds.length) render();
2887        return movedIds;
2888    }
2889
2890    // --- DRAG & DROP & SELECTION SYSTEM ---
2891    function initDragDropSystemLegacy(editorEl, store) {
2892        if (!editorEl) return;
2893
2894        let dragAllowed = false;
2895        let dragSrcId = null;
2896        let isDragOp = false;
2897
2898        let isSelecting = false;
2899        let startY = 0;
2900        let startX = 0;
2901        let longPressTimer = null;
2902        let dragSelectionActive = false;
2903        let startBlockId = null;
2904        let initialSelectedIds = new Set();
2905
2906        let isPointerDown = false;
2907        let startEditable = null;
2908
2909        editorEl.addEventListener('pointerdown', (e) => {
2910            if (e.button !== 0 && e.pointerType === 'mouse') return;
2911
2912            // Ignore interactive elements
2913            const isInteractive = e.target.closest('.block-controls button, .action-add-sub, .action-del, input, button');
2914            if (isInteractive) return;
2915
2916            isPointerDown = true;
2917            startX = e.clientX;
2918            startY = e.clientY;
2919            dragSelectionActive = false;
2920            startEditable = e.target.closest('.editable-text');
2921            
2922            initialSelectedIds = new Set(selection.ids);
2923            
2924            const blockEl = e.target.closest('.editor-block');
2925            const blockId = blockEl?.dataset?.id;
2926
2927            if (longPressTimer) clearTimeout(longPressTimer);
2928            longPressTimer = setTimeout(() => {
2929                isSelecting = true;
2930                dragSelectionActive = true;
2931                initialSelectedIds = new Set(selection.ids);
2932                editorEl.classList.add('dragging-selection');
2933
2934                if (blockId) {
2935                    selection.add(blockId);
2936                }
2937                
2938                try {
2939                    editorEl.setPointerCapture(e.pointerId);
2940                } catch (err) {}
2941
2942                if (navigator.vibrate) navigator.vibrate(10);
2943            }, 250);
2944        });
2945
2946        editorEl.addEventListener('pointermove', (e) => {
2947            if (longPressTimer) {
2948                const dist = Math.hypot(e.clientX - startX, e.clientY - startY);
2949                if (dist > 10) {
2950                    clearTimeout(longPressTimer);
2951                    longPressTimer = null;
2952                }
2953            }
2954
2955            if (!isPointerDown) return;
2956
2957            if (!dragSelectionActive) {
2958                const distY = Math.abs(e.clientY - startY);
2959                const distX = Math.abs(e.clientX - startX);
2960                
2961                let shouldStartBlockSelection = false;
2962                
2963                if (startEditable) {
2964                    // Started in focused text: only switch to block selection if dragged vertically outside the block's bounds
2965                    const rect = startEditable.getBoundingClientRect();
2966                    if (e.clientY < rect.top - 5 || e.clientY > rect.bottom + 5) {
2967                        shouldStartBlockSelection = true;
2968                    }
2969                } else {
2970                    // Started in non-focused text or empty space: start block selection immediately
2971                    if (Math.hypot(distX, distY) > 5) {
2972                        shouldStartBlockSelection = true;
2973                    }
2974                }
2975
2976                if (shouldStartBlockSelection) {
2977                    if (longPressTimer) {
2978                        clearTimeout(longPressTimer);
2979                        longPressTimer = null;
2980                    }
2981                    dragSelectionActive = true;
2982                    isSelecting = true;
2983                    editorEl.classList.add('dragging-selection');
2984                    
2985                    const sel = window.getSelection();
2986                    if (sel) sel.removeAllRanges();
2987                    
2988                    if (!e.shiftKey && !e.ctrlKey && !e.metaKey) {
2989                        selection.clear();
2990                        initialSelectedIds = new Set();
2991                    }
2992                    try { editorEl.setPointerCapture(e.pointerId); } catch(err){}
2993                }
2994            }
2995
2996            if (dragSelectionActive) {
2997                e.preventDefault();
2998
2999                const currentY = e.clientY;
3000                const minY = Math.min(startY, currentY);
3001                const maxY = Math.max(startY, currentY);
3002
3003                const allBlockEls = Array.from(editorEl.querySelectorAll('.editor-block'));
3004                const newSelection = new Set(initialSelectedIds);
3005                allBlockEls.forEach(el => {
3006                    const rect = el.getBoundingClientRect();
3007                    if (rect.bottom >= minY && rect.top <= maxY) {
3008                        newSelection.add(el.dataset.id);
3009                    }
3010                });
3011                selection.ids = newSelection;
3012                selection.updateVisuals();
3013            }
3014        });
3015
3016        editorEl.addEventListener('selectstart', (e) => {
3017            if (dragSelectionActive || (isPointerDown && !startEditable)) {
3018                e.preventDefault();
3019            }
3020        });
3021
3022        const endSelection = (e) => {
3023            if (longPressTimer) {
3024                clearTimeout(longPressTimer);
3025                longPressTimer = null;
3026            }
3027            isPointerDown = false;
3028            if (dragSelectionActive) {
3029                dragSelectionActive = false;
3030                isSelecting = false;
3031                ignoredSelectionClick = {
3032                    clientX: e.clientX,
3033                    clientY: e.clientY,
3034                    expiresAt: performance.now() + 250
3035                };
3036                editorEl.classList.remove('dragging-selection');
3037                try {
3038                    editorEl.releasePointerCapture(e.pointerId);
3039                } catch (err) {}
3040            }
3041        };
3042
3043        editorEl.addEventListener('pointerup', endSelection);
3044        editorEl.addEventListener('pointercancel', endSelection);
3045
3046        // 1. Validate Intent on MouseDown
3047        editorEl.addEventListener('mousedown', (e) => {
3048            const handle = e.target.closest('.block-handle');
3049            /*
3050                User Req:
3051                - Click gutter (handle) -> select
3052                - Shift+Click -> range
3053                - Ctrl+Click -> toggle
3054                - Drag -> normal DnD
3055            */
3056            if (handle) {
3057                dragAllowed = true;
3058                isDragOp = false;
3059
3060                const blockEl = handle.closest('.editor-block');
3061                if (blockEl) {
3062                    blockEl.setAttribute('draggable', 'true');
3063                }
3064                const blockId = blockEl ? blockEl.dataset.id : null;
3065
3066                const allBlockIds = Array.from(editorEl.querySelectorAll('.editor-block')).map(el => el.dataset.id);
3067
3068                if (e.shiftKey) {
3069                    e.preventDefault(); // Prevent text selection
3070                    selection.setRange(blockId, allBlockIds);
3071                } else if (e.ctrlKey || e.metaKey) {
3072                    e.preventDefault();
3073                    selection.toggle(blockId);
3074                } else {
3075                    // Normal click: 
3076                    // If already selected, don't clear (might be preparing to drag multiple)
3077                    // If not selected, select single. 
3078                    if (!selection.ids.has(blockId)) {
3079                        selection.setSingle(blockId);
3080                    }
3081                    // Note: If I click an already selected block, I might want to de
3081select others ON MOUSEUP if no drag happened.
3082                    // For now simplicity: Click selects single.
3083                }
3084            } else {
3085                dragAllowed = false;
3086                // Clicked outside handle (e.g. text or blank)? Clear selection if not Shift/Ctrl
3087                if (!e.target.closest('.block-plus') && !e.target.closest('.block-handle')) {
3088                    // Check if click is in text (don't clear if text interaction?)
3089                    // User says: "Click dans une zone vide (hors blocs) : clear selection" -> Handle in container click
3090                    // User says: "ne pas casser la sélection de texte à l’intérieur du bloc"
3091                    // If click is in editable-text, we shouldn't clear unless we want to?
3092                    // Notion clears block selection when editing text.
3093                    if (e.target.classList.contains('editable-text') || e.target.closest('.editable-text')) {
3094                        selection.clear();
3095                    }
3096                }
3097            }
3098        });
3099
3100        editorEl.addEventListener('mouseup', (e) => {
3101            editorEl.querySelectorAll('.editor-block').forEach(el => {
3102                el.setAttribute('draggable', 'false');
3103            });
3104        });
3105
3106        // 2. Start Dragging
3107        editorEl.addEventListener('dragstart', (e) => {
3108            if (!dragAllowed) {
3109                e.preventDefault();
3110                return;
3111            }
3112
3113            isDragOp = true;
3114            const blockEl = e.target.closest('.editor-block');
3115            if (!blockEl) return;
3116
3117            // If dragging a selected block, drag all selected? 
3118            // For now, simpler: Just drag the source block.
3119            // Future TODO: Multi-drag.
3120
3121            dragSrcId = blockEl.dataset.id;
3122            e.dataTransfer.effectAllowed = 'copyMove'; // Allow copy to other blocks
3123            e.dataTransfer.setData('text/plain', dragSrcId); // Keep for internal sort
3124
3125            // Export Rich Data for external drops (e.g. To Do block)
3126            const allBlocks = store.getBlocks(selectedPageId);
3127            const srcIdx = allBlocks.findIndex(b => String(b.id) === dragSrcId);
3128
3129            if (srcIdx !== -1) {
3130                const rootBlock = allBlocks[srcIdx];
3131                const exportItems = [];
3132
3133                // Add Root
3134                const project = store.getPage(selectedPageId);
3135                exportItems.push({
3136                    origin: 'objectifs',
3137                    id: rootBlock.id,
3138                    type: rootBlock.type,
3139                    text: rootBlock.text,
3140                    checked: rootBlock.checked,
3141                    projectId: selectedPageId,
3142                    projectName: project ? store.getPagePath(project.id) : '',
3143                    projectIcon: project ? project.icon : '',
3144                    estimatedMinutes: rootBlock.estimatedMinutes || rootBlock.estimated_minutes || rootBlock.duration_minutes || null,
3145                    level: 0 // Root is level 0 relative to export
3146                });
3147
3148                // Add Descendants
3149                const rootLevel = rootBlock.level || 0;
3150                for (let i = srcIdx + 1; i < allBlocks.length; i++) {
3151                    const b = allBlocks[i];
3152                    const bLevel = b.level || 0;
3153                    if (bLevel > rootLevel) {
3154                        exportItems.push({
3155                            origin: 'objectifs',
3156                            id: b.id,
3157                            type: b.type,
3158                            text: b.text,
3159                            checked: b.checked,
3160                            projectId: selectedPageId,
3161                            projectName: project ? store.getPagePath(project.id) : '',
3162                            projectIcon: project ? project.icon : '',
3163                            estimatedMinutes: b.estimatedMinutes || b.estimated_minutes || b.duration_minutes || null,
3164                            level: bLevel - rootLevel // Normalize relative to root
3165                        });
3166                    } else {
3167                        break; // Not a child anymore
3168                    }
3169                }
3170
3171                e.dataTransfer.setData('application/json', JSON.stringify(exportItems));
3172                window.__roximaDraggingTask = {
3173                    text: rootBlock.text,
3174                    id: rootBlock.id,
3175                    origin: 'projects'
3176                };
3177            }
3178
3179            requestAnimationFrame(() => blockEl.classList.add('drag-source'));
3180        });
3181
3182        editorEl.addEventListener('dragend', (e) => {
3183            dragAllowed = false; // Reset
3184            window.__roximaDraggingTask = null;
3185            const blockEl = e.target.closest('.editor-block');
3186            if (blockEl) {
3187                blockEl.classList.remove('drag-source');
3188                blockEl.setAttribute('draggable', 'false');
3189            }
3190
3191            // Clean up all borders
3192            editorEl.querySelectorAll('.drag-over-top, .drag-over-bottom').forEach(el => {
3193                el.classList.remove('drag-over-top');
3194                el.classList.remove('drag-over-bottom');
3195            });
3196        });
3197
3198        editorEl.addEventListener('dragover', (e) => {
3199            e.preventDefault(); // Necessary to allow dropping
3200            const targetBlock = e.target.closest('.editor-block');
3201            if (!targetBlock || targetBlock.dataset.id === dragSrcId) return;
3202
3203            const rect = targetBlock.getBoundingClientRect();
3204            const offset = e.clientY - rect.top;
3205            const height = rect.height;
3206
3207            // Remove existing classes first
3208            editorEl.querySelectorAll('.drag-over-top, .drag-over-bottom').forEach(el => {
3209                if (el !== targetBlock) {
3210                    el.classList.remove('drag-over-top');
3211                    el.classList.remove('drag-over-bottom');
3212                }
3213            });
3214
3215            if (offset < height / 2) {
3216                targetBlock.classList.add('drag-over-top');
3217                targetBlock.classList.remove('drag-over-bottom');
3218            } else {
3219                targetBlock.classList.add('drag-over-bottom');
3220                targetBlock.classList.remove('drag-over-top');
3221            }
3222        });
3223
3224        editorEl.addEventListener('dragleave', (e) => {
3225            // We can't clear immediately because dragleave fires when entering a child.
3226            // We rely on dragover to clear others.
3227        });
3228
3229        editorEl.addEventListener('drop', (e) => {
3230            e.stopPropagation();
3231            const targetBlock = e.target.closest('.editor-block');
3232            if (!targetBlock || !dragSrcId) return;
3233
3234            const targetId = targetBlock.dataset.id;
3235            if (dragSrcId === targetId) return;
3236
3237            // Determine position
3238            let position = 'after'; // default
3239            if (targetBlock.classList.contains('drag-over-top')) {
3240                position = 'before';
3241            }
3242
3243            store.moveBlock(selectedPageId, dragSrcId, targetId, position);
3244            render(); // Re-render whole list
3245        });
3246    }
3247
3248    function initDragDropSystem(editorEl, store) {
3249        if (!editorEl) return;
3250
3251        const scroller = editorEl.closest('.obj-content') || editorEl;
3252        const selectionSurface = editorEl.closest('.card') || scroller;
3253
3254        let dragAllowed = false;
3255        let dragSrcIds = [];
3256        let dragMovingIds = new Set();
3257        let isPointerDown = false;
3258        let pointerId = null;
3259        let startClientX = 0;
3260        let startClientY = 0;
3261        let startPoint = null;
3262        let lastClientPoint = null;
3263        let startBlockId = null;
3264        let startEditable = null;
3265        let canStartMarquee = false;
3266        let dragSelectionActive = false;
3267        let initialSelectedIds = new Set();
3268        let longPressTimer = null;
3269        let marqueeEl = null;
3270        let marqueeViewportEl = null;
3271
3272        const allEditorIds = () => Array.from(editorEl.querySelectorAll('.editor-block'), el => el.dataset.id);
3273        const removeMarquee = () => {
3274            marqueeViewportEl?.remove();
3275            marqueeEl = null;
3276            marqueeViewportEl = null;
3277        };
3278        const getSelectionGeometry = () => ({
3279            surfaceRect: selectionSurface.getBoundingClientRect(),
3280            scrollportRect: scroller.getBoundingClientRect(),
3281            scrollTop: scroller.scrollTop
3282        });
3283        const getSelectionPoint = (clientX, clientY, geometry = getSelectionGeometry()) => (
3284            clientPointToSelectionSpace({ clientX, clientY }, geometry.surfaceRect, geometry.scrollportRect, geometry.scrollTop)
3285        );
3286        const isInsideSelectionSurface = (event) => {
3287            const { surfaceRect, scrollportRect } = getSelectionGeometry();
3288            return event.clientX >= surfaceRect.left
3289                && event.clientX <= surfaceRect.right
3290                && event.clientY >= scrollportRect.top
3291                && event.clientY <= scrollportRect.bottom;
3292        };
3293        const showMarquee = (clientX, clientY) => {
3294            const geometry = getSelectionGeometry();
3295            const currentPoint = getSelectionPoint(clientX, clientY, geometry);
3296            const marqueeRect = createSelectionRect(startPoint, currentPoint);
3297            if (!marqueeRect) return null;
3298
3299            if (!marqueeViewportEl) {
3300                marqueeViewportEl = document.createElement('div');
3301                marqueeViewportEl.className = 'project-selection-marquee-viewport';
3302                marqueeViewportEl.setAttribute('aria-hidden', 'true');
3303                marqueeEl = document.createElement('div');
3304                marqueeEl.className = 'project-selection-marquee';
3305                marqueeViewportEl.appendChild(marqueeEl);
3306                document.body.appendChild(marqueeViewportEl);
3307            }
3308
3309            Object.assign(marqueeViewportEl.style, {
3310                left: `${geometry.surfaceRect.left}px`,
3311                top: `${geometry.scrollportRect.top}px`,
3312                width: `${geometry.surfaceRect.width}px`,
3313                height: `${geometry.scrollportRect.height}px`
3314            });
3315            Object.assign(marqueeEl.style, {
3316                left: `${marqueeRect.left}px`,
3317                top: `${marqueeRect.top - geometry.scrollTop}px`,
3318                width: `${marqueeRect.right - marqueeRect.left}px`,
3319                height: `${marqueeRect.bottom - marqueeRect.top}px`
3320            });
3321            return { marqueeRect, geometry };
3322        };
3323        const updateMarqueeSelection = (clientX, clientY) => {
3324            lastClientPoint = { clientX, clientY };
3325            const marqueeState = showMarquee(clientX, clientY);
3326            if (!marqueeState) return;
3327
3328            const newSelection = new Set(initialSelectedIds);
3329            editorEl.querySelectorAll('.editor-block').forEach(el => {
3330                const blockRect = clientRectToSelectionSpace(
3331                    el.getBoundingClientRect(),
3332                    marqueeState.geometry.surfaceRect,
3333                    marqueeState.geometry.scrollportRect,
3334                    marqueeState.geometry.scrollTop
3335                );
3336                if (rectanglesIntersect(marqueeState.marqueeRect, blockRect)) newSelection.add(el.dataset.id);
3337            });
3338            const orderedSelectedIds = allEditorIds().filter(id => newSelection.has(id));
3339            const firstSelectedId = orderedSelectedIds[0] || startBlockId;
3340            selection.replace(newSelection, {
3341                anchorId: startBlockId || firstSelectedId,
3342                extentId: orderedSelectedIds.at(-1) || firstSelectedId
3343            });
3344        };
3345        const beginMarquee = (event) => {
3346            dragSelectionActive = true;
3347            editorEl.classList.add('dragging-selection');
3348            window.getSelection()?.removeAllRanges();
3349            if (!event.shiftKey && !event.ctrlKey && !event.metaKey) {
3350                initialSelectedIds = new Set();
3351                selection.replace([]);
3352            }
3353            try { selectionSurface.setPointerCapture(event.pointerId); } catch (error) {}
3354        };
3355
3356        selectionSurface.addEventListener('pointerdown', (event) => {
3357            if (event.pointerType === 'mouse' && event.button !== 0) return;
3358            const isInteractive = event.target.closest('.block-controls button, .action-add-sub, .action-del, input, button, a');
3359            if (isInteractive || !isInsideSelectionSurface(event)) return;
3360
3361            const blockEl = event.target.closest('.editor-block');
3362            const blockId = blockEl?.dataset?.id || null;
3363            const hasToggleModifier = event.ctrlKey || event.metaKey;
3364            if (blockId && (event.shiftKey || hasToggleModifier)) {
3365                event.preventDefault();
3366                if (event.shiftKey) {
3367                    selection.setRange(blockId, allEditorIds(), { additive: hasToggleModifier });
3368                } else {
3369                    selection.toggle(blockId);
3370                }
3371                return;
3372            }
3373
3374            isPointerDown = true;
3375            pointerId = event.pointerId;
3376            startClientX = event.clientX;
3377            startClientY = event.clientY;
3378            startPoint = getSelectionPoint(event.clientX, event.clientY);
3379            lastClientPoint = { clientX: event.clientX, clientY: event.clientY };
3380            startBlockId = blockId;
3381            startEditable = event.target.closest('.editable-text');
3382            // Contenteditable always wins over marquee selection. This keeps an
3383            // empty block immediately clickable and prevents tiny mouse moves
3384            // from turning a text-editing intent into a block selection.
3385            canStartMarquee = !startEditable;
3386            initialSelectedIds = new Set(selection.ids);
3387
3388            if (event.pointerType !== 'mouse') {
3389                longPressTimer = setTimeout(() => {
3390                    canStartMarquee = true;
3391                    beginMarquee(event);
3392                    if (blockId) selection.add(blockId, { preserveAnchor: true });
3393                    navigator.vibrate?.(10);
3394                }, 300);
3395            }
3396        });
3397
3398        selectionSurface.addEventListener('pointermove', (event) => {
3399            if (!isPointerDown || event.pointerId !== pointerId) return;
3400            const distance = Math.hypot(event.clientX - startClientX, event.clientY - startClientY);
3401            if (longPressTimer && distance > 10) {
3402                clearTimeout(longPressTimer);
3403                longPressTimer = null;
3404            }
3405            if (!dragSelectionActive && canStartMarquee && distance > 5) beginMarquee(event);
3406            if (!dragSelectionActive) return;
3407
3408            event.preventDefault();
3409            updateMarqueeSelection(event.clientX, event.clientY);
3410
3411            const rect = scroller.getBoundingClientRect();
3412            if (event.clientY < rect.top + 36) scroller.scrollBy({ top: -12 });
3413            if (event.clientY > rect.bottom - 36) scroller.scrollBy({ top: 12 });
3414        });
3415
3416        scroller.addEventListener('scroll', () => {
3417            if (!dragSelectionActive || !lastClientPoint) return;
3418            updateMarqueeSelection(lastClientPoint.clientX, lastClientPoint.clientY);
3419        });
3420
3421        selectionSurface.addEventListener('selectstart', (event) => {
3422            if (dragSelectionActive || (isPointerDown && canStartMarquee)) event.preventDefault();
3423        });
3424
3425        const endSelection = (event) => {
3426            if (longPressTimer) clearTimeout(longPressTimer);
3427            longPressTimer = null;
3428            isPointerDown = false;
3429            pointerId = null;
3430            if (!dragSelectionActive) return;
3431            dragSelectionActive = false;
3432            if (event.type === 'pointerup') {
3433                ignoredSelectionClick = {
3434                    clientX: event.clientX,
3435                    clientY: event.clientY,
3436                    expiresAt: performance.now() + 250
3437                };
3438            }
3439            editorEl.classList.remove('dragging-selection');
3440            removeMarquee();
3441            try { selectionSurface.releasePointerCapture(event.pointerId); } catch (error) {}
3442        };
3443        selectionSurface.addEventListener('pointerup', endSelection);
3444        selectionSurface.addEventListener('pointercancel', endSelection);
3445
3446        editorEl.addEventListener('mousedown', (event) => {
3447            const handle = event.target.closest('.block-handle');
3448            if (!handle) {
3449                dragAllowed = false;
3450                return;
3451            }
3452
3453            dragAllowed = true;
3454            const blockEl = handle.closest('.editor-block');
3455            if (!blockEl) return;
3456            blockEl.setAttribute('draggable', 'true');
3457            const blockId = blockEl.dataset.id;
3458            if (event.shiftKey) {
3459                event.preventDefault();
3460                selection.setRange(blockId, allEditorIds(), { additive: event.ctrlKey || event.metaKey });
3461            } else if (event.ctrlKey || event.metaKey) {
3462                event.preventDefault();
3463                selection.toggle(blockId);
3464            } else if (!selection.ids.has(blockId)) {
3465                selection.setSingle(blockId);
3466            }
3467        });
3468
3469        editorEl.addEventListener('click', (event) => {
3470            const blockEl = event.target.closest('.editor-block');
3471            if (!blockEl) return;
3472
3473            const modifiedBlockSelection = (event.shiftKey || event.ctrlKey || event.metaKey)
3474                && (event.target.closest('.block-handle') || !event.target.closest('button, input, a'));
3475            if (modifiedBlockSelection) {
3476                event.preventDefault();
3477                event.stopImmediatePropagation();
3478                return;
3479            }
3480            if (event.target.closest('.editable-text')) {
3481                selection.clear();
3482                return;
3483            }
3484            if (event.target.closest('button, input, a')) return;
3485            if (!event.shiftKey && !event.ctrlKey && !event.metaKey) selection.setSingle(blockEl.dataset.id);
3486        }, true);
3487
3488        editorEl.addEventListener('mouseup', () => {
3489            editorEl.querySelectorAll('.editor-block').forEach(el => el.setAttribute('draggable', 'false'));
3490        });
3491
3492        editorEl.addEventListener('dragstart', (event) => {
3493            if (!dragAllowed) {
3494                event.preventDefault();
3495                return;
3496            }
3497            const blockEl = event.target.closest('.editor-block');
3498            if (!blockEl) return;
3499            const dragSrcId = blockEl.dataset.id;
3500            dragSrcIds = selection.ids.has(dragSrcId)
3501                ? selection.getSortedIds(allEditorIds())
3502                : [dragSrcId];
3503
3504            const allBlocks = store.getBlocks(selectedPageId);
3505            dragMovingIds = new Set(getSelectionWithDescendants(allBlocks, dragSrcIds).map(block => String(block.id)));
3506            const project = store.getPage(selectedPageId);
3507            const exportItems = createProjectDragItems(allBlocks, dragSrcIds, {
3508                id: selectedPageId,
3509                name: project ? store.getPagePath(project.id) : '',
3510                icon: project?.icon || ''
3511            });
3512
3513            event.dataTransfer.effectAllowed = 'copyMove';
3514            event.dataTransfer.setData('text/plain', dragSrcIds.join(','));
3515            event.dataTransfer.setData('application/json', JSON.stringify(exportItems));
3516            window.__roximaDraggingTasks = exportItems;
3517            window.__roximaDraggingTask = exportItems.length ? {
3518                ...exportItems[0],
3519                origin: 'projects',
3520                count: exportItems.length
3521            } : null;
3522
3523            if (dragSrcIds.length > 1) {
3524                const ghost = document.createElement('div');
3525                ghost.className = 'project-multi-drag-ghost';
3526                ghost.textContent = `${dragSrcIds.length} ${i18n.t('selection_items_selected')}`;
3527                document.body.appendChild(ghost);
3528                event.dataTransfer.setDragImage(ghost, 18, 18);
3529                setTimeout(() => ghost.remove(), 0);
3530            }
3531            requestAnimationFrame(() => {
3532                editorEl.querySelectorAll('.editor-block').forEach(el => {
3533                    el.classList.toggle('drag-source', dragMovingIds.has(el.dataset.id));
3534                });
3535            });
3536        });
3537
3538        editorEl.addEventListener('dragend', () => {
3539            dragAllowed = false;
3540            dragSrcIds = [];
3541            dragMovingIds.clear();
3542            window.__roximaDraggingTask = null;
3543            window.__roximaDraggingTasks = null;
3544            editorEl.querySelectorAll('.drag-source, .drag-over-top, .drag-over-bottom').forEach(el => {
3545                el.classList.remove('drag-source', 'drag-over-top', 'drag-over-bottom');
3546                el.setAttribute('draggable', 'false');
3547            });
3548        });
3549
3550        editorEl.addEventListener('dragover', (event) => {
3551            event.preventDefault();
3552            const targetBlock = event.target.closest('.editor-block');
3553            if (!targetBlock || dragMovingIds.has(targetBlock.dataset.id)) return;
3554            editorEl.querySelectorAll('.drag-over-top, .drag-over-bottom').forEach(el => {
3555                if (el !== targetBlock) el.classList.remove('drag-over-top', 'drag-over-bottom');
3556            });
3557            const rect = targetBlock.getBoundingClientRect();
3558            const before = event.clientY < rect.top + rect.height / 2;
3559            targetBlock.classList.toggle('drag-over-top', before);
3560            targetBlock.classList.toggle('drag-over-bottom', !before);
3561        });
3562
3563        editorEl.addEventListener('drop', (event) => {
3564            event.preventDefault();
3565            event.stopPropagation();
3566            const targetBlock = event.target.closest('.editor-block');
3567            if (!targetBlock || !dragSrcIds.length || dragMovingIds.has(targetBlock.dataset.id)) return;
3568            const position = targetBlock.classList.contains('drag-over-top') ? 'before' : 'after';
3569            const movedIds = store.moveBlocks(selectedPageId, dragSrcIds, targetBlock.dataset.id, position);
3570            if (movedIds.length) render();
3571        });
3572    }
3573
3574    function createFormattingToolbar() {
3575        if (formattingToolbar) return;
3576
3577        formattingToolbar = document.createElement('div');
3578        formattingToolbar.className = 'notion-formatting-toolbar';
3579        
3580        formattingToolbar.innerHTML = `
3581            <button class="format-btn bold" type="button" title="Gras (Ctrl+B)">B</button>
3582            <button class="format-btn italic" type="button" title="Italique (Ctrl+I)">I</button>
3583            <button class="format-btn underline" type="button" title="Souligné (Ctrl+U)">U</button>
3584            <button class="format-btn strike" type="button" title="Barré">S</button>
3585            <button class="format-btn code" type="button" title="Code en ligne">&lt;/&gt;</button>
3586            <div class="format-divider" style="width: 1px; height: 16px; background: var(--border-color, #e2e8f0); margin: 0 4px;"></div>
3587            <button class="format-btn color-picker" type="button" title="Couleur">
3588                <span class="color-indicator">A</span>
3589                <span style="font-size: 8px; margin-left: 2px;">▼</span>
3590            </button>
3591        `;
3592
3593        document.body.appendChild(formattingToolbar);
3594
3595        // Bind events
3596        formattingToolbar.querySelector('.bold').addEventListener('mousedown', (e) => {
3597            e.preventDefault();
3598            document.execCommand('bold', false, null);
3599            triggerTextChange();
3600        });
3601        formattingToolbar.querySelector('.italic').addEventListener('mousedown', (e) => {
3602            e.preventDefault();
3603            document.execCommand('italic', false, null);
3604            triggerTextChange();
3605        });
3606        formattingToolbar.querySelector('.underline').addEventListener('mousedown', (e) => {
3607            e.preventDefault();
3608            document.execCommand('underline', false, null);
3609            triggerTextChange();
3610        });
3611        formattingToolbar.querySelector('.strike').addEventListener('mousedown', (e) => {
3612            e.preventDefault();
3613            document.execCommand('strikeThrough', false, null);
3614            triggerTextChange();
3615        });
3616        formattingToolbar.querySelector('.code').addEventListener('mousedown', (e) => {
3617            e.preventDefault();
3618            wrapSelectionInTag('code');
3619            triggerTextChange();
3620        });
3621        
3622        const colorBtn = formattingToolbar.querySelector('.color-picker');
3623        colorBtn.addEventListener('mousedown', (e) => {
3624            e.preventDefault();
3625            e.stopPropagation();
3626            toggleColorDropdown();
3627        });
3628    }
3629
3630    function triggerTextChange() {
3631        const activeEl = document.activeElement;
3632        if (activeEl && activeEl.classList.contains('editable-text')) {
3633            const event = new Event('input', { bubbles: true });
3634            activeEl.dispatchEvent(event);
3635        }
3636    }
3637
3638    function wrapSelectionInTag(tagName, className = '') {
3639        const selection = window.getSelection();
3640        if (!selection || selection.rangeCount === 0 || selection.isCollapsed) return;
3641
3642        const range = selection.getRangeAt(0);
3643        let parent = range.commonAncestorContainer;
3644        if (parent.nodeType === 3) parent = parent.parentNode;
3645        
3646        const existingNode = parent.closest(tagName);
3647        if (existingNode && (!className || existingNode.className === className)) {
3648            const textNode = document.createTextNode(existingNode.textContent);
3649            existingNode.parentNode.replaceChild(textNode, existingNode);
3650            return;
3651        }
3652
3653        const el = document.createElement(tagName);
3654        if (className) el.className = className;
3655        
3656        try {
3657            el.appendChild(range.extractContents());
3658            range.insertNode(el);
3659            
3660            const newRange = document.createRange();
3661            newRange.selectNodeContents(el);
3662            selection.removeAllRanges();
3663            selection.addRange(newRange);
3664        } catch (err) {
3665            console.error("Failed to wrap selection", err);
3666        }
3667    }
3668
3669    function toggleColorDropdown() {
3670        if (currentColorDropdown) {
3671            currentColorDropdown.remove();
3672            currentColorDropdown = null;
3673            return;
3674        }
3675
3676        currentColorDropdown = document.createElement('div');
3677        currentColorDropdown.className = 'notion-color-dropdown';
3678
3679        const colors = [
3680            { name: 'Défaut', value: 'default', color: 'inherit', bg: 'transparent' },
3681            { name: 'Gris', value: 'gray', color: '#8a8986', bg: 'transparent' },
3682            { name: 'Marron', value: 'brown', color: '#64473a', bg: 'transparent' },
3683            { name: 'Orange', value: 'orange', color: '#d9730d', bg: 'transparent' },
3684            { name: 'Jaune', value: 'yellow', color: '#dfab01', bg: 'transparent' },
3685            { name: 'Vert', value: 'green', color: '#0f7b4b', bg: 'transparent' },
3686            { name: 'Bleu', value: 'blue', color: '#0b6e9f', bg: 'transparent' },
3687            { name: 'Violet', value: 'purple', color: '#6940a5', bg: 'transparent' },
3688            { name: 'Rose', value: 'pink', color: '#ad1a72', bg: 'transparent' },
3689            { name: 'Rouge', value: 'red', color: '#e03e3e', bg: 'transparent' }
3690        ];
3691
3692        const bgColors = [
3693            { name: 'Fond Gris', value: 'gray_background', color: 'inherit', bg: 'rgba(138, 137, 134, 0.15)' },
3694            { name: 'Fond Marron', value: 'brown_background', color: 'inherit', bg: 'rgba(100, 71, 58, 0.15)' },
3695            { name: 'Fond Orange', value: 'orange_background', color: 'inherit', bg: 'rgba(217, 115, 13, 0.15)' },
3696            { name: 'Fond Jaune', value: 'yellow_background', color: 'inherit', bg: 'rgba(223, 171, 1, 0.15)' },
3697            { name: 'Fond Vert', value: 'green_background', color: 'inherit', bg: 'rgba(15, 123, 75, 0.15)' },
3698            { name: 'Fond Bleu', value: 'blue_background', color: 'inherit', bg: 'rgba(11, 110, 159, 0.15)' },
3699            { name: 'Fond Violet', value: 'purple_background', color: 'inherit', bg: 'rgba(105, 64, 165, 0.15)' },
3700            { name: 'Fond Rose', value: 'pink_background', color: 'inherit', bg: 'rgba(173, 26, 114, 0.15)' },
3701            { name: 'Fond Rouge', value: 'red_background', color: 'inherit', bg: 'rgba(224, 62, 62, 0.15)' }
3702        ];
3703
3704        let html = '<div style="font-size: 10px; font-weight: 600; color: var(--text-muted); padding: 2px 4px 6px;">COULEUR</div>';
3705        colors.forEach(c => {
3706            html += `<button class="color-opt-btn" type="button" data-val="${c.value}" style="color: ${c.color}; text-align: left; background: none; border: none; padding: 4px 8px; border-radius: 4px; font-size: 13px; font-weight: 500; cursor: pointer; display: flex; align-items: center; gap: 6px;">${c.name}</button>`;
3707        });
3708
3709        html += '<div style="font-size: 10px; font-weight: 600; color: var(--text-muted); padding: 8px 4px 6px;">ARRIÈRE-PLAN</div>';
3710        bgColors.forEach(c => {
3711            html += `<button class="color-opt-btn" type="button" data-val="${c.value}" style="text-align: left; background: none; border: none; padding: 4px 8px; border-radius: 4px; font-size: 13px; font-weight: 500; cursor: pointer; display: flex; align-items: center; gap: 6px;"><span style="background: ${c.bg}; padding: 0 4px; border-radius: 2px; color: ${c.color}">A</span> ${c.name}</button>`;
3712        });
3713
3714        currentColorDropdown.innerHTML = html;
3715
3716        const rect = formattingToolbar.getBoundingClientRect();
3717        currentColorDropdown.style.left = `${rect.left + window.scrollX}px`;
3718        currentColorDropdown.style.top = `${rect.bottom + window.scrollY + 4}px`;
3719
3720        document.body.appendChild(currentColorDropdown);
3721
3722        currentColorDropdown.querySelectorAll('.color-opt-btn').forEach(btn => {
3723            btn.addEventListener('mousedown', (e) => {
3724                e.preventDefault();
3725                e.stopPropagation();
3726                const colorVal = btn.dataset.val;
3727                
3728                if (colorVal === 'default') {
3729                    const selection = window.getSelection();
3730                    if (selection.rangeCount > 0) {
3731                        const range = selection.getRangeAt(0);
3732                        let parent = range.commonAncestorContainer;
3733                        if (parent.nodeType === 3) parent = parent.parentNode;
3734                        const span = parent.closest('span[class*="notion-color-"]');
3735                        if (span) {
3736                            const textNode = document.createTextNode(span.textContent);
3737                            span.parentNode.replaceChild(textNode, span);
3738                        }
3739                    }
3740                } else {
3741                    wrapSelectionInTag('span', `notion-color-${colorVal}`);
3742                }
3743                
3744                triggerTextChange();
3745                closeColorDropdown();
3746                hideFormattingToolbar();
3747            });
3748        });
3749    }
3750
3751    function closeColorDropdown() {
3752        if (currentColorDropdown) {
3753            currentColorDropdown.remove();
3754            currentColorDropdown = null;
3755        }
3756    }
3757
3758    function monitorSelection() {
3759        createFormattingToolbar();
3760
3761        const handleSelectionChange = () => {
3762            const selection = window.getSelection();
3763            if (selection.isCollapsed || selection.rangeCount === 0 || selection.toString().trim() === '') {
3764                hideFormattingToolbar();
3765                return;
3766            }
3767
3768            const range = selection.getRangeAt(0);
3769            let parent = range.commonAncestorContainer;
3770            if (parent.nodeType === 3) parent = parent.parentNode;
3771
3772            const editableBlock = parent.closest('.editable-text');
3773            if (!editableBlock) {
3774                hideFormattingToolbar();
3775                return;
3776            }
3777
3778            const rects = range.getClientRects();
3779            if (rects.length > 0) {
3780                const rect = rects[0];
3781                formattingToolbar.style.display = 'flex';
3782                
3783                const toolbarWidth = formattingToolbar.offsetWidth || 230;
3784                const toolbarHeight = formattingToolbar.offsetHeight || 38;
3785                
3786                let left = rect.left + rect.width / 2 - toolbarWidth / 2 + window.scrollX;
3787                let top = rect.top - toolbarHeight - 8 + window.scrollY;
3788
3789                left = Math.max(8, left);
3790                if (left + toolbarWidth > window.innerWidth) {
3791                    left = window.innerWidth - toolbarWidth - 8;
3792                }
3793                top = Math.max(8, top);
3794
3795                formattingToolbar.style.left = `${left}px`;
3796                formattingToolbar.style.top = `${top}px`;
3797            }
3798        };
3799
3800        document.addEventListener('mouseup', () => {
3801            setTimeout(handleSelectionChange, 10);
3802        });
3803
3804        document.addEventListener('keyup', (e) => {
3805            if (e.key === 'ArrowLeft' || e.key === 'ArrowRight' || e.key === 'ArrowUp' || e.key === 'ArrowDown' || e.key === 'Shift') {
3806                setTimeout(handleSelectionChange, 10);
3807            }
3808        });
3809
3810        document.addEventListener('mousedown', (e) => {
3811            if (formattingToolbar && !formattingToolbar.contains(e.target) && (!currentColorDropdown || !currentColorDropdown.contains(e.target))) {
3812                hideFormattingToolbar();
3813            }
3814        });
3815    }
3816
3817    function hideFormattingToolbar() {
3818        if (formattingToolbar) {
3819            formattingToolbar.style.display = 'none';
3820        }
3821        closeColorDropdown();
3822    }
3823
3824    function parseMarkdownToHtml(text) {
3825        if (!text) return '';
3826        let html = escapeHtml(String(text));
3827        
3828        // Match **bold** -> <strong>bold</strong>
3829        let parts = html.split('**');
3830        let parsed = '';
3831        for (let i = 0; i < parts.length; i++) {
3832            if (i % 2 === 1) {
3833                parsed += `<strong>${parts[i]}</strong>`;
3834            } else {
3835                parsed += parts[i];
3836            }
3837        }
3838        
3839        // Match *italic* -> <em>italic</em>
3840        let partsItalic = parsed.split('*');
3841        parsed = '';
3842        for (let i = 0; i < partsItalic.length; i++) {
3843            if (i % 2 === 1) {
3844                parsed += `<em>${partsItalic[i]}</em>`;
3845            } else {
3846                parsed += partsItalic[i];
3847            }
3848        }
3849
3850        // Match ~~strike~~ -> <del>strike</del>
3851        let partsStrike = parsed.split('~~');
3852        parsed = '';
3853        for (let i = 0; i < partsStrike.length; i++) {
3854            if (i % 2 === 1) {
3855                parsed += `<del>${partsStrike[i]}</del>`;
3856            } else {
3857                parsed += partsStrike[i];
3858            }
3859        }
3860
3861        // Match `code` -> <code>code</code>
3862        let partsCode = parsed.split('`');
3863        parsed = '';
3864        for (let i = 0; i < partsCode.length; i++) {
3865            if (i % 2 === 1) {
3866                parsed += `<code>${partsCode[i]}</code>`;
3867            } else {
3868                parsed += partsCode[i];
3869            }
3870        }
3871        
3872        // Decode safe HTML tags
3873        parsed = parsed.replace(/&lt;u&gt;/gi, '<u>').replace(/&lt;\/u&gt;/gi, '</u>');
3874        parsed = parsed.replace(/&lt;span class="notion-color-([a-zA-Z_]+)"&gt;(.*?)&lt;\/span&gt;/gi, '<span class="notion-color-$1">
3874$2</span>');
3875
3876        return parsed;
3877    }
3878
3879    function convertHtmlToMarkdown(html) {
3880        if (!html) return '';
3881        
3882        let text = html;
3883        
3884        // Strip common browser tags
3885        text = text.replace(/<br>/gi, '');
3886        text = text.replace(/<div>/gi, '\n').replace(/<\/div>/gi, '');
3887        text = text.replace(/<p>/gi, '\n').replace(/<\/p>/gi, '');
3888        
3889        // Replace bold tags
3890        text = text.replace(/<(strong|b)[^>]*>(.*?)<\/\1>/gi, '**$2**');
3891        
3892        // Replace italic tags
3893        text = text.replace(/<(em|i)[^>]*>(.*?)<\/\1>/gi, '*$2*');
3894        
3895        // Replace strikethrough tags
3896        text = text.replace(/<(del|s|strike)[^>]*>(.*?)<\/\1>/gi, '~~$2~~');
3897        
3898        // Replace code tags
3899        text = text.replace(/<code[^>]*>(.*?)<\/code>/gi, '`$2`');
3900        
3901        // Keep colors tags clean
3902        text = text.replace(/<span class="notion-color-([a-zA-Z_]+)"[^>]*>(.*?)<\/span>/gi, '<span class="notion-color-$1">$2</span>');
3903        
3904        const tempDiv = document.createElement('div');
3905        tempDiv.innerHTML = text;
3906        
3907        let result = tempDiv.textContent || tempDiv.innerText || '';
3908        result = result.replace(/\u00a0/g, ' '); // replace &nbsp; with space
3909        return result;
3910    }
3911
3912    function renderBlocks(blocks) {
3913        if (!blocks.length) return '';
3914        const dailyItems = getDailyItems();
3915        return blocks.map(block => {
3916            let contentHtml = '';
3917            let todoQuickActionHtml = '';
3918            let firstFocusActivationHtml = '';
3919            const typeClass = `b-${block.type}`;
3920            const blockText = String(block.text || '');
3921            
3922            let escapedBlockText = '';
3923            if (block.notionData && block.notionData.rich_text) {
3924                escapedBlockText = NotionRichText.toHtml(block.notionData.rich_text);
3925            } else {
3926                escapedBlockText = parseMarkdownToHtml(blockText);
3927            }
3928            
3929            const blockIdArg = inlineEventString(block.id);
3930            const blockTextArg = inlineEventString(blockText);
3931
3932            if (block.type === 'divider') {
3933                contentHtml = `<div class="b-divider"></div>`;
3934            } else if (block.type === 'todo') {
3935                const priority = normalizeGeneralPriority(block.generalPriority ?? block.general_priority ?? block.priority);
3936                const priorityMeta = GENERAL_PRIORITY_META[priority];
3937                const priorityLabel = getGeneralPriorityLabel(priorityMeta);
3938                const alreadyInToday = isBlockInToday(block, dailyItems);
3939                const isFirstFocusActivation = block.id === firstFocusActivationTaskId;
3940                const priorityFlagHtml = `
3941                    <button class="task-priority-flag-button is-${priority}" type="button" data-block-id="${escapeAttribute(block.id)}"
3942                            aria-haspopup="menu" aria-expanded="false"
3943                            aria-label="${escapeAttribute(`${i18n.t('daily_priority_general')} : ${priorityLabel}`)}" title="${escapeAttribute(`${i18n.t('daily_priority_general')} : ${priorityLabel}`)}">
3944                        <span aria-hidden="true">⚑</span>
3945                    </button>
3946                `;
3947
3948                if (!block.checked && isFirstFocusActivation) {
3949                    firstFocusActivationHtml = `
3950                        <div class="first-focus-activation">
3951                            <button class="first-focus-activation-button" type="button"
3952                                    data-block-id="${escapeAttribute(block.id)}" ${blockText.trim() ? '' : 'disabled'}>
3953                                <span data-i18n="${alreadyInToday ? 'project_activation_focus_open_cta' : 'project_activation_focus_cta'}">${i18n.t(alreadyInToday ? 'project_activation_focus_open_cta' : 'project_activation_focus_cta')}</span>
3954                                <span aria-hidden="true">→</span>
3955                            </button>
3956                            <small data-i18n="project_activation_focus_hint">${i18n.t('project_activation_focus_hint')}</small>
3957                        </div>
3958                    `;
3959                } else if (!block.checked) {
3960                    todoQuickActionHtml = `
3961                        <button class="add-to-today-button${alreadyInToday ? ' is-added' : ''}" type="button"
3962                                data-block-id="${escapeAttribute(block.id)}"
3963                                aria-label="${escapeAttribute(i18n.t(alreadyInToday ? 'daily_priority_remove_today_aria' : 'daily_priority_add_today_aria'))}">
3964                            <span class="btn-label-default">${escapeHtml(i18n.t(alreadyInToday ? 'daily_priority_added_today' : 'daily_priority_add_today'))}</span>
3965                            ${alreadyInToday ? `<span class="btn-label-hover">✕ ${escapeHtml(i18n.t('daily_priority_remove'))}</span>` : ''}
3966                        </button>
3967                    `;
3968                }
3969
3970                contentHtml = `
3971                    <input type="checkbox" class="todo-check" ${block.checked ? 'checked' : ''} data-id="${block.id}">
3972                    ${priorityFlagHtml}
3973                    <div class="editable-text ${typeClass} ${block.checked ? 'checked-text' : ''}" 
3974                         contenteditable="true" 
3975                         data-id="${block.id}" 
3976                         placeholder="${escapeHtml(i18n.t('projects_block_todo'))}"
3977                         data-i18n-placeholder="projects_block_todo">${escapedBlockText}</div>
3978                `;
3979            } else if (block.type === 'page_link') {
3980                const targetPage = store.getPage(block.text); // block.text holds pageId
3981                if (targetPage) {
3982                    contentHtml = `
3983                        <div class="page-link-block" data-page-id="${targetPage.id}" contenteditable="false" style="
3984                            padding: 6px 10px; 
3985                            background: rgba(0,0,0,0.03); 
3986                            border-radius: 4px; 
3987                            cursor: pointer; 
3988                            display: flex; 
3989                            align-items: center; 
3990                            gap: 8px; 
3991                            width: fit-content;
3992                            transition: background 0.2s;">
3993                            <span style="font-size:1.1rem;">${escapeHtml(targetPage.icon || '📄')}</span>
3994                            <span style="border-bottom: 1px solid rgba(0,0,0,0.2); font-weight:500;">${escapeHtml(targetPage.title)}</span>
3995                        </div>
3996                    `;
3997                } else {
3998                    contentHtml = `<div style="color:red; opacity:0.6; font-size:0.8rem;" data-i18n="projects_page_not_found">${i18n.t('projects_page_not_found')}</div>`;
3999                }
4000            } else if (block.type === 'bulleted_list_item') {
4001                contentHtml = `
4002                    <span class="bullet-point" style="margin-right: 8px; font-size: 1.2rem; line-height: 1.5; color: var(--text-muted); user-select: none;">•</span>
4003                    <div class="editable-text ${typeClass}" 
4004                         contenteditable="true" 
4005                         data-id="${block.id}" 
4006                         placeholder="${escapeHtml(i18n.t('projects_command_placeholder') || 'Taper / pour les commandes')}">${escapedBlockText}</div>
4007                `;
4008            } else if (block.type === 'numbered_list_item') {
4009                contentHtml = `
4010                    <span class="number-point" style="margin-right: 8px; font-size: 1rem; line-height: 1.5; color: var(--text-muted); user-select: none;">1.</span>
4011                    <div class="editable-text ${typeClass}" 
4012                         contenteditable="true" 
4013                         data-id="${block.id}" 
4014                         placeholder="${escapeHtml(i18n.t('projects_command_placeholder') || 'Taper / pour les commandes')}">${escapedBlockText}</div>
4015                `;
4016            } else if (block.type === 'quote') {
4017                contentHtml = `
4018                    <div class="quote-bar" style="width: 3px; background: var(--text-muted); margin-right: 12px; border-radius: 2px; align-self: stretch;"></div>
4019                    <div class="editable-text ${typeClass}" 
4020                         contenteditable="true" 
4021                         data-id="${block.id}" 
4022                         placeholder="Citation...">${escapedBlockText}</div>
4023                `;
4024            } else if (block.type === 'callout') {
4025                contentHtml = `
4026                    <div class="callout-box" style="display: flex; gap: 12px; background: var(--hover-bg); padding: 12px 16px; border-radius: 6px; width: 100%;">
4027                        <span>💡</span>
4028                        <div class="editable-text ${typeClass}" 
4029                             contenteditable="true" 
4030                             data-id="${block.id}" 
4031                             style="flex: 1; padding: 0;"
4032                             placeholder="Texte de mise en évidence...">${escapedBlockText}</div>
4033                    </div>
4034                `;
4035            } else if (block.type === 'code') {
4036                contentHtml = `
4037                    <div class="code-box" style="background: rgba(0,0,0,0.05); padding: 12px 16px; border-radius: 6px; width: 100%; font-family: monospace; overflow-x: auto;">
4038                        <div class="editable-text ${typeClass}" 
4039                             contenteditable="true" 
4040                             data-id="${block.id}" 
4041                             style="padding: 0;"
4042                             placeholder="Code...">${escapedBlockText}</div>
4043                    </div>
4044                `;
4045            } else if (block.type === 'unsupported' || block.type === 'table' || block.type === 'column_list' || block.type === 'column' || block.type === 'image' || block.type === 'video' || block.type === 'bookmark') {
4046                contentHtml = `
4047                    <div class="unsupported-box" style="display: flex; gap: 8px; background: rgba(0,0,0,0.03); padding: 8px 12px; border-radius: 4px; width: 100%; border: 1px dashed rgba(0,0,0,0.1); user-select: none;">
4048                        <span style="opacity:0.6;">📦</span>
4049                        <div style="flex: 1; opacity:0.6; font-size: 0.9em; display: flex; align-items: center;">
4050                            Bloc ${block.type} (lecture seule)
4051                        </div>
4052                    </div>
4053                `;
4054            } else {
4055                let placeholderKey = block.type === 'p' ? 'projects_command_placeholder' : '';
4056                if (block.type === 'h1') placeholderKey = 'projects_block_h1';
4057                if (block.type === 'h2') placeholderKey = 'projects_block_h2';
4058                const placeholder = placeholderKey ? i18n.t(placeholderKey) : block.type;
4059
4060                contentHtml = `
4061                    <div class="editable-text ${typeClass}" 
4062                         contenteditable="true" 
4063                         data-id="${block.id}" 
4064                         placeholder="${escapeHtml(placeholder)}"
4065                         ${placeholderKey ? `data-i18n-placeholder="${placeholderKey}"` : ''}>${escapedBlockText}</div>
4066                `;
4067            }
4068
4069            const level = block.level || 0;
4070            const indent = level * 24;
4071
4072            const draggableAttr = isTouchLayout() ? 'false' : 'true';
4073
4074            const blockColor = (block.notionData && block.notionData.color && block.notionData.color !== 'default') ? block.notionData.color : '';
4075            const blockColorClass = blockColor ? ` notion-color-${blockColor}` : '';
4076
4077            return `
4078                <div class="editor-block${blockColorClass}" 
4079                     data-id="${block.id}" 
4080                     data-type="${block.type}"
4081                     role="option"
4082                     aria-selected="false"
4083                     draggable="false"
4084                     style="padding-left: ${indent}px;">
4085                    <div class="block-controls">
4086                        <button class="block-plus" type="button" data-i18n-title="projects_add_block" data-i18n-aria-label="projects_add_block"
4087                                title="${escapeAttribute(i18n.t('projects_add_block'))}" aria-label="${escapeAttribute(i18n.t('projects_add_block'))}"
4088                                onclick="window.Roxima.handlePlusClick(event, ${blockIdArg})">+</button>
4089                        <button class="block-handle" type="button" data-i18n-title="common_actions" data-i18n-aria-label="common_actions"
4090                                title="${escapeAttribute(i18n.t('common_actions'))}" aria-label="${escapeAttribute(i18n.t('common_actions'))}">⋮⋮</button>
4091                    </div>
4092                    <div class="block-content-wrapper">
4093                        ${contentHtml}
4094                    </div>
4095                    ${todoQuickActionHtml}
4096                    ${firstFocusActivationHtml}
4097                    <button class="mobile-task-action" data-id="${block.id}" type="button" data-i18n-title="projects_task_actions" data-i18n-aria-label="projects_task_actions">⋯</button>
4098                </div>
4099            `;
4100        }).join('');
4101    }
4102
4103    // --- DRAG & DROP LOGIC ---
4104    let draggedBlockId = null;
4105
4106    function initDragDrop(container) {
4107        // We attach listeners to the container (delegation) for drag events where possible, 
4108        // but dragstart/dragover often need to be on specific elements or bubble up. 
4109        // Bubbling works fine.
4110
4111        container.addEventListener('dragstart', (e) => {
4112            const blockEl = e.target.closest('.editor-block');
4113            if (!blockEl) return;
4114
4115            // ONLY Allow drag if handle is the target (or inside handle)
4116            // But HTML5 Drag image behaves better if drag is on the row.
4117            // We check if the mouse started on the handle.
4118            // However, 'dragstart' event doesn't give mouse position or original click target easily if we didn't track mousedown.
4119            // WORKAROUND: We'll assume if the user is dragging the block, they grabbed the handle because standard text selection behaves differently.
4120            // But to be precise like Notion, we should verify the handle.
4121            // Since we can't easily check 'mousedown' target here without global state, 
4122            // we will check if the 'handle' is hovered or if we trust the user intent.
4123            // Actually, if 'draggable=true' is on the block, clicking text and dragging selects text usually. 
4124            // We need to prevent drag unless handle is grabbed.
4125            // Let's rely on a global 'isHandleGrabbed' flag set by mousedown.
4126        });
4127    }
4128
4129    // We'll reimplement initDragDrop properly below and call it from render or bindGlobalEvents.
4130    // Instead of complex init, let's just add event listeners to the Editor Container in bindGlobalEvents.
4131
4132
4133    // --- PLUS MENU LOGIC ---
4134    function initPlusMenu(menuEl) {
4135        // Render Options (same as Slash but clickable)
4136        const OPTIONS = [
4137            ...SLASH_OPTIONS,
4138            { label: 'JSON 📋', type: 'export_json', icon: '📋' }
4139        ];
4140
4141        menuEl.innerHTML = OPTIONS.map((opt) => `
4142            <div class="slash-item" data-type="${opt.type}">
4143                <span class="slash-icon">${opt.icon}</span>
4144                <span ${opt.labelKey ? `data-i18n="${opt.labelKey}"` : ''}>${optionLabel(opt)}</span>
4145            </div>
4146        `).join('');
4147    }
4148
4149    // --- MENU LOGIC (Combined Slash & Plus) --- 
4150
4151    function openMenu(targetEl, blockId) {
4152        // Reuse slash-menu container since we know it works
4153        const menuEl = document.getElementById('slash-menu');
4154        if (!menuEl) return;
4155
4156        // Determine Mode
4157        isPlusMenuMode = targetEl.classList.contains('block-plus') || !!targetEl.closest('.block-plus');
4158
4159        // Render Options (Full set for menu)
4160        const OPTIONS = [
4161            ...SLASH_OPTIONS,
4162            // Only add JSON if not already there or if we want it in all menus
4163            { label: 'JSON 📋', type: 'export_json', icon: '📋' }
4164        ];
4165
4166        menuEl.innerHTML = OPTIONS.map((opt) => `
4167            <div class="slash-item" data-type="${opt.type}">
4168                <span class="slash-icon">${opt.icon}</span>
4169                <span ${opt.labelKey ? `data-i18n="${opt.labelKey}"` : ''}>${optionLabel(opt)}</span>
4170            </div>
4171        `).join('');
4172
4173        // Show
4174        menuEl.style.display = 'flex';
4175        slashMenuOpen = true;
4176        slashMenuBlockId = blockId; // Use same ID tracker for both
4177
4178        // Position
4179        const rect = targetEl.getBoundingClientRect();
4180
4181        if (isPlusMenuMode) {
4182            menuEl.style.top = rect.top + 'px';
4183            menuEl.style.left = (rect.right + 5) + 'px';
4184        } else {
4185            menuEl.style.top = (rect.bottom + 5) + 'px';
4186            menuEl.style.left = rect.left + 'px';
4187        }
4188    }
4189
4190    // Alias for existing slash calls
4191    function openSlashMenu(targetInput, blockId) {
4192        openMenu(targetInput, blockId);
4193    }
4194
4195    function closeSlashMenu() {
4196        slashMenuOpen = false;
4197        slashMenuBlockId = null;
4198        isPlusMenuMode = false;
4199        const menuEl = document.getElementById('slash-menu');
4200        if (menuEl) menuEl.style.display = 'none';
4201    }
4202
4203    // Global Handler for Plus Click (Consolidated)
4204    window.Roxima.handlePlusClick = (e, blockId) => {
4205        if (e) {
4206            e.preventDefault();
4207            e.stopPropagation();
4208        }
4209        console.log("Direct Plus Click Unified:", blockId);
4210        const btn = e.currentTarget || e.target.closest('.block-plus');
4211        openMenu(btn, blockId);
4212    };
4213
4214    // --- CONTEXT MENU (Handle Click) ---
4215    function openContextMenu(targetEl, blockId) {
4216        const menuEl = document.getElementById('slash-menu');
4217        if (!menuEl) return;
4218
4219        const pageId = selectedPageId;
4220        const block = store.getBlocks(pageId).find(b => b.id === blockId);
4221
4222        // Determine Mode
4223        isPlusMenuMode = true; // Acts like plus menu for behavior
4224
4225        // Options: Delete Block
4226        const OPTIONS = [
4227            { label: 'Supprimer', labelKey: 'common_delete', type: 'delete_block', icon: '🗑️' }
4228        ];
4229
4230        let prioritySelectorHtml = '';
4231        if (block && block.type === 'todo') {
4232            prioritySelectorHtml = `
4233                <div class="context-menu-priority-selector" style="padding: 8px 12px; border-bottom: 1px solid rgba(0,0,0,0.06); display:flex; flex-direction:column; gap:6px;">
4234                    <div style="font-size:0.75rem; font-weight:600; color:#888; text-align:left;">Priorité</div>
4235                    <div style="display:flex; gap:8px; align-items:center;">
4236                        <button class="ctx-priority-btn" data-priority="3" title="Haute" style="background:none; border:none; padding:4px; border-radius:4px; cursor:pointer;">🚩</button>
4237                        <button class="ctx-priority-btn" data-priority="2" title="Moyenne" style="background:none; border:none; padding:4px; border-radius:4px; cursor:pointer;">🟧</button>
4238                        <button class="ctx-priority-btn" data-priority="1" title="Basse" style="background:none; border:none; padding:4px; border-radius:4px; cursor:pointer;">🟦</button>
4239                        <button class="ctx-priority-btn" data-priority="0" title="Aucune" style="background:none; border:none; padding:4px; border-radius:4px; cursor:pointer; opacity:0.5;">🏳️</button>
4240                    </div>
4241                </div>
4242            `;
4243        }
4244
4245        menuEl.innerHTML = `
4246            ${prioritySelectorHtml}
4247            ${OPTIONS.map((opt) => `
4248                <div class="slash-item" data-type="${opt.type}">
4249                    <span class="slash-icon">${opt.icon}</span>
4250                    <span ${opt.labelKey ? `data-i18n="${opt.labelKey}"` : ''}>${optionLabel(opt)}</span>
4251                </div>
4252            `).join('')}
4253        `;
4254
4255        // Bind priority buttons
4256        if (block && block.type === 'todo') {
4257            menuEl.querySelectorAll('.ctx-priority-btn').forEach(btn => {
4258                const btnPriority = parseInt(btn.dataset.priority, 10);
4259                if (btnPriority === (block.priority || 0)) {
4260                    btn.classList.add('active');
4261                }
4262                btn.addEventListener('click', (e) => {
4263                    e.stopPropagation();
4264                    store.updateBlock(pageId, blockId, { priority: btnPriority });
4265                    render();
4266                    if (ctx.bus) ctx.bus.emit('objectifs:refresh');
4267                    closeSlashMenu();
4268                });
4269            });
4270        }
4271
4272        // Show
4273        menuEl.style.display = 'flex';
4274        slashMenuOpen = true;
4275        slashMenuBlockId = blockId;
4276
4277        const rect = targetEl.getBoundingClientRect();
4278        menuEl.style.top = (rect.bottom + 5) + 'px';
4279        menuEl.style.left = rect.left + 'px';
4280    }
4281
4282    // Update handlePlusAction to handle delete
4283    function handlePlusAction(type) {
4284        const store = objectifsStore;
4285
4286        if (type === 'delete_block') {
4287            const blockObj = store.getBlocks(selectedPageId).find(b => b.id === slashMenuBlockId);
4288            if (blockObj && blockObj.isNotion && blockObj.notionBlockId) {
4289                deleteBlockFromNotion(selectedPageId, blockObj.notionBlockId);
4290            }
4291            store.deleteBlock(selectedPageId, slashMenuBlockId);
4292            render();
4293            closeSlashMenu();
4294            return;
4295        }
4296
4297        if (type === 'export_json') {
4298            const json = JSON.stringify(store.state, null, 2);
4299            console.log(json);
4300            navigator.clipboard.writeText(json).then(() => alert(i18n.t('projects_json_copied')));
4301            closeSlashMenu();
4302            return;
4303        }
4304
4305        if (type === 'page_link') {
4306            const title = prompt(i18n.t('projects_new_page_prompt')) || i18n.t('common_untitled');
4307            const newPage = store.createPage(title, selectedPageId);
4308            store.addBlock(selectedPageId, { type: 'page_link', text: newPage.id }, slashMenuBlockId);
4309            closeSlashMenu();
4310            render();
4311            return;
4312        }
4313
4314        // Add new block AFTER the current one (using slashMenuBlockId as the reference)
4315        console.log("Adding block after:", slashMenuBlockId, "Type:", type);
4316        store.addBlock(selectedPageId, { type: type, text: '' }, slashMenuBlockId);
4317        render();
4318        // Focus usually handled by render loop looking for empty blocks or we can force it?
4319        // Let's rely on render for now.
4320        closeSlashMenu();
4321    }
4322
4323    // -- UTILS --
4324    function debounce(func, wait) {
4325        let timeout;
4326        let lastArgs = null;
4327        let lastThis = null;
4328        const debounced = function (...args) {
4329            clearTimeout(timeout);
4330            lastArgs = args;
4331            lastThis = this;
4332            timeout = setTimeout(() => {
4333                timeout = null;
4334                const pendingArgs = lastArgs;
4335                const pendingThis = lastThis;
4336                lastArgs = null;
4337                lastThis = null;
4338                func.apply(pendingThis, pendingArgs);
4339            }, wait);
4340        };
4341        debounced.cancel = function () {
4342            clearTimeout(timeout);
4343            timeout = null;
4344            lastArgs = null;
4345            lastThis = null;
4346        };
4347        debounced.flush = function () {
4348            if (!timeout || !lastArgs) return;
4349            clearTimeout(timeout);
4350            timeout = null;
4351            const pendingArgs = lastArgs;
4352            const pendingThis = lastThis;
4353            lastArgs = null;
4354            lastThis = null;
4355            func.apply(pendingThis, pendingArgs);
4356        };
4357        return debounced;
4358    }
4359
4360    const debouncedUpdateBlock = debounce((store, pageId, id, text, notionData) => {
4361        store.updateBlock(pageId, id, { text, notionData });
4362    }, 200);
4363
4364    function commitEditableBlockNow(store, pageId, editor) {
4365        if (!editor?.classList?.contains('editable-text')) return null;
4366
4367        const id = editor.dataset.id;
4368        const html = editor.innerHTML;
4369        const text = convertHtmlToMarkdown(html);
4370        const block = (store.getBlocks(pageId) || [])
4371            .find(item => String(item.id) === String(id));
4372
4373        // A delayed write must never overwrite this authoritative DOM value.
4374        debouncedUpdateBlock.cancel();
4375        if (!block) return null;
4376
4377        const notionData = { ...(block.notionData || {}) };
4378        notionData.rich_text = NotionRichText.fromHtml(html);
4379        store.updateBlock(pageId, id, { text, notionData });
4380        return { id, text };
4381    }
4382
4383    const debouncedSyncBlockToNotion = debounce((pageId, id) => {
4384        syncBlockToNotion(pageId, id);
4385    }, 1500);
4386
4387    // -- EDITOR LOGIC --
4388    function initEditor(editorEl, pageId, bus) {
4389        const store = objectifsStore;
4390
4391        editorEl.addEventListener('input', (e) => {
4392            const target = e.target;
4393            if (target.classList.contains('editable-text')) {
4394                const id = target.dataset.id;
4395                const html = target.innerHTML;
4396                const text = convertHtmlToMarkdown(html);
4397
4398                if (id === firstFocusActivationTaskId) {
4399                    const activationButton = target.closest('.editor-block')?.querySelector('.first-focus-activation-button');
4400                    if (activationButton && !activationButton.hasAttribute('aria-busy')) {
4401                        activationButton.disabled = !text.trim();
4402                    }
4403                }
4404
4405                // SPECIAL: Slash command trigger immediate check (no debounce)
4406                if (text === '/') {
4407                    openSlashMenu(target, id);
4408                } else if (slashMenuOpen && !text.startsWith('/')) {
4409                    closeSlashMenu();
4410                }
4411
4412                const blocks = store.getBlocks(pageId);
4413                const block = blocks ? blocks.find(b => b.id === id) : null;
4414                let newNotionData = block ? { ...block.notionData } : {};
4415                newNotionData.rich_text = NotionRichText.fromHtml(html);
4416
4417                // Normal text update -> Debounced save
4418                debouncedUpdateBlock(store, pageId, id, text, newNotionData);
4419
4420                // Auto-push to Notion after a pause in typing
4421                debouncedSyncBlockToNotion(pageId, id);
4422            }
4423        });
4424
4425        editorEl.addEventListener('change', (e) => {
4426            if (e.target.classList.contains('todo-check')) {
4427                const id = e.target.dataset.id;
4428                const checked = e.target.checked;
4429                store.updateBlock(pageId, id, { checked });
4430                // Visual update immediately
4431                const textEl = e.target.closest('.editor-block')?.querySelector('.editable-text');
4432                if (textEl) {
4433                    if (checked) textEl.classList.add('checked-text'); else textEl.classList.remove('checked-text');
4434                }
4435                if (bus) bus.emit('objectifs:refresh');
4436                syncBlockToNotion(pageId, id); // Sync checkbox toggle
4437            }
4438        });
4439
4440        editorEl.addEventListener('focusout', (e) => {
4441            const target = e.target;
4442            if (target.classList.contains('editable-text')) {
4443                const id = target.dataset.id;
4444                if (editorsCommittedBeforeFocusout.has(target)) {
4445                    editorsCommittedBeforeFocusout.delete(target);
4446                } else {
4447                    // A click can rebuild the editor before the typing debounce fires.
4448                    commitEditableBlockNow(store, pageId, target);
4449                }
4450                // Cancel debounced sync and push immediately
4451                debouncedSyncBlockToNotion.cancel();
4452                // Keep the existing short delay for the remote Notion sync only.
4453                setTimeout(() => {
4454                    syncBlockToNotion(pageId, id);
4455                }, 250);
4456            }
4457        });
4458
4459        editorEl.addEventListener('keydown', (e) => {
4460            const blockControl = e.target.closest('.block-controls button');
4461            if (blockControl && (e.key === 'Enter' || e.key === ' ')) {
4462                e.preventDefault();
4463                e.stopPropagation();
4464                blockControl.click();
4465                return;
4466            }
4467
4468            handleEditorKeydown_v2(e, store, pageId, editorEl, bus);
4469        });
4470
4471        // PASTE HANDLER (Smart Paste)
4472        editorEl.addEventListener('paste', (e) => {
4473            const clipboardData = e.clipboardData || window.clipboardData;
4474            if (!clipboardData) return;
4475
4476            const pastedText = clipboardData.getData('text');
4477            if (!pastedText) return;
4478
4479            // Only intervene if it looks like a list or multiple lines
4480            const lines = pastedText.split(/\r?\n/);
4481            const isListLike = lines.length > 1 || /^\s*(-|\*|\[\s?\]|\[x\])\s/.test(lines[0]);
4482
4483            if (isListLike) {
4484                e.preventDefault();
4485
4486                const target = e.target;
4487                const currentBlockEl = target.closest('.editor-block');
4488                if (!currentBlockEl) return;
4489
4490                const currentId = currentBlockEl.dataset.id;
4491                let insertAfterId = currentId;
4492
4493                // Process first line into CURRENT block? 
4494                // Only if current block is empty or we want to append?
4495                // Standard behavior: replace selection or append. 
4496                // Simpler approach: update current block with first line content, add others after.
4497
4498                // regexes - improved to catch leading bullets on Todos
4499                const regexTodo = /^\s*(-|\*)?\s*\[( |x)\]\s+(.*)/i;
4500                const regexBullet = /^\s*(-|\*)\s+(.*)/;
4501
4502                // Detect indentation context
4503                const rootBlock = store.getBlocks(pageId).find(b => String(b.id) === currentId);
4504                const baseLevel = rootBlock ? (rootBlock.level || 0) : 0;
4505                let firstLineRawIndent = null;
4506
4507                lines.forEach((line, index) => {
4508                    let type = 'p';
4509                    let text = line;
4510                    let checked = false;
4511                    let level = baseLevel;
4512
4513                    // Calculate Indentation
4514                    const leadingSpace = line.match(/^\s*/)[0];
4515                    const tabCount = (leadingSpace.match(/\t/g) || []).length;
4516                    const spaceCount = (leadingSpace.match(/ /g) || []).length;
4517                    const currentRawIndent = tabCount + Math.floor(spaceCount / 2); // 2 spaces = 1 level
4518
4519                    if (index === 0) {
4520                        firstLineRawIndent = currentRawIndent;
4521                    } else {
4522                        const indentDelta = Math.max(0, currentRawIndent - firstLineRawIndent);
4523                        level = baseLevel + indentDelta;
4524                    }
4525
4526                    // Check Type
4527                    const matchTodo = line.match(regexTodo);
4528                    if (matchTodo) {
4529                        type = 'todo';
4530                        checked = matchTodo[2].toLowerCase() === 'x';
4531                        text = matchTodo[3];
4532                    } else {
4533                        const matchBullet = line.match(regexBullet);
4534                        if (matchBullet) {
4535                            type = 'bulleted_list_item';
4536                            text = matchBullet[2];
4537                        } else {
4538                            // Strip leading indent from P text
4539                            text = line.replace(/^\s+/, '');
4540                        }
4541                    }
4542
4543                    if (index === 0) {
4544                        store.updateBlock(pageId, insertAfterId, { type, text, checked, level });
4545                    } else {
4546                        const newBlock = store.addBlock(pageId, { type, text, checked, level }, insertAfterId);
4547                        insertAfterId = newBlock.id;
4548                    }
4549                });
4550
4551
4552                render({ preserveCaret: false });
4553                // Focus insertion point (last block)
4554                setTimeout(() => focusBlock(insertAfterId, 'end'), 0);
4555            }
4556        });
4557    }
4558
4559    // --- UTILS ---
4560    function focusBlock(blockId, position = 'end') {
4561        const applyFocus = () => {
4562            const el = container.querySelector(`.editor-block[data-id="${blockId}"] .editable-text`);
4563            if (!el) return false;
4564            el.focus();
4565            if (typeof position === 'number') {
4566                let remaining = Math.max(0, position);
4567                let caretNode = el;
4568                let caretOffset = el.childNodes.length;
4569                const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
4570                let textNode = walker.nextNode();
4571
4572                while (textNode) {
4573                    const textLength = textNode.textContent?.length || 0;
4574                    if (remaining <= textLength) {
4575                        caretNode = textNode;
4576                        caretOffset = remaining;
4577                        break;
4578                    }
4579                    remaining -= textLength;
4580                    textNode = walker.nextNode();
4581                }
4582
4583                const range = document.createRange();
4584                range.setStart(caretNode, caretOffset);
4585                range.collapse(true);
4586                const sel = window.getSelection();
4587                sel.removeAllRanges();
4588                sel.addRange(range);
4589            } else if (position === 'end') {
4590                const range = document.createRange();
4591                range.selectNodeContents(el);
4592                range.collapse(false);
4593                const sel = window.getSelection();
4594                sel.removeAllRanges();
4595                sel.addRange(range);
4596            } else if (position === 'start') {
4597                const range = document.createRange();
4598                range.selectNodeContents(el);
4599                range.collapse(true);
4600                const sel = window.getSelection();
4601                sel.removeAllRanges();
4602                sel.addRange(range);
4603            }
4604            return true;
4605        };
4606
4607        // render() replaces the editor synchronously, so the destination normally
4608        // exists right away. Keep one animation-frame fallback for async callers.
4609        if (!applyFocus()) requestAnimationFrame(applyFocus);
4610    }
4611
4612    // --- KEYDOWN HANDLER ---
4613    function handleEditorKeydown(e, store, pageId, editorEl) {
4614        const target = e.target;
4615
4616        // 1. Slash Menu Navigation (Priority)
4617        if (slashMenuOpen) {
4618            if (e.key === 'ArrowDown') {
4619                e.preventDefault();
4620                slashMenuIndex = (slashMenuIndex + 1) % SLASH_OPTIONS.length;
4621                renderSlashMenu(document.getElementById('slash-menu'));
4622                return;
4623            } else if (e.key === 'ArrowUp') {
4624                e.preventDefault();
4625                slashMenuIndex = (slashMenuIndex - 1 + SLASH_OPTIONS.length) % SLASH_OPTIONS.length;
4626                renderSlashMenu(document.getElementById('slash-menu'));
4627                return;
4628            } else if (e.key === 'Enter') {
4629                e.preventDefault();
4630                applySlashType(pageId);
4631                return;
4632            } else if (e.key === 'Escape') {
4633                e.preventDefault();
4634                closeSlashMenu();
4635                return;
4636            }
4637        }
4638
4639        // 0. SELECTION: Escape to clear
4640        if (e.key === 'Escape') {
4641            if (selection.ids.size > 0) {
4642                selection.clear();
4643                e.preventDefault();
4644                return;
4645            }
4646        }
4647
4648        // 0. SELECTION: Shift + Arrows (Expand Selection)
4649        if (e.shiftKey && (e.key === 'ArrowUp' || e.key === 'ArrowDown')) {
4650            if (selection.ids.size > 0) {
4651                e.preventDefault();
4652                const allBlocks = Array.from(editorEl.querySelectorAll('.editor-block')).map(el => el.dataset.id);
4653                // Last selected is conceptually the one we moved do last, or anchor if fresh.
4654                // We need to know "active end". For simplicity, let's assume SetRange covers it if we pick target correctly.
4655                // Re-calculating range based on anchor + direction.
4656                // Let's find the "other end" of the current range relative to anchor.
4657                // Or just find the extrema.
4658
4659                // Simple implementation: Just extend from anchor to current focused? No focus might be lost.
4660                // Extend from anchor to neighbor of current "extent". 
4661                // Hard to know "extent" without state. 
4662                // Let's assume extent is the block visually furthest from anchor in the set.
4663
4664                const sorted = selection.getSortedIds(allBlocks);
4665                const anchorIdx = allBlocks.indexOf(selection.anchorBlockId);
4666                const firstIdx = allBlocks.indexOf(sorted[0]);
4667                const lastIdx = allBlocks.indexOf(sorted[sorted.length - 1]);
4668
4669                let currentExtentIdx = (anchorIdx === firstIdx) ? lastIdx : firstIdx;
4670                if (sorted.length === 1) currentExtentIdx = anchorIdx;
4671
4672                let nextIdx = currentExtentIdx;
4673                if (e.key === 'ArrowUp') nextIdx = Math.max(0, currentExtentIdx - 1); // Move extent up?
4674                // Logic is tricky: If I am below anchor, Up shrinks. If I am above anchor, Up expands. 
4675                // Let's do a naive "Move the cursor block" approach if we had a cursor.
4676                // Without cursor, let's just use the "most recent added" logic or simply direction from anchor.
4677
4678                // SIMPLIFIED: Just select neighbor of 'currentBlockEl' if exists, else extend from anchor.
4679                if (currentBlockEl) {
4680                    // If we are editing text and press Shift+Up, standard text selection happens.
4681                    // We only want block selection if we explicitly started it.
4682                    // But user said "Shift + Arrow ... étend la sélection".
4683                }
4684
4685                // Let's rely on standard text selection if inside text. 
4686                // ONLY handle this if selection.ids.size > 0 (Block Mode).
4687
4688                // Direction Logic:
4689                if (e.key === 'ArrowUp') nextIdx = Math.max(0, currentExtentIdx - 1);
4690                if (e.key === 'ArrowDown') nextIdx = Math.min(allBlocks.length - 1, currentExtentIdx + 1);
4691
4692                // Wait, if I am at (Anchor=5, Extent=10) and press UP. I was at 10. Go to 9. range 5-9.
4693                // If I am at (Anchor=5, Extent=10) and press DOWN. Go to 11. range 5-11.
4694                // If I am at (Anchor=5, Extent=0) and press UP. Go to -1 (limit 0). 
4695                // If I am at (Anchor=5, Extent=0) and press DOWN. Go to 1.
4696
4697                // So we need to know the 'active' edge. Storing 'extent' in state would be better, but 'currentBlockEl' helps if focus is there.
4698                // If focus is lost (clicked gutter), we need state. 
4699                // Let's trust that 'anchor' is static. We guess extent.
4700                // Pivot logic:
4701                let targetIdx = currentExtentIdx;
4702                if (currentExtentIdx >= anchorIdx) {
4703                    // We are below or at anchor.
4704                    if (e.key === 'ArrowDown') targetIdx++;
4705                    else targetIdx--; // Up shrinks or flips
4706                } else {
4707                    // We are above anchor.
4708                    if (e.key === 'ArrowUp') targetIdx--;
4709                    else targetIdx++; // Down shrinks or flips
4710                }
4711
4712                targetIdx = Math.max(0, Math.min(allBlocks.length - 1, targetIdx));
4713                selection.setRange(allBlocks[targetIdx], allBlockIds);
4714                return;
4715            }
4716        }
4717
4718        // 0. SELECTION: Delete / Backspace
4719        if ((e.key === 'Delete' || e.key === 'Backspace') && selection.ids.size > 0) {
4720            e.preventDefault();
4721            const sortedIds = selection.getSortedIds(Array.from(editorEl.querySelectorAll('.editor-block')).map(el => el.dataset.id));
4722
4723            const firstId = sortedIds[0];
4724            const firstEl = editorEl.querySelector(`.editor-block[data-id="${firstId}"]`);
4725            const prevEl = firstEl ? firstEl.previousElementSibling : null;
4726
4727            sortedIds.forEach(id => store.deleteBlock(pageId, id));
4728            selection.clear();
4729            render({ preserveCaret: false });
4730
4731            if (prevEl) {
4732                focusBlock(prevEl.dataset.id, 'end');
4733            } else if (store.getBlocks(pageId).length === 0) {
4734                store.addBlock(pageId, { type: 'p', text: '' });
4735                render({ preserveCaret: false });
4736                const newBlk = editorEl.querySelector('.editor-block');
4737                if (newBlk) focusBlock(newBlk.dataset.id);
4738            }
4739            return;
4740        }
4741
4742        // 0. SELECTION: Copy (Ctrl+C)
4743        if ((e.ctrlKey || e.metaKey) && e.key === 'c' && selection.ids.size > 0) {
4744            e.preventDefault();
4745            const sortedIds = selection.getSortedIds(Array.from(editorEl.querySelectorAll('.editor-block')).map(el => el.dataset.id));
4746            const textToCopy = sortedIds.map(id => {
4747                const block = store.state.blocksByPageId[pageId].find(b => String(b.id) === id);
4748                if (!block) return '';
4749                if (block.type === 'todo') return block.checked ? `- [x] ${block.text}` : `- [ ] ${block.text}`;
4750                if (block.type === 'bulleted_list_item' || block.type === 'bullet') return `- ${block.text}`;
4751                if (block.type === 'numbered_list_item') return `1. ${block.text}`;
4752                return block.text;
4753            }).join('\n');
4754            navigator.clipboard.writeText(textToCopy);
4755            return;
4756        }
4757
4758        // 0. PASTE (Generic)
4759        if ((e.ctrlKey || e.metaKey) && e.key === 'v') {
4760            // If selection active, delete it first? 
4761            if (selection.ids.size > 0) {
4762                // Simulate delete then paste? 
4763                // Implement later. Paste usually handled by 'paste' event, but we want custom block paste.
4764                // Let's rely on browser default for text paste unless we intercepted it.
4765                // If we want to intercept paste to create blocks, we need 'paste' event listener, not keydown.
4766            }
4767        }
4768
4769
4770        if (!target.classList.contains('editable-text')) {
4771            // Allow navigation if we have selection, otherwise return
4772            if (selection.ids.size === 0) return;
4773            // If we have selection and pressed arrow (handled above), or other keys?
4774            // If we are here, we might want to prevent default for some keys?
4775        }
4776
4777        // Ensure we have currentBlockEl for standard operations
4778        const currentBlockEl = target.closest('.editor-block');
4779        if (!currentBlockEl) return;
4780
4781        const currentId = currentBlockEl.dataset.id;
4782        const currentType = currentBlockEl.dataset.type;
4783
4784        // 2. Ctrl + Enter (Toggle Todo)
4785        if (e.ctrlKey && e.key === 'Enter') {
4786            if (currentType === 'todo') {
4787                e.preventDefault();
4788                const checkbox = currentBlockEl.querySelector('.todo-check');
4789                if (checkbox) {
4790                    checkbox.checked = !checkbox.checked;
4791                    store.updateBlock(pageId, currentId, { checked: checkbox.checked });
4792                    const textEl = currentBlockEl.querySelector('.editable-text');
4793                    if (checkbox.checked) textEl.classList.add('checked-text'); else textEl.classList.remove('checked-text');
4794                }
4795            }
4796            return;
4797        }
4798
4799        // 3. Enter (New Block)
4800        if (e.key === 'Enter' && !e.shiftKey) {
4801            e.preventDefault();
4802
4803            // Should next block inherit type?
4804            // Todo -> Todo, Bullet -> Bullet. Others (H1, H2, P) -> P
4805            let nextType = 'p';
4806            if (currentType === 'todo') nextType = 'todo';
4807            if (currentType === 'bullet') nextType = 'bullet';
4808
4809            // Add new block AFTER current
4810            store.addBlock(pageId, { type: nextType, text: '' }, currentId);
4811            render();
4812
4813            // Focus new block (Wait for render)
4814            setTimeout(() => {
4815                // We need to find the block AFTER the one we were just in.
4816                // Since we re-rendered, DOM refs are lost. Use querySelector by ID to find old one, then nextSibling.
4817                const oldBlockRef = editorEl.querySelector(`.editor-block[data-id="${currentId}"]`);
4818                if (oldBlockRef && oldBlockRef.nextElementSibling) {
4819                    const nextInput = oldBlockRef.nextElementSibling.querySelector('.editable-text');
4820                    if (nextInput) nextInput.focus();
4821                }
4822            }, 0);
4823            return;
4824        }
4825
4826        // 4. Backspace (Delete empty)
4827        if (e.key === 'Backspace') {
4828            if (target.innerText === '') {
4829                e.preventDefault();
4830
4831                // Find previous block ID to focus BEFORE deleting
4832                const prevBlockEl = currentBlockEl.previousElementSibling;
4833
4834                store.deleteBlock(pageId, currentId);
4835                render();
4836
4837                if (prevBlockEl) {
4838                    // Focus end of previous
4839                    setTimeout(() => {
4840                        const prevInput = editorEl.querySelector(`.editor-block[data-id="${prevBlockEl.dataset.id}"] .editable-text`);
4841                        if (prevInput) {
4842                            prevInput.focus();
4843                            // Move cursor to end
4844                            const range = document.createRange();
4845                            range.selectNodeContents(prevInput);
4846                            range.collapse(false);
4847                            const sel = window.getSelection();
4848                            sel.removeAllRanges();
4849                            sel.addRange(range);
4850                        }
4851                    }, 0);
4852                } else {
4853                    // If it was the first block, maybe recreate a default P if list empty?
4854                    // Checked in render(): checks blocks.length. If 0, shows empty state.
4855                }
4856                return;
4857            }
4858        }
4859
4860        // 5. Arrow Navigation
4861        if (e.key === 'ArrowUp') {
4862            const prevBlock = currentBlockEl.previousElementSibling;
4863            if (prevBlock) {
4864                e.preventDefault();
4865                const input = prevBlock.querySelector('.editable-text');
4866                if (input) input.focus(); // Simple focus, keeping cursor pos is hard across blocks
4867            }
4868        } else if (e.key === 'ArrowDown') {
4869            const nextBlock = currentBlockEl.nextElementSibling;
4870            if (nextBlock) {
4871                e.preventDefault();
4872                const input = nextBlock.querySelector('.editable-text');
4873                if (input) input.focus();
4874            }
4875        }
4876    }
4877
4878    // --- KEYDOWN HANDLER V2 (Refined) ---
4879    function handleEditorKeydown_v2(e, store, pageId, editorEl, bus) {
4880        const target = e.target;
4881
4882        // 1. Slash Menu Navigation
4883        if (slashMenuOpen) {
4884            if (e.key === 'ArrowUp') {
4885                e.preventDefault();
4886                slashMenuIndex = Math.max(0, slashMenuIndex - 1);
4887                renderSlashMenu(document.getElementById('slash-menu'));
4888                return;
4889            }
4890            if (e.key === 'ArrowDown') {
4891                e.preventDefault();
4892                slashMenuIndex = Math.min(SLASH_OPTIONS.length - 1, slashMenuIndex + 1); // Fixed limit
4893                renderSlashMenu(document.getElementById('slash-menu'));
4894                return;
4895            }
4896            if (e.key === 'Enter') {
4897                e.preventDefault();
4898                applySlashType(pageId);
4899                return;
4900            }
4901            if (e.key === 'Escape') {
4902                closeSlashMenu();
4903                return; // Don't propagate Escape clearing selection if menu was open
4904            }
4905        }
4906
4907        if (e.key === 'Escape') {
4908            if (selection.ids.size > 0) {
4909                e.preventDefault();
4910                selection.clear();
4911                return;
4912            }
4913        }
4914
4915        if (selection.ids.size > 0) {
4916            const primaryModifier = e.ctrlKey || e.metaKey;
4917            const key = e.key.toLowerCase();
4918
4919            // Notion-style block movement: keep Ctrl/Cmd + Shift held and use
4920            // the vertical arrows to move the complete selection as one unit.
4921            if (primaryModifier && e.shiftKey && (e.key === 'ArrowUp' || e.key === 'ArrowDown')) {
4922                e.preventDefault();
4923                moveCurrentBlockSelection(e.key === 'ArrowUp' ? 'up' : 'down');
4924                return;
4925            }
4926
4927            if (primaryModifier && key === 'c') {
4928                e.preventDefault();
4929                copyCurrentBlockSelection();
4930                return;
4931            }
4932            if (primaryModifier && key === 'x') {
4933                e.preventDefault();
4934                if (copyCurrentBlockSelection({ showToast: false })) deleteCurrentBlockSelection();
4935                return;
4936            }
4937            if (primaryModifier && key === 'd') {
4938                e.preventDefault();
4939                insertProjectClipboard({ duplicate: true });
4940                return;
4941            }
4942            if (primaryModifier && key === 'v' && projectBlockClipboard.length) {
4943                e.preventDefault();
4944                insertProjectClipboard();
4945                return;
4946            }
4947            if (e.key === 'Delete' || e.key === 'Backspace') {
4948                e.preventDefault();
4949                deleteCurrentBlockSelection();
4950                return;
4951            }
4952            if (e.shiftKey && (e.key === 'ArrowUp' || e.key === 'ArrowDown')) {
4953                e.preventDefault();
4954                const allBlockIds = getEditorBlockIds(editorEl);
4955                const currentExtent = selection.extentBlockId || selection.anchorBlockId;
4956                const currentIndex = Math.max(0, allBlockIds.indexOf(currentExtent));
4957                const direction = e.key === 'ArrowUp' ? -1 : 1;
4958                const targetIndex = Math.max(0, Math.min(allBlockIds.length - 1, currentIndex + direction));
4959                selection.setRange(allBlockIds[targetIndex], allBlockIds);
4960                return;
4961            }
4962        }
4963
4964        if (!target.classList.contains('editable-text')) return;
4965        const currentBlockEl = target.closest('.editor-block');
4966        const currentId = currentBlockEl.dataset.id;
4967        const currentType = currentBlockEl.dataset.type;
4968
4969        // 2. Ctrl + Enter (Toggle Todo)
4970        if (e.ctrlKey && e.key === 'Enter') {
4971            if (currentType === 'todo') {
4972                e.preventDefault();
4973                const checkbox = currentBlockEl.querySelector('.todo-check');
4974                if (checkbox) {
4975                    checkbox.checked = !checkbox.checked;
4976                    store.updateBlock(pageId, currentId, { checked: checkbox.checked });
4977                    const textEl = currentBlockEl.querySelector('.editable-text');
4978                    if (checkbox.checked) textEl.classList.add('checked-text'); else textEl.classList.remove('checked-text');
4979                    if (bus) bus.emit('objectifs:refresh');
4980                }
4981            }
4982            return;
4983        }
4984
4985        // TAB: Indent / Outdent
4986        if (e.key === 'Tab') {
4987            e.preventDefault();
4988            debouncedUpdateBlock.flush();
4989            const blocks = store.getBlocks(pageId);
4990            const idx = blocks.findIndex(b => String(b.id) === currentId);
4991            const item = blocks[idx];
4992            if (!item) return;
4993
4994            if (e.shiftKey) {
4995                // Outdent
4996                if ((item.level || 0) > 0) {
4997                    store.updateBlock(pageId, currentId, { level: (item.level || 0) - 1 });
4998                    render({ preserveCaret: false });
4999                    focusBlock(currentId);
5000                }
5001            } else {
5002                // Indent
5003                const prevItem = idx > 0 ? blocks[idx - 1] : null;
5004                const maxLevel = prevItem ? (prevItem.level || 0) + 1 : 0;
5005                const nextLevel = (item.level || 0) + 1;
5006
5007                if (nextLevel <= maxLevel) {
5008                    store.updateBlock(pageId, currentId, { level: nextLevel });
5009                    render({ preserveCaret: false });
5010                    focusBlock(currentId);
5011                }
5012            }
5013            return;
5014        }
5015
5016        // Space key: Markdown shortcuts (e.g. "- " -> bulleted list, "1. " -> numbered list, "[] " -> todo, etc.)
5017        if (e.key === ' ') {
5018            const text = target.innerText;
5019            const sel = window.getSelection();
5020            if (sel.rangeCount > 0) {
5021                const range = sel.getRangeAt(0);
5022                const preCaretRange = range.cloneRange();
5023                preCaretRange.selectNodeContents(target);
5024                preCaretRange.setEnd(range.endContainer, range.endOffset);
5025                const caretAt = preCaretRange.toString().length;
5026                
5027                if (caretAt === text.length) {
5028                    let newType = null;
5029                    if (currentType === 'p') {
5030                        if (text === '-' || text === '*') newType = 'bulleted_list_item';
5031                        else if (text === '1.') newType = 'numbered_list_item';
5032                        else if (text === '[]' || text === '- [ ]' || text === '[ ]') newType = 'todo';
5033                        else if (text === '#') newType = 'h1';
5034                        else if (text === '##') newType = 'h2';
5035                        else if (text === '>') newType = 'quote';
5036                    }
5037                    
5038                    if (newType) {
5039                        e.preventDefault();
5040                        store.updateBlock(pageId, currentId, { type: newType, text: '' });
5041                        render({ preserveCaret: false });
5042                        focusBlock(currentId, 'start');
5043                        return;
5044                    }
5045                }
5046            }
5047        }
5048
5049        // 3. Enter (New Block / Split Block)
5050        if (e.key === 'Enter' && !e.shiftKey) {
5051            e.preventDefault();
5052            // Read the live content directly: creating and rendering the next block
5053            // must never outrun the local save of the current one.
5054            commitEditableBlockNow(store, pageId, target);
5055            editorsCommittedBeforeFocusout.add(target);
5056
5057            // A. Empty Item -> Cancel List (Todo/Bullet/Number -> P)
5058            if (target.innerText.trim() === '' && (currentType === 'todo' || currentType === 'bulleted_list_item' || currentType === 'numbered_list_item')) {
5059                store.updateBlock(pageId, currentId, { type: 'p' });
5060                render({ preserveCaret: false });
5061                focusBlock(currentId, 'start');
5062                return;
5063            }
5064
5065            // B. Split Text Logic
5066            let textBefore = target.innerText;
5067            let textAfter = '';
5068
5069            const sel = window.getSelection();
5070            if (sel.rangeCount > 0) {
5071                const range = sel.getRangeAt(0);
5072                const preCaretRange = range.cloneRange();
5073                preCaretRange.selectNodeContents(target);
5074                preCaretRange.setEnd(range.endContainer, range.endOffset);
5075                const caretAt = preCaretRange.toString().length;
5076
5077                textBefore = target.innerText.substring(0, caretAt);
5078                textAfter = target.innerText.substring(caretAt);
5079            }
5080
5081            // Update Current Block (Before split)
5082            if (textAfter !== '') {
5083                store.updateBlock(pageId, currentId, { text: textBefore });
5084            }
5085
5086            // Create New Block
5087            let nextType = 'p';
5088            if (currentType === 'todo') nextType = 'todo';
5089            if (currentType === 'bulleted_list_item' || currentType === 'bullet') nextType = 'bulleted_list_item';
5090            if (currentType === 'numbered_list_item') nextType = 'numbered_list_item';
5091
5092            // Inherit Level
5093            const currentBlockFn = store.getBlocks(pageId).find(b => String(b.id) === currentId);
5094            const currentLevel = currentBlockFn ? (currentBlockFn.level || 0) : 0;
5095
5096            const newBlock = store.addBlock(pageId, { type: nextType, text: textAfter, level: currentLevel }, currentId);
5097
5098            render({ preserveCaret: false });
5099            focusBlock(newBlock.id, 'start');
5100            return;
5101        }
5102
5103        // 4. Backspace
5104        if (e.key === 'Backspace') {
5105            const sel = window.getSelection();
5106            if (sel.rangeCount > 0) {
5107                const range = sel.getRangeAt(0);
5108                const preCaretRange = range.cloneRange();
5109                preCaretRange.selectNodeContents(target);
5110                preCaretRange.setEnd(range.endContainer, range.endOffset);
5111                const caretAt = preCaretRange.toString().length;
5112                
5113                // If caret is at the beginning of the block
5114                if (caretAt === 0) {
5115                    // A. Convert list item / header / quote / callout back to paragraph 'p' first
5116                    if (currentType !== 'p') {
5117                        e.preventDefault();
5118                        store.updateBlock(pageId, currentId, { type: 'p' });
5119                        render({ preserveCaret: false });
5120                        focusBlock(currentId, 'start');
5121                        return;
5122                    }
5123                    
5124                    // B. Delete empty block if it is a paragraph
5125                    if (target.innerText.trim() === '') {
5126                        e.preventDefault();
5127                        const allBlocks = editorEl.querySelectorAll('.editor-block');
5128                        if (allBlocks.length === 1) return; // Keep at least one block
5129                        
5130                        const prevBlockEl = currentBlockEl.previousElementSibling;
5131                        const blockObj = store.getBlocks(pageId).find(b => String(b.id) === currentId);
5132                        if (blockObj && blockObj.isNotion && blockObj.notionBlockId) {
5133                            deleteBlockFromNotion(pageId, blockObj.notionBlockId);
5134                        }
5135
5136                        store.deleteBlock(pageId, currentId);
5137                        render({ preserveCaret: false });
5138                        if (prevBlockEl) {
5139                            focusBlock(prevBlockEl.dataset.id, 'end');
5140                        }
5141                        return;
5142                    }
5143                }
5144            }
5145        }
5146
5147        // 5. Arrow Navigation
5148        if (e.key === 'ArrowUp') {
5149            const prevBlock = currentBlockEl.previousElementSibling;
5150            if (prevBlock) {
5151                e.preventDefault();
5152                focusBlock(prevBlock.dataset.id, 'end');
5153            }
5154        } else if (e.key === 'ArrowDown') {
5155            const nextBlock = currentBlockEl.nextElementSibling;
5156            if (nextBlock) {
5157                e.preventDefault();
5158                focusBlock(nextBlock.dataset.id, 'start');
5159            }
5160        }
5161    }
5162
5163    // --- HELPERS FOR INIT (Since openMenu handles the real work) ---
5164    function initSlashMenu(menuEl) {
5165        menuEl.style.display = 'none';
5166        menuEl.innerHTML = '';
5167    }
5168
5169    function renderSlashMenu(menuEl) {
5170        // Only for arrow key nav highlight updates
5171        const activeItem = menuEl.children[slashMenuIndex];
5172        if (activeItem) {
5173            Array.from(menuEl.children).forEach(c => c.classList.remove('active'));
5174            activeItem.classList.add('active');
5175        }
5176    }
5177
5178    function applySlashType(pageId, typeOverride = null) {
5179        const store = objectifsStore;
5180        const OPTIONS = [
5181            ...SLASH_OPTIONS,
5182            { label: 'JSON 📋', type: 'export_json', icon: '📋' }
5183        ];
5184        const type = typeOverride || OPTIONS[slashMenuIndex].type;
5185
5186        if (type === 'export_json') {
5187            const json = JSON.stringify(store.state, null, 2);
5188            console.log(json);
5189            navigator.clipboard.writeText(json).then(() => alert(i18n.t('projects_json_copied')));
5190            closeSlashMenu(); // Close menu after action
5191            return;
5192        }
5193
5194        if (type === 'page_link') {
5195            const title = prompt(i18n.t('projects_new_page_prompt')) || i18n.t('common_untitled');
5196            const newPage = store.createPage(title, pageId);
5197            store.updateBlock(pageId, slashMenuBlockId, { type: 'page_link', text: newPage.id });
5198            closeSlashMenu();
5199            render();
5200            return;
5201        }
5202
5203        // Update current block to new type and clear text
5204        store.updateBlock(pageId, slashMenuBlockId, { type: type, text: '' });
5205        render(); // Force UI update
5206
5207        closeSlashMenu();
5208        // Focus back
5209        setTimeout(() => {
5210            const input = document.querySelector(`[data-id="${slashMenuBlockId}"] .editable-text`);
5211            if (input) input.focus();
5212        }, 50);
5213    }
5214
5215    // -- SHARED BINDINGS --
5216    function bindGlobalEvents(container, store, treeRoot) {
5217        // Clear block selection only when the user really leaves the editor or its action bar.
5218        if (container.__roximaProjectSelectionOutsideClick) {
5219            container.removeEventListener('click', container.__roximaProjectSelectionOutsideClick, true);
5220        }
5221        container.__roximaProjectSelectionOutsideClick = (e) => {
5222            if (selection.ids.size === 0) return;
5223            const remainsInSelectionUi = e.target.closest('.editor-block, .selection-action-bar, .modal-container, .action-sheet');
5224            if (!remainsInSelectionUi) selection.clear();
5225        };
5226        container.addEventListener('click', container.__roximaProjectSelectionOutsideClick, true);
5227
5228        // Selection Action Bar Buttons
5229        const selectionBar = container.querySelector('#project-selection-bar');
5230        if (selectionBar) {
5231            const btnCopy = selectionBar.querySelector('.btn-copy-legacy-disabled');
5232            if (btnCopy) {
5233                btnCopy.addEventListener('click', (e) => {
5234                    e.stopPropagation();
5235                    const pageId = selectedPageId;
5236                    if (!pageId) return;
5237                    const sortedIds = selection.getSortedIds(Array.from(container.querySelectorAll('.editor-block')).map(el => el.dataset.id));
5238                    const textToCopy = sortedIds.map(id => {
5239                        const block = store.state.blocksByPageId[pageId].find(b => String(b.id) === id);
5240                        if (!block) return '';
5241                        if (block.type === 'todo') return block.checked ? `- [x] ${block.text}` : `- [ ] ${block.text}`;
5242                        if (block.type === 'bulleted_list_item' || block.type === 'bullet') return `- ${block.text}`;
5243                        if (block.type === 'numbered_list_item') return `1. ${block.text}`;
5244                        return block.text;
5245                    }).join('\n');
5246                    navigator.clipboard.writeText(textToCopy);
5247                    window.Roxima.showToast?.(i18n.t('projects_toast_copied') || 'Blocs copiés !');
5248                    selection.clear();
5249                });
5250            }
5251
5252            const btnDelete = selectionBar.querySelector('.btn-delete-legacy-disabled');
5253            if (btnDelete) {
5254                btnDelete.addEventListener('click', (e) => {
5255                    e.stopPropagation();
5256                    const pageId = selectedPageId;
5257                    if (!pageId) return;
5258                    const sortedIds = selection.getSortedIds(Array.from(container.querySelectorAll('.editor-block')).map(el => el.dataset.id));
5259                    sortedIds.forEach(id => {
5260                        const blockObj = store.getBlocks(pageId).find(b => String(b.id) === id);
5261                        if (blockObj && blockObj.isNotion && blockObj.notionBlockId) {
5262                            deleteBlockFromNotion(pageId, blockObj.notionBlockId);
5263                        }
5264                        store.deleteBlock(pageId, id);
5265                    });
5266                    selection.clear();
5267                    render();
5268                });
5269            }
5270
5271            selectionBar.querySelector('.btn-copy')?.addEventListener('click', (e) => {
5272                e.stopPropagation();
5273                copyCurrentBlockSelection();
5274            });
5275            selectionBar.querySelector('.btn-paste')?.addEventListener('click', (e) => {
5276                e.stopPropagation();
5277                insertProjectClipboard();
5278            });
5279            selectionBar.querySelector('.btn-duplicate')?.addEventListener('click', (e) => {
5280                e.stopPropagation();
5281                if (copyCurrentBlockSelection({ showToast: false })) insertProjectClipboard();
5282            });
5283            selectionBar.querySelector('.btn-delete')?.addEventListener('click', (e) => {
5284                e.stopPropagation();
5285                deleteCurrentBlockSelection();
5286            });
5287
5288            const btnCancel = selectionBar.querySelector('.btn-cancel');
5289            if (btnCancel) {
5290                btnCancel.addEventListener('click', (e) => {
5291                    e.stopPropagation();
5292                    selection.clear();
5293                });
5294            }
5295        }
5296
5297        // Zoom
5298        const btnZoom = container.querySelector('#btn-zoom');
5299        if (btnZoom) btnZoom.addEventListener('click', () => ctx.bus.emit('ui:zoom', { target: container }));
5300
5301        // Settings
5302        const btnSettings = container.querySelector('#btn-settings');
5303        if (btnSettings) {
5304            btnSettings.type = 'button';
5305            btnSettings.dataset.i18nTitle = 'projects_settings';
5306            btnSettings.dataset.i18nAriaLabel = 'projects_settings';
5307            btnSettings.title = i18n.t('projects_settings');
5308            btnSettings.setAttribute('aria-label', i18n.t('projects_settings'));
5309            btnSettings.querySelector('svg')?.setAttribute('aria-hidden', 'true');
5310            btnSettings.querySelector('svg')?.setAttribute('focusable', 'false');
5311            btnSettings.addEventListener('click', () => ctx.bus.emit('ui:modal:open', {
5312                titleKey: 'projects_settings_title',
5313                content: '<p data-i18n="projects_settings_body">...</p>'
5314            }));
5315        }
5316
5317        // Add Page (Sidebar Header) - Also delegated in handleContainerClick? 
5318        // Let's rely on handleContainerClick for consistency, or keep here if it works.
5319        // It's in .obj-actions which is re-rendered. So listener here is fine.
5320        const btnAdd = container.querySelector('#btn-add-page');
5321        if (btnAdd) btnAdd.addEventListener('click', (e) => {
5322            e.stopPropagation(); // Prevent container click
5323            const newPage = store.createPage(i18n.t('common_untitled'));
5324            setSelectedPageId(newPage.id);
5325            render();
5326        });
5327
5328        // NOTE: Sidebar Tree Listeners (Delete/Add Sub-page/Select) are handled by 
5329        // handleContainerClick via event delegation. 
5330        // This ensures they work even after re-renders without re-binding complex logic.
5331    }
5332
5333    // --- EMOJI PICKER ---
5334    const EMOJIS = [
5335        '📄', '🍅', '⚡', '🎯', '📅', '✅', '🔥', '💡', '🎨', '🧠', '🚀', '⭐', '📁', '⚙️', '🧱', '❤️', '🎓', '🏆', '🎉', '🍎', '👻', '🤖', '📚', '📝', '💼',
5336        '💻', '⌨️', '🖥️', '📱', '📊', '📈', '📉', '📌', '📍', '📎', '🗑️', '🔒', '🔑',
5337        '🔮', '🧩', '🎲', '🛠️', '⚖️', '🧭', '⏳', '⏱️', '⏰', '💎', '💸', '💰',
5338        '😀', '😂', '😎', '🤔', '😅', '🙄', '😴', '🤯', '🥳', '😇',
5339        '🌲', '🌿', '🌍', '🌙', '☀️', '☁️', '🌊', '⛰️', '🏕️',
5340        '🎧', '🎵', '🎮', '🎬', '📸', '✈️', '🚗', '🚴', '🍔', '☕', '🍕', '🍺'
5341    ];
5342
5343    function showEmojiPicker(x, y, onSelect) {
5344        const root = document.getElementById('slash-menu'); // Reuse or create new? Reuse is fine just clear
5345        if (!root) return;
5346
5347        root.innerHTML = `
5348            <div class="hide-scrollbar" style="display:grid; grid-template-columns:repeat(5, 1fr); gap:4px; padding-bottom:6px; max-height:200px; overflow-y:auto;">
5349                ${EMOJIS.map(e => `<div class="emoji-item" style="cursor:pointer; padding:6px; font-size:1.2rem; text-align:center; border-radius:4px;">${e}</div>`).join('')}
5350            </div>
5351            <button id="remove-icon-btn" style="width:100%; margin-top:6px; padding:4px; border:none; background:#f3f4f6; cursor:pointer; border-radius:4px; font-size:0.8rem;" data-i18n="common_delete">${i18n.t('common_delete')}</button>
5352        `;
5353
5354        root.style.display = 'block';
5355        root.style.left = x + 'px';
5356        root.style.top = y + 'px';
5357
5358        // Bind clicks
5359        root.querySelectorAll('.emoji-item').forEach(el => {
5360            el.addEventListener('click', (e) => {
5361                e.stopPropagation();
5362                onSelect(el.innerText);
5363                root.style.display = 'none';
5364            });
5365            el.addEventListener('mouseover', () => el.style.background = '#f3f4f6');
5366            el.addEventListener('mouseout', () => el.style.background = 'transparent');
5367        });
5368
5369        root.querySelector('#remove-icon-btn').addEventListener('click', (e) => {
5370            e.stopPropagation();
5371            onSelect(null);
5372            root.style.display = 'none';
5373        });
5374
5375        // Click outside to close (Global listener handles this usually via slashMenuOpen flag if reused, but here generic)
5376        const close = (e) => {
5377            if (!root.contains(e.target)) {
5378                root.style.display = 'none';
5379                document.removeEventListener('click', close);
5380            }
5381        };
5382        setTimeout(() => document.addEventListener('click', close), 100);
5383    }
5384
5385    // --- UPDATED RENDER TREE (Icons) ---
5386    function renderTree(rootEl, allPages, parentId) {
5387        const children = allPages.filter(p => p.parentId === parentId).sort((a, b) => (a.position ?? 0) - (b.position ?? 0));
5388        children.forEach(page => {
5389            const div = document.createElement('div');
5390            const pageId = String(page.id ?? '');
5391            const isActive = selectedPageIds.has(pageId);
5392            const icon = page.icon || '📄';
5393            
5394            const hasChildren = allPages.some(p => String(p.parentId) === pageId);
5395            const isExpanded = expandedPages.has(pageId);
5396
5397            div.innerHTML = `
5398                <div class="page-item ${isActive ? 'active' : ''}" data-id="${escapeAttribute(pageId)}">
5399                    ${hasChildren ? `
5400                        <button class="toggle-folder-btn" data-id="${escapeAttribute(pageId)}" type="button" style="background:none; border:none; cursor:pointer; padding:0 4px; color:var(--text-muted); display:flex; align-items:center; justify-content:center; width:24px; height:24px; margin-right:2px; flex-shrink:0;">
5401                            <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="transform: ${isExpanded ? 'rotate(90deg)' : 'rotate(0deg)'}; transition: transform 0.2s;">
5402                                <polyline points="9 18 15 12 9 6"></polyline>
5403                            </svg>
5404                        </button>
5405                    ` : `<div style="width:24px; height:24px; margin-right:2px; flex-shrink:0;"></div>`}
5406                    <div class="page-item-content">
5407                        <span class="page-icon">${escapeHtml(icon)}</span>
5408                        <span class="page-title-tree">${escapeHtml(page.title || i18n.t('common_untitled'))}</span>
5409                    </div>
5410                    <div class="page-item-actions">
5411                        <button class="mini-btn action-add-sub" data-id="${escapeAttribute(pageId)}" type="button" data-i18n-title="projects_add_subpage" data-i18n-aria-label="projects_add_subpage" title="${escapeAttribute(i18n.t('projects_add_subpage'))}" aria-label="${escapeAttribute(i18n.t('projects_add_subpage'))}">+</button>
5412                        <button class="mini-btn action-del" data-id="${escapeAttribute(pageId)}" type="button" data-i18n-title="common_delete" data-i18n-aria-label="common_delete" title="${escapeAttribute(i18n.t('common_delete'))}" aria-label="${escapeAttribute(i18n.t('common_delete'))}">×</button>
5413                    </div>
5414                </div>
5415                <div class="page-children" style="display: ${isExpanded ? 'flex' : 'none'};"></div>
5416            `;
5417            rootEl.appendChild(div);
5418            // Recursive
5419            renderTree(div.querySelector('.page-children'), allPages, page.id);
5420        });
5421    }
5422
5423    // --- RE-BIND Slash Options to include Page Link ---
5424    // Note: We need to modify slashMenu logic to handle 'page_link' specifically
5425
5426    // Initial Render
5427    render();
5428    syncPlanningTimer();
5429
5430    // Charger les connexions depuis le compte Roxima. Le cache local ne
5431    // contient que leurs métadonnées et permet un rendu immédiat hors ligne.
5432    if (ctx.currentUser) {
5433        refreshNotionConnections()
5434            .then((connections) => {
5435                if (window.Roxima?.notionOAuthReturn === true) {
5436                    const latestConnection = [...connections]
5437                        .filter(connection => !isDemoNotionWorkspace(connection))
5438                        .sort((a, b) => (Date.parse(b.updatedAt || b.connectedAt) || 0) - (Date.parse(a.updatedAt || a.connectedAt) || 0))[0];
5439                    if (latestConnection) {
5440                        localStorage.setItem('roxima_notion_active_workspace_id', latestConnection.id);
5441                    }
5442                }
5443                const activeWorkspace = getActiveNotionWorkspace();
5444                render();
5445                if (activeWorkspace && !isDemoNotionWorkspace(activeWorkspace)) {
5446                    syncNotion(activeWorkspace, {
5447                        revealImportedPage: window.Roxima?.notionOAuthReturn === true
5448                    });
5449                    setupNotionSyncInterval(activeWorkspace);
5450                }
5451            })
5452            .catch(error => {
5453                console.warn('Impossible de charger les connexions Notion :', error);
5454                if (window.Roxima?.notionOAuthReturn === true) {
5455                    window.Roxima.notionOAuthReturn = false;
5456                    ctx.bus.emit('notion:sync:error', { message: error?.message || '' });
5457                }
5458                const cachedWorkspace = getActiveNotionWorkspace();
5459                if (cachedWorkspace && !isDemoNotionWorkspace(cachedWorkspace)) {
5460                    setupNotionSyncInterval(cachedWorkspace);
5461                }
5462            });
5463    }
5464
5465    ctx.bus?.on('auth:state', ({ user } = {}) => {
5466        if (!user) return;
5467        refreshNotionConnections()
5468            .then(() => {
5469                const activeWorkspace = getActiveNotionWorkspace();
5470                render();
5471                if (activeWorkspace && !isDemoNotionWorkspace(activeWorkspace)) {
5472                    syncNotion(activeWorkspace);
5473                    setupNotionSyncInterval(activeWorkspace);
5474                }
5475            })
5476            .catch(error => console.warn('Impossible d actualiser les connexions Notion :', error));
5477    });
5478}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.