fix: /local-workspace — chemin du header + clic sur un dossier du sidebar (v7.69.9)
FlowDeck CI / lint (push) Successful in 1m37s
FlowDeck CI / test (push) Failing after 24m19s
FlowDeck CI / docker (push) Skipped

Corrige deux régressions de /local-workspace :

- le chemin du header restait bloqué sur « Home / <workspace> » quel que
  soit le dossier affiché : la route rend désormais breadcrumb_items
  (Home / <workspace> / <dossier> / <sous-dossier>, niveaux cliquables,
  collapse « … » au-delà de 4) et la navigation sans rechargement recalcule
  le chemin via l'event flowdeck:breadcrumb-changed ;
- le clic sur un dossier du sidebar affichait TOUS les composants à la
  fois : Alpine.data('wsInitData') retournait le même objet singleton, le
  2e montage (navigation partielle) levait « Cannot redefine property:
  \ » et initTree abandonnait, laissant tout le contenu au state
  brut. La factory retourne désormais une enveloppe fraîche par montage
  qui délègue à l'état réactif partagé. #lw-config est aussi relu à chaque
  exécution (le 2e montage gardait le folder_id du 1er chargement).

Inclus également le travail en cours de l'arbre : Library (colonnes Last
visited/Source, ordre d'en-tête, favoris à icônes Workspace), Meeting
Notes (bloc, CSS, routes, docs), coloration de code hljs, badges
favori/publié dans l'arbre local-workspace, docs (DATA_MODEL,
architectures) et tests associés.
This commit is contained in:
2026-10-09 16:58:04 -04:00
parent 7468e497c6
commit 0218d8f5e5
44 changed files with 12060 additions and 1729 deletions
+348
View File
@@ -0,0 +1,348 @@
/* ═══════════════════════════════════════════════════════════════════
FlowDeck — central syntax-highlighting helper (highlight.js, vendored).
Single source of truth mapping file extensions / well-known filenames to
highlight.js language ids. Covers ~230 file types (code, config, IaC,
docs). Every code viewer (Gitea file view, page-editor code blocks & file
viewer, workspace previews, public pages) must go through this helper so a
new language is added in ONE place.
Requires (load order): highlight.min.js, highlight-extra.min.js.
All functions are defensive: without hljs they degrade to escaped plain
text — never throw, never use eval (CSP-safe).
═══════════════════════════════════════════════════════════════════ */
(function () {
'use strict';
/* Extension (lowercase, no dot) → highlight.js language id.
Ids must exist in static/js/highlight.min.js (common bundle) or
static/js/highlight-extra.min.js — anything else falls back to the
alias table / plaintext via normalizeLang(). */
var EXT_TO_LANG = {
// ── Python ──
py: 'python', pyw: 'python', pyi: 'python', gyp: 'python', gypi: 'python',
// ── JavaScript / TypeScript ──
js: 'javascript', mjs: 'javascript', cjs: 'javascript', jsx: 'javascript',
ts: 'typescript', mts: 'typescript', cts: 'typescript', tsx: 'typescript',
// ── Web markup / styles ──
html: 'xml', htm: 'xml', xhtml: 'xml', xht: 'xml', xml: 'xml',
xsd: 'xml', xsl: 'xml', xslt: 'xml', rss: 'xml', atom: 'xml',
plist: 'xml', wsf: 'xml', svg: 'xml', vue: 'xml', svelte: 'xml',
astro: 'xml', axml: 'xml',
css: 'css', scss: 'scss', sass: 'scss', less: 'less',
// ── Data / config ──
json: 'json', json5: 'json', jsonc: 'json', geojson: 'json',
webmanifest: 'json', avsc: 'json', har: 'json',
yaml: 'yaml', yml: 'yaml',
toml: 'ini', ini: 'ini', cfg: 'ini', conf: 'ini', env: 'ini',
properties: 'ini', prop: 'ini', gitconfig: 'ini', gitmodules: 'ini',
editorconfig: 'ini', inf: 'ini', url: 'ini', reg: 'ini',
// ── Markdown / docs ──
md: 'markdown', markdown: 'markdown', mkd: 'markdown', mdwn: 'markdown',
mdown: 'markdown', rmd: 'markdown', qmd: 'markdown',
tex: 'latex', latex: 'latex', sty: 'latex', cls: 'latex', dtx: 'latex',
// ── SQL ──
sql: 'sql', pgsql: 'sql', psql: 'sql', mysql: 'sql', mssql: 'sql',
// ── Shells ──
// NOTE: no `.shell` extension key (unused); the `shell` grammar stays
// available via `fish`, aliases and the language picker.
sh: 'bash', bash: 'bash', zsh: 'bash', ksh: 'bash', mksh: 'bash',
fish: 'shell', yash: 'bash',
ps1: 'powershell', psm1: 'powershell', psd1: 'powershell',
bat: 'dos', cmd: 'dos',
// ── Systems ──
c: 'c', h: 'c',
cpp: 'cpp', cc: 'cpp', cxx: 'cpp', 'c++': 'cpp', hh: 'cpp',
hpp: 'cpp', hxx: 'cpp', hcc: 'cpp', ino: 'cpp', pde: 'cpp',
cs: 'csharp', csx: 'csharp', cake: 'csharp',
rs: 'rust',
go: 'go',
swift: 'swift',
kt: 'kotlin', kts: 'kotlin',
java: 'java', jsp: 'java',
scala: 'scala', sc: 'scala',
groovy: 'groovy', gvy: 'groovy', gradle: 'groovy',
dart: 'dart',
lua: 'lua',
// NOTE: no `.perl` / `.t` / `.mat` / `.app` / `.gpg` / `.wasm` keys on
// purpose (unused, ambiguous or binary extensions).
pl: 'perl', pm: 'perl', pod: 'perl',
php: 'php', phtml: 'php-template',
r: 'r', jl: 'julia',
rb: 'ruby', gemspec: 'ruby', rake: 'ruby', rbw: 'ruby',
// ── Functional / misc ──
hs: 'haskell', lhs: 'haskell',
ex: 'elixir', exs: 'elixir', eex: 'elixir', heex: 'elixir', leex: 'elixir',
erl: 'erlang', hrl: 'erlang', escript: 'erlang',
clj: 'clojure', cljs: 'clojure', cljc: 'clojure', edn: 'clojure',
ml: 'ocaml', mli: 'ocaml',
fs: 'fsharp', fsi: 'fsharp', fsx: 'fsharp', fsscript: 'fsharp',
nim: 'nim', nims: 'nim',
d: 'd', di: 'd',
m: 'objectivec', mm: 'objectivec',
vb: 'vbnet', vbs: 'vbnet', bas: 'vbnet',
pas: 'delphi', pp: 'delphi', dpr: 'delphi',
// ── HDL / asm / legacy ──
vhd: 'vhdl', vhdl: 'vhdl',
v: 'verilog', vh: 'verilog', sv: 'verilog', svh: 'verilog',
asm: 'x86asm', s: 'x86asm', nasm: 'x86asm', inc: 'x86asm',
ada: 'ada', ads: 'ada', adb: 'ada',
f: 'fortran', for: 'fortran', f90: 'fortran', f95: 'fortran',
f03: 'fortran', f08: 'fortran',
cob: 'plaintext', cbl: 'plaintext', cpy: 'plaintext',
// ── Build / IaC / containers ──
dockerfile: 'dockerfile',
tf: 'plaintext', tfvars: 'plaintext', hcl: 'plaintext',
cmake: 'cmake',
makefile: 'makefile', mk: 'makefile', mak: 'makefile',
ninja: 'ini',
gql: 'graphql', graphql: 'graphql', graphqls: 'graphql',
proto: 'protobuf',
thrift: 'thrift',
vim: 'vim', vimrc: 'vim', gvimrc: 'vim',
awk: 'awk',
gcode: 'gcode', gco: 'gcode', nc: 'gcode', ngc: 'gcode',
// ── Web servers / protocols ──
http: 'http', rest: 'http',
nginx: 'nginx',
apache: 'apache', htaccess: 'apache',
wat: 'wasm',
// ── Diffs / misc text ──
diff: 'diff', patch: 'diff', rej: 'diff',
log: 'plaintext', txt: 'plaintext', text: 'plaintext',
csv: 'plaintext', tsv: 'plaintext', tab: 'plaintext',
lock: 'plaintext', gitignore: 'plaintext', gitattributes: 'plaintext',
gitkeep: 'plaintext', dockerignore: 'plaintext', npmignore: 'plaintext',
bib: 'plaintext', bbl: 'plaintext', nfo: 'plaintext', asc: 'plaintext',
};
/* Exact basenames (lowercase) → language, checked BEFORE the extension
(Dockerfile / Makefile / go.mod … often carry no usable extension). */
var FILE_TO_LANG = {
'dockerfile': 'dockerfile', 'containerfile': 'dockerfile',
'docker-compose.yml': 'yaml', 'docker-compose.yaml': 'yaml',
'compose.yml': 'yaml', 'compose.yaml': 'yaml',
'makefile': 'makefile', 'gnumakefile': 'makefile',
'cmakelists.txt': 'cmake',
'go.mod': 'go', 'go.sum': 'plaintext', 'go.work': 'go',
'cargo.toml': 'ini', 'cargo.lock': 'plaintext',
'package.json': 'json', 'package-lock.json': 'json',
'composer.json': 'json', 'tsconfig.json': 'json',
'nginx.conf': 'nginx',
'.bashrc': 'bash', '.bash_profile': 'bash', '.bash_login': 'bash',
'.bash_logout': 'bash', '.zshrc': 'bash', '.zshenv': 'bash',
'.profile': 'bash', '.shrc': 'bash', '.kshrc': 'bash',
'.vimrc': 'vim', '.gvimrc': 'vim',
'rakefile': 'ruby', 'gemfile': 'ruby', 'vagrantfile': 'ruby',
'podfile': 'ruby', 'fastfile': 'ruby', 'dangerfile': 'ruby',
'brewfile': 'ruby', 'berksfile': 'ruby',
'build.gradle': 'groovy', 'settings.gradle': 'groovy',
'build.gradle.kts': 'kotlin', 'settings.gradle.kts': 'kotlin',
'.gitconfig': 'ini', '.gitmodules': 'ini', '.editorconfig': 'ini',
'.htaccess': 'apache', '.viminfo': 'vim',
};
/* Aliases users may type (markdown fences, old block labels, Prism ids…)
→ canonical highlight.js id. */
var ALIAS_TO_LANG = {
'py': 'python', 'python3': 'python', 'gyp': 'python',
'js': 'javascript', 'jsx': 'javascript', 'mjs': 'javascript',
'cjs': 'javascript', 'node': 'javascript',
'ts': 'typescript', 'tsx': 'typescript',
'html': 'xml', 'htm': 'xml', 'xhtml': 'xml', 'svg': 'xml',
'markup': 'xml', 'vue': 'xml', 'svelte': 'xml',
'yml': 'yaml', 'yaml': 'yaml',
'md': 'markdown', 'mkd': 'markdown',
'sh': 'bash', 'shell': 'shell', 'zsh': 'bash', 'console': 'shell',
'ps1': 'powershell', 'ps': 'powershell', 'powershell': 'powershell',
'bat': 'dos', 'cmd': 'dos', 'batch': 'dos',
'shell-session': 'shell', 'shellsession': 'shell',
'c++': 'cpp', 'cc': 'cpp', 'hpp': 'cpp', 'h': 'c',
'c#': 'csharp', 'csharp': 'csharp', 'cs': 'csharp',
'kt': 'kotlin', 'kts': 'kotlin',
'rb': 'ruby',
'rs': 'rust',
'golang': 'go',
'pl': 'perl', 'pm': 'perl',
'm': 'objectivec', 'mm': 'objectivec', 'objc': 'objectivec',
'r': 'r',
'jl': 'julia',
'toml': 'ini', 'ini': 'ini', 'cfg': 'ini', 'conf': 'ini',
'env': 'ini', 'properties': 'ini',
'tex': 'latex', 'latex': 'latex',
'docker': 'dockerfile',
'make': 'makefile',
'patch': 'diff',
'text': 'plaintext', 'plain': 'plaintext', 'plain text': 'plaintext',
'plaintext': 'plaintext', 'txt': 'plaintext', 'log': 'plaintext',
'none': 'plaintext', '': 'plaintext',
};
/* Language id → short human label for the code-block badge / selector. */
var LANG_LABELS = {
python: 'Python', javascript: 'JavaScript', typescript: 'TypeScript',
xml: 'HTML/XML', css: 'CSS', scss: 'SCSS', less: 'Less',
json: 'JSON', yaml: 'YAML', ini: 'INI/Config', markdown: 'Markdown',
latex: 'LaTeX', sql: 'SQL', bash: 'Bash', shell: 'Shell',
powershell: 'PowerShell', dos: 'Batch', c: 'C', cpp: 'C++',
csharp: 'C#', rust: 'Rust', go: 'Go', swift: 'Swift', kotlin: 'Kotlin',
java: 'Java', scala: 'Scala', groovy: 'Groovy', dart: 'Dart',
lua: 'Lua', perl: 'Perl', php: 'PHP', 'php-template': 'PHP',
r: 'R', julia: 'Julia', ruby: 'Ruby', haskell: 'Haskell',
elixir: 'Elixir', erlang: 'Erlang', clojure: 'Clojure',
ocaml: 'OCaml', fsharp: 'F#', nim: 'Nim', d: 'D',
objectivec: 'Objective-C', matlab: 'MATLAB', vbnet: 'VB.NET',
delphi: 'Delphi', vhdl: 'VHDL', verilog: 'Verilog',
x86asm: 'ASM', armasm: 'ARM ASM', ada: 'Ada', fortran: 'Fortran',
dockerfile: 'Docker', cmake: 'CMake', makefile: 'Makefile',
graphql: 'GraphQL', protobuf: 'Protobuf', thrift: 'Thrift',
vim: 'Vim', awk: 'AWK', gcode: 'G-code',
http: 'HTTP', nginx: 'Nginx', apache: 'Apache', wasm: 'WASM',
diff: 'Diff', plaintext: 'Plain Text',
};
/** Languages offered in the code-block language picker (id order). */
var PICKER_LANGS = [
'plaintext', 'python', 'javascript', 'typescript', 'xml', 'css', 'scss',
'json', 'yaml', 'ini', 'markdown', 'sql', 'bash', 'shell', 'powershell',
'dos', 'c', 'cpp', 'csharp', 'rust', 'go', 'swift', 'kotlin', 'java',
'scala', 'groovy', 'dart', 'lua', 'perl', 'php', 'r', 'julia', 'ruby',
'haskell', 'elixir', 'erlang', 'clojure', 'ocaml', 'fsharp', 'nim', 'd',
'objectivec', 'dockerfile', 'cmake', 'makefile', 'graphql', 'protobuf',
'latex', 'vim', 'http', 'nginx', 'diff',
];
function hasHljs() {
return typeof window.hljs !== 'undefined' && !!window.hljs.getLanguage;
}
function escHtml(s) {
return String(s == null ? '' : s)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}
/** Normalize any user label / extension / Prism id to a bundled hljs id. */
function normalizeLang(lang) {
var l = String(lang == null ? '' : lang).trim().toLowerCase();
if (!l || l === 'plain text' || l === 'none') return 'plaintext';
if (ALIAS_TO_LANG[l]) return ALIAS_TO_LANG[l];
if (!hasHljs()) return l || 'plaintext';
try {
if (window.hljs.getLanguage(l)) return l;
} catch (e) { /* ignore → plaintext */ }
return 'plaintext';
}
/** Best highlight.js id for a file path (basename map, then extension). */
function langForFile(path) {
var name = String(path || '').replace(/\\/g, '/').split('/').pop().toLowerCase();
if (!name) return 'plaintext';
if (FILE_TO_LANG[name]) return FILE_TO_LANG[name];
// dotfiles like ".bashrc" without another dot are covered above; a
// trailing ".xyz" is the extension otherwise.
var dot = name.lastIndexOf('.');
if (dot < 0) return 'plaintext';
var ext = name.slice(dot + 1);
if (FILE_TO_LANG['.' + name]) return FILE_TO_LANG['.' + name];
if (EXT_TO_LANG[ext]) return EXT_TO_LANG[ext];
return 'plaintext';
}
function labelFor(lang) {
var n = normalizeLang(lang);
return LANG_LABELS[n] || n;
}
/** Highlight `text` → HTML string (escaped when hljs/lang missing). */
function highlightText(text, lang) {
var src = String(text == null ? '' : text);
var n = normalizeLang(lang);
if (n === 'plaintext' || !hasHljs()) return escHtml(src);
try {
return window.hljs.highlight(src, { language: n }).value;
} catch (e) {
return escHtml(src);
}
}
/**
* Highlight a <code> element in place. The element's TEXT is preserved
* (highlight.js reads textContent), so this is safe for contenteditable
* code blocks: sync() re-reads text, spans are transparent.
* Re-highlighting is skipped when neither language nor text changed
* (cheap length+prefix fingerprint) — e.g. repeated editor renders.
*/
function highlightBlock(el, lang) {
if (!el) return;
var n = normalizeLang(lang || el.getAttribute('data-hljs-lang') || langClassOf(el));
el.setAttribute('data-hljs-lang', n);
if (n !== 'plaintext') {
el.classList.add('hljs');
el.classList.add('language-' + n);
}
if (!hasHljs() || n === 'plaintext') return;
try {
var fp = fingerprint(el);
if (el.dataset && el.dataset.hljsFp === fp) return; // unchanged
window.hljs.highlightElement(el);
if (el.dataset) el.dataset.hljsFp = fp;
} catch (e) { /* keep plain text */ }
}
function langClassOf(el) {
if (!el || !el.className) return '';
var m = /(?:^|\s)language-([\w-]+)/.exec(el.className);
return m ? m[1] : '';
}
function fingerprint(el) {
var t = el.textContent || '';
return (el.getAttribute('data-hljs-lang') || '') + '|' + t.length + ':' + t.slice(0, 64);
}
/**
* Highlight every `pre code` viewer block under `root`:
* `[data-hljs]` (explicit, e.g. editor code blocks) and
* `code[class*=language-]` (Gitea viewer, exports, public pages).
* Editable code blocks are included — their text round-trips losslessly.
*/
function highlightAllIn(root) {
var scope = root || document;
if (!hasHljs() || !scope.querySelectorAll) return 0;
var count = 0;
try {
var nodes = scope.querySelectorAll(
'pre code[data-hljs], pre code[data-hljs-lang], pre code[class*="language-"]'
);
for (var i = 0; i < nodes.length; i++) {
var el = nodes[i];
var lang = el.getAttribute('data-hljs-lang') || langClassOf(el) || 'plaintext';
var n = normalizeLang(lang);
if (n === 'plaintext') continue;
el.setAttribute('data-hljs-lang', n);
el.classList.add('hljs');
var fp = n + '|' + (el.textContent || '').length + ':' + (el.textContent || '').slice(0, 64);
if (el.dataset && el.dataset.hljsFp === fp) continue;
window.hljs.highlightElement(el);
if (el.dataset) el.dataset.hljsFp = fp;
count++;
}
} catch (e) { /* never break rendering */ }
return count;
}
window.FDHighlight = {
EXT_TO_LANG: EXT_TO_LANG,
FILE_TO_LANG: FILE_TO_LANG,
PICKER_LANGS: PICKER_LANGS,
normalizeLang: normalizeLang,
langForFile: langForFile,
labelFor: labelFor,
highlightText: highlightText,
highlightBlock: highlightBlock,
highlightAllIn: highlightAllIn,
hasHljs: hasHljs,
};
})();
+14 -3
View File
@@ -19,7 +19,8 @@ var _regGiteaWs = () => {
fileSize: 0,
fileSha: '',
fileLoading: false,
fileLanguage: 'text',
fileLanguage: 'plaintext',
fileLanguageLabel: 'Plain Text',
editContent: '',
commitMessage: 'Update via FlowDeck',
commitHistory: JSON.parse(localStorage.getItem('fd_commit_msgs') || '[]'),
@@ -290,17 +291,27 @@ var _regGiteaWs = () => {
this.fileContent = '[Error loading file]';
}
this.fileLoading = false;
// Highlight after Alpine renders
// Highlight after Alpine renders (FDHighlight covers ~230 file types;
// legacy Prism kept as a last-resort fallback).
var self = this;
this.$nextTick(function() {
var block = self.$refs.codeBlock;
if (block && block.textContent && typeof Prism !== 'undefined') {
if (!block || !block.textContent) return;
if (window.FDHighlight) {
try {
FDHighlight.highlightBlock(block, self.fileLanguage);
self.fileLanguageLabel = FDHighlight.labelFor(self.fileLanguage);
} catch (e) { /* plain text */ }
} else if (typeof Prism !== 'undefined') {
Prism.highlightElement(block);
}
});
},
getLanguage(path) {
if (window.FDHighlight) {
try { return FDHighlight.langForFile(path); } catch (e) { /* fall through */ }
}
var ext = (path || '').split('.').pop().toLowerCase();
var map = {
js:'javascript', jsx:'javascript', ts:'typescript', tsx:'typescript',
+776
View File
File diff suppressed because one or more lines are too long
+1213
View File
File diff suppressed because one or more lines are too long
+67 -9
View File
@@ -35,6 +35,9 @@ function libraryPage() {
shareDir: 'all',
showAuthor: false,
showSource: false,
showEditedBy: true,
showEditedTime: true,
showVisited: true,
// Tags
workspaceTags: [],
@@ -90,6 +93,12 @@ function libraryPage() {
for (var i = 0; i < this.flatItems.length; i++) {
var item = this.flatItems[i];
// ── En-tête de section de date (onglet AI Meeting Notes) ──
if (item._isSection) {
html += '<div class="lib-section-header">' + self._escHtml(item.title) + '</div>';
continue;
}
// ── Synthetic "+ Add new" placeholder row for an expanded parent ──
if (item._isAddNew) {
var pad = 30 + (item.depth || 0) * 20;
@@ -154,10 +163,19 @@ function libraryPage() {
html += '<div class="row-cell lib-col-author"><div class="row-author">';
html += '<div class="author-avatar">' + (item.author_initial || '?') + '</div>';
html += '<span>' + (item.author || '\u2014') + '</span></div></div>';
// Source
html += '<div class="row-cell lib-col-source"><span class="row-source">' + (item.source_label || 'Private') + '</span></div>';
// Last edited by
html += '<div class="row-cell lib-col-editedby"><div class="row-author">';
html += '<div class="author-avatar">' + (item.edited_by_initial || '?') + '</div>';
html += '<span>' + (item.edited_by || '\u2014') + '</span></div></div>';
// Edited
html += '<div class="row-cell lib-col-edited"><span class="row-time">' + self.formatTime(item.updated_at) + '</span></div>';
// Last visited
html += '<div class="row-cell lib-col-visited"><span class="row-time">' +
(item.visited_at ? self.formatTime(item.visited_at) : '\u2014') + '</span></div>';
// Source (icône de source + libellé) — en dernier, comme l'en-tête
html += '<div class="row-cell lib-col-source"><span class="row-source">' +
getSvgIcon(item.source_icon || 'file', 13) +
'<span>' + (item.source_label || 'Private') + '</span></span></div>';
html += '</div>';
}
body.innerHTML = html;
@@ -200,8 +218,9 @@ bindHtmlIcon(el) {
bindHtmlItem(el, item) {
Alpine.effect(() => { el.innerHTML = this._renderIcon(item); });
},
_renderIcon(item) {
_renderIcon(item) {
if (!item) return getSvgIcon('file', 14);
if (item.is_meeting) return getSvgIcon('calendar', 14);
// Custom emoji (image URL), icon name, or unicode emoji
if (item.page_icon) return '<span style="font-size:14px;line-height:1;display:inline-flex;align-items:center;">' + (window.fdIconHtml ? window.fdIconHtml(item.page_icon, 14) : this._escHtml(item.page_icon)) + '</span>';
if (item.is_folder || item.has_children) return getSvgIcon('folder', 14);
@@ -226,13 +245,47 @@ _renderIcon(item) {
return (r.tags || []).some(function(t) { return t.name === tf; });
});
}
if (this.tab === 'meetings') result = this._groupMeetings(result);
// Use splice for Alpine 3.14.9 reactivity (array reference reassign fails)
this.flatItems.splice(0, this.flatItems.length, ...result);
this.selectedCount = Object.keys(this.selected).length;
this.allSelected = this.flatItems.length > 0 && this.selectedCount === this.flatItems.length;
var selectable = result.filter(function(r) { return !r._isSection; });
this.allSelected = selectable.length > 0 && this.selectedCount === selectable.length;
this._renderTable();
},
// Regroupement par date pour l'onglet AI Meeting Notes : sections
// Today / Yesterday / This week / Last week / This month / Older.
_meetingBucket(updatedAt) {
var d = new Date(String(updatedAt || '').replace(' ', 'T'));
if (isNaN(d.getTime())) return 5;
var now = new Date();
var start = new Date(now.getFullYear(), now.getMonth(), now.getDate());
var diff = Math.floor((start.getTime() - new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime()) / 86400000);
if (diff <= 0) return 0;
if (diff === 1) return 1;
if (diff <= 7) return 2;
if (diff <= 14) return 3;
if (diff <= 30) return 4;
return 5;
},
_groupMeetings(items) {
var labels = ['Today', 'Yesterday', 'This week', 'Last week', 'This month', 'Older'];
var buckets = [[], [], [], [], [], []];
for (var i = 0; i < items.length; i++) {
if (items[i]._isSection) continue;
buckets[this._meetingBucket(items[i].updated_at)].push(items[i]);
}
var out = [];
for (var b = 0; b < buckets.length; b++) {
if (!buckets[b].length) continue;
out.push({ _isSection: true, id: 'sec-' + b, title: labels[b] });
for (var j = 0; j < buckets[b].length; j++) out.push(buckets[b][j]);
}
return out;
},
// ── Global dispatch (bypasses Alpine x-for reactivity issues) ──
toggleItemById(id) {
var item = this._findItem(id);
@@ -399,7 +452,7 @@ _renderIcon(item) {
},
_canAddChild() {
return this.tab !== 'favorites' && this.tab !== 'shared' && this.tab !== 'published' && this.tab !== 'repository';
return this.tab !== 'favorites' && this.tab !== 'shared' && this.tab !== 'published' && this.tab !== 'repository' && this.tab !== 'meetings';
},
_getCsrf() {
@@ -443,13 +496,14 @@ _renderIcon(item) {
emptyText: '',
_recomputeEmpty() {
var icons = {recents:'clock',favorites:'star',shared:'users',published:'globe',private:'lock',workspace:'folder',repository:'folder'};
var icons = {recents:'clock',meetings:'calendar',favorites:'star',shared:'users',published:'globe',private:'lock',workspace:'folder',repository:'folder'};
this.emptyIcon = icons[this.tab] || 'file';
var titles = {recents:'No recent pages',favorites:'No favorites',shared:'No shared pages',
var titles = {recents:'No recent pages',meetings:'No meeting notes',favorites:'No favorites',shared:'No shared pages',
published:'No published pages',private:'No private pages',workspace:'No workspace pages',
repository:'Empty repository'};
this.emptyTitle = titles[this.tab] || 'No items';
var texts = {recents:'Pages you\'ve recently viewed or edited will appear here.',
meetings:'Create a meeting note from the sidebar (+) — it will appear here, grouped by date.',
favorites:'Right-click a page in the sidebar and select "Add to Favorites".',
shared:'Shared pages from your team will appear here.',
published:'Pages published to the web will appear here.',
@@ -478,7 +532,7 @@ _renderIcon(item) {
console.log('[Library] workspaceId=' + this.workspaceId + ' isGitea=' + this.isGiteaActive);
var params = new URLSearchParams(window.location.search);
var t = params.get('tab');
if (t && ['recents','favorites','shared','published','private','workspace','repository'].indexOf(t) !== -1) {
if (t && ['recents','meetings','favorites','shared','published','private','workspace','repository'].indexOf(t) !== -1) {
this.tab = t;
}
this._recomputeEmpty();
@@ -524,7 +578,7 @@ _renderIcon(item) {
if (this.tab === 'repository' && this.isGiteaActive) {
url += sep + 'gitea_owner=' + encodeURIComponent(this.giteaOwner) + '&gitea_repo=' + encodeURIComponent(this.giteaRepo);
sep = '&';
} else if (this.tab !== 'repository' && this.tab !== 'recents' && this.tab !== 'favorites') {
} else if (this.tab !== 'repository' && this.tab !== 'recents' && this.tab !== 'favorites' && this.tab !== 'meetings') {
// Hierarchical tree view: show only root-level pages at the top
url += sep + 'tree=1';
sep = '&';
@@ -573,6 +627,9 @@ _renderIcon(item) {
toggleColumn(col) {
if (col === 'author') this.showAuthor = !this.showAuthor;
else if (col === 'source') this.showSource = !this.showSource;
else if (col === 'editedby') this.showEditedBy = !this.showEditedBy;
else if (col === 'editedtime') this.showEditedTime = !this.showEditedTime;
else if (col === 'visited') this.showVisited = !this.showVisited;
},
doSort() {
@@ -650,6 +707,7 @@ _renderIcon(item) {
} else {
var s = {};
for (var i = 0; i < this.flatItems.length; i++) {
if (this.flatItems[i]._isSection || this.flatItems[i]._isAddNew) continue;
s[this.flatItems[i].id] = true;
}
this.selected = s;
+111 -12
View File
@@ -1,6 +1,12 @@
// Config de la page courante — relue à CHAQUE exécution du script : sur une
// navigation partielle htmx le <script> est ré-exécuté (avec le nouveau
// #lw-config) alors que le corps du fichier est verrouillé par le garde
// ci-dessous. Avec un `const` capturé à la première exécution, la page
// gardait le folder/workspace du premier chargement.
var LW = (function(){try{var el=document.getElementById('lw-config');return el?JSON.parse(el.textContent):{}}catch{return {}}})();
if (!window.__fdLocalWorkspaceScriptsLoaded) {
window.__fdLocalWorkspaceScriptsLoaded = true;
const LW=(()=>{try{const el=document.getElementById('lw-config');return el?JSON.parse(el.textContent):{}}catch{return {}}})();
console.log('FLOWDECK v2.4.6 — IIFE running, about to set _wsInitData');
const _wsInitData = (function() { // lexical : NON propriété globalThis →
@@ -150,6 +156,19 @@ const _wsInitData = (function() { // lexical : NON propriété globalThis →
try {
// Expose globally for event handlers (BEFORE await)
window._wsData = this;
// Navigation partielle : l'objet est partagé entre les montages →
// on recale l'état sur la page courante (#lw-config fraîchement lu)
// et on remet à zéro les panneaux/menus de la vue précédente.
if (LW && LW.current_folder_id != null) this.currentFolder = LW.current_folder_id;
this.searchQuery = '';
this.filterType = '';
this.editingPath = false;
this.previewItem = null;
this.uploading = false;
this.undoVisible = false;
this.moveModalOpen = false;
this.isDragging = false;
this.clearSelection();
var url = '/api/local-workspace/tree';
if (this.currentFolder && this.currentFolder > 0) {
url += '?folder=' + this.currentFolder;
@@ -197,6 +216,32 @@ const _wsInitData = (function() { // lexical : NON propriété globalThis →
} catch(e) { console.error('Tree load error:', e); this.loaded = true; }
this._recompute();
this._initResizeHandle();
this._syncHeaderPath();
},
/** Recalcule le chemin du header (Home / <workspace> / <dossier> / …) à
partir de la pile de dossiers courante — même mécanique que le rendu
serveur, mais sans rechargement. */
_syncHeaderPath() {
var stack = this.folderStack || [];
var crumbs = [
{ label: 'Home', url: '/workspaces', id: null, icon: 'home', menu: false, home: true },
{ label: (LW && LW.workspace_name) || 'Workspace', url: '/local-workspace',
id: null, icon: 'folder', menu: false }
];
for (var i = 0; i < stack.length; i++) {
var last = (i === stack.length - 1);
crumbs.push({
label: stack[i].name,
url: last ? null : '/local-workspace?folder=' + stack[i].id,
id: stack[i].id,
icon: 'folder',
menu: true
});
}
window.dispatchEvent(new CustomEvent('flowdeck:breadcrumb-changed', {
detail: { workspace_id: (LW && LW.workspace_id) || 0, crumbs: crumbs }
}));
},
_initResizeHandle() {
@@ -486,6 +531,12 @@ const _wsInitData = (function() { // lexical : NON propriété globalThis →
if (b.type === 'code' || b.type === 'code_block') {
tag = 'pre';
style += 'background:var(--bg-tertiary);padding:10px 14px;border-radius:6px;font-family:monospace;font-size:12px;white-space:pre-wrap;';
var cLang = (window.FDHighlight && b.language) ? FDHighlight.normalizeLang(b.language) : '';
var cAttr = (cLang && cLang !== 'plaintext') ? ' data-hljs data-hljs-lang="' + cLang + '" class="hljs language-' + cLang + '"' : '';
var cHtml = (window.FDHighlight && cLang && cLang !== 'plaintext')
? (function(){ try { return FDHighlight.highlightText(txt, cLang); } catch (e) { return ''; } })() || this._escHtml(txt)
: this._escHtml(txt);
return '<' + tag + ' style="' + style + '"><code' + cAttr + '>' + (txt ? cHtml : '<br>') + '</code></' + tag + '>';
}
return '<' + tag + ' style="' + style + '">' + (txt ? this._escHtml(txt) : '<br>') + '</' + tag + '>';
}, this).join('');
@@ -493,7 +544,7 @@ const _wsInitData = (function() { // lexical : NON propriété globalThis →
preview = '<div style="text-align:center;padding:30px;color:var(--text-tertiary)">Empty document</div>';
}
} catch {
preview = '<pre style="background:var(--bg-tertiary);padding:14px;border-radius:6px;overflow:auto;font-size:12px;font-family:monospace;white-space:pre-wrap;">' + this._escHtml(String(content).substring(0, 3000)) + '</pre>';
preview = this._codeHtml(String(content).substring(0, 3000), node.name, 3000);
}
} else if (format === 'markdown') {
@@ -501,15 +552,16 @@ const _wsInitData = (function() { // lexical : NON propriété globalThis →
preview = '<div style="white-space:pre-wrap;line-height:1.6;font-size:13px;">' + this._escHtml(String(content).substring(0, 5000)) + '</div>';
} else {
// Unknown format — show as text
// Unknown format — show as (highlighted) text
var display = String(content || '').trim();
if (display) {
preview = '<pre style="background:var(--bg-tertiary);padding:14px;border-radius:6px;overflow:auto;font-size:12px;font-family:monospace;white-space:pre-wrap;">' + this._escHtml(display.substring(0, 5000)) + '</pre>';
preview = this._codeHtml(display, node.name, 5000);
} else {
preview = '<div style="text-align:center;padding:30px;color:var(--text-tertiary)">No content</div>';
}
}
body.innerHTML = preview || '<div style="color:var(--text-tertiary);text-align:center;padding:20px">(empty)</div>';
try { if (window.FDHighlight) FDHighlight.highlightAllIn(body); } catch (e) {}
} catch {
body.innerHTML = '<div style="text-align:center;padding:20px;color:var(--text-tertiary)">Preview not available</div>';
}
@@ -519,6 +571,23 @@ const _wsInitData = (function() { // lexical : NON propriété globalThis →
return String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
},
// ── hljs: code-aware <pre> for previews (FDHighlight, ~230 file types).
// Falls back to escaped plain text when the helper is unavailable.
_codeHtml(text, name, maxLen) {
var t = String(text == null ? '' : text).substring(0, maxLen || 5000);
if (window.FDHighlight) {
try {
var lang = FDHighlight.langForFile(name || '');
var inner = FDHighlight.highlightText(t, lang);
var cls = lang !== 'plaintext' ? ' class="hljs language-' + lang + '"' : '';
return '<pre style="background:var(--bg-tertiary);padding:14px;border-radius:6px;overflow:auto;font-size:12px;font-family:monospace;white-space:pre-wrap;">'
+ '<code' + cls + '>' + inner + '</code></pre>';
} catch (e) { /* fallback below */ }
}
return '<pre style="background:var(--bg-tertiary);padding:14px;border-radius:6px;overflow:auto;font-size:12px;font-family:monospace;white-space:pre-wrap;">'
+ this._escHtml(t) + '</pre>';
},
// ── Content snippet for content view ──
contentSnippetMap: {},
@@ -765,7 +834,10 @@ const _wsInitData = (function() { // lexical : NON propriété globalThis →
html += '<span class="icon">'+icon+'</span>';
if (c.is_folder) {
var countBadge = (c.child_count > 0) ? ' <span class="child-count">('+c.child_count+')</span>' : '';
html += '<span class="name folder" onclick="window._wsData.navigateToFolder('+escId+')">'+c.name+countBadge+'</span>';
var statusBadges = (c.is_shared ? '<span class="ws-shared-badge" title="Partag\u00e9" onclick="event.stopPropagation()">👥</span>' : '')
+ (c.favorited ? '<span class="ws-fav-badge" title="Favorite">★</span>' : '')
+ (c.published ? '<span class="ws-pub-badge" title="Published to web">🌐</span>' : '');
html += '<span class="name folder" onclick="window._wsData.navigateToFolder('+escId+')">'+statusBadges+c.name+countBadge+'</span>';
} else {
// Click to rename, double-click to open
html += '<span class="name file" onclick="event.stopPropagation();window._wsData.startRenderChildRename('+escId+',\''+escName+'\')"'
@@ -773,8 +845,10 @@ const _wsInitData = (function() { // lexical : NON propriété globalThis →
+ ' title="Click to rename, double-click to open"'
+ ' onmouseenter="var d=window._wsData;if(d)d.showPreview(event,{id:'+escId+',name:\''+escName+'\',is_folder:false,content_format:\''+(c.content_format||'')+'\'})"'
+ ' onmouseleave="var d=window._wsData;if(d)d.hidePreview()">'
+ (c.is_shared ? '<span class="ws-shared-badge" title="Partag\u00e9" onclick="event.stopPropagation()">👥</span>' : '')
+ '<span id="rc-name-'+escId+'">'+c.name+'</span>'
+ (c.is_shared ? '<span class="ws-shared-badge" title="Partag\u00e9" onclick="event.stopPropagation()">👥</span>' : '')
+ (c.favorited ? '<span class="ws-fav-badge" title="Favorite">★</span>' : '')
+ (c.published ? '<span class="ws-pub-badge" title="Published to web">🌐</span>' : '')
+ '<span id="rc-name-'+escId+'">'+c.name+'</span>'
+ '<input id="rc-input-'+escId+'" class="inline-rename-input" style="display:none"'
+ ' onkeydown="if(event.key===\'Enter\')window._wsData.commitRenderChildRename('+escId+')"'
+ ' onblur="window._wsData.commitRenderChildRename('+escId+')"'
@@ -956,6 +1030,7 @@ const _wsInitData = (function() { // lexical : NON propriété globalThis →
this._saveExpandedState();
this.previewItem = null;
this._recompute();
this._syncHeaderPath();
var self = this;
this.$nextTick(function() { self._syncCheckboxes(); });
} catch(e) { console.error('Load folder error:', e); }
@@ -1230,8 +1305,9 @@ const _wsInitData = (function() { // lexical : NON propriété globalThis →
} else {
this.previewContent = 'Loading...';
this._fetchRetry(url).then(function(t) {
var content = t.substring(0, 500);
self.previewContent = content; self.previewCache[cacheKey] = content;
var html = self._codeHtml(t, node.name, 500);
// Tooltip body is already monospace/pre-wrap: keep it lightweight.
self.previewContent = html; self.previewCache[cacheKey] = html;
}).catch(function() { self.previewContent = '(preview not available)'; });
}
} else {
@@ -2043,9 +2119,32 @@ console.log('FLOWDECK v2.4.6 — IIFE done, _wsInitData keys:', Object.keys(_wsI
// A20 ph3 : registre Alpine.data (le build CSP ne résout que le registre ;
// x-data="wsInitData()" au lieu de l'identifiant global _wsInitData).
var _regWs = function () {
// objet partagé JS↔Alpine via closure LEXICALE (const hors window →
// non snapshoté par ji ; probe4 : factory retournant l'objet = accepté).
Alpine.data('wsInitData', function () { return _wsInitData; });
// Objet partagé JS↔Alpine via closure LEXICALE (const hors window →
// non snapshoté par ji). L'état reste unique, MAIS la factory Alpine
// doit retourner une enveloppe FRAÎCHE à chaque montage : Alpine pose ses
// magics ($nextTick…) avec Object.defineProperty non-configurable. En
// retournant le même objet, le 2e montage (navigation partielle
// /local-workspace → /local-workspace?folder=) levait
// « Cannot redefine property: $nextTick », initTree échouait et TOUT le
// contenu restait au state brut (tous les x-show visibles à la fois).
// L'enveloppe délègue lecture/écriture au proxy réactif partagé.
Alpine.data('wsInitData', function () {
var R = Alpine.reactive(_wsInitData);
var t = {};
Object.keys(_wsInitData).forEach(function (k) {
if (typeof _wsInitData[k] === 'function') {
t[k] = _wsInitData[k];
} else {
Object.defineProperty(t, k, {
get: function () { return R[k]; },
set: function (v) { R[k] = v; },
enumerable: true,
configurable: true,
});
}
});
return t;
});
// Bloc preview : le parseur le place HORS de la div racine (structure
// pré-existante) → montage distinct DÉLÉGUANT vers l'objet partagé.
// Wrapper = objet unique (les magics $nextTick… ne sont redéfinissables
File diff suppressed because it is too large Load Diff
+52 -5
View File
@@ -13,6 +13,7 @@ const PD=(()=>{try{const el=document.getElementById('page-data');return el?JSON.
]},{name:'AI',items:[
{id:'ai_write',name:'Write with AI',icon:'✨'},{id:'ai_summarize',name:'Summarize',icon:'📝'},
{id:'ai_translate',name:'Translate',icon:'🌐'},{id:'ai_continue',name:'Continue writing',icon:'➡️'},
{id:'meeting',name:'AI Meeting Notes',icon:'🎙️'},
]},
{name:'MEDIA',items:[
{id:'image',name:'Image',icon:'🖼️'},{id:'embed',name:'Embed',icon:'🔗'},
@@ -398,7 +399,7 @@ const PD=(()=>{try{const el=document.getElementById('page-data');return el?JSON.
// ── Block type shortcuts (shown on the right of each slash item) ──
// Raccourcis = préfixes markdown (alignés sur la description mobile Notion : -, [], >, ---)
const _SHORTCUT={text:'T',heading_1:'#',heading_2:'##',heading_3:'###',heading_4:'####',bulleted_list:'-',numbered_list:'1.',to_do:'[]',toggle:'>',quote:'"',callout:'!',table_of_contents:'TOC',math:'∑',columns:'‖',divider:'---',code:'<>',table:'|',image:'[img]',embed:'[🔗]',bookmark:'[🔖]',video:'[🎬]',audio:'[🎵]',database:'DB',ai_write:'/ai',ai_summarize:'/ai',ai_translate:'/ai',ai_continue:'/ai',duplicate:'Ctrl+D',turn_into:'⋯',copy_link:'#',delete:'⌫'};
const _SHORTCUT={text:'T',heading_1:'#',heading_2:'##',heading_3:'###',heading_4:'####',bulleted_list:'-',numbered_list:'1.',to_do:'[]',toggle:'>',quote:'"',callout:'!',table_of_contents:'TOC',math:'∑',columns:'‖',divider:'---',code:'<>',table:'|',image:'[img]',embed:'[🔗]',bookmark:'[🔖]',video:'[🎬]',audio:'[🎵]',database:'DB',ai_write:'/ai',ai_summarize:'/ai',ai_translate:'/ai',ai_continue:'/ai',meeting:'/meet',duplicate:'Ctrl+D',turn_into:'⋯',copy_link:'#',delete:'⌫'};
const _GROUP_LABEL={BASIC:'Basic blocks',AI:'AI writing',MEDIA:'Media',DATA:'Data',ACTIONS:'Actions',SYNCED:'Synced'};
// ── Hover preview card (matches Notion's visual hint) ──
function _previewHtml(blockId){
@@ -426,6 +427,7 @@ const PD=(()=>{try{const el=document.getElementById('page-data');return el?JSON.
case 'video': return p('<div style="border:1px solid var(--border);border-radius:6px;padding:14px;text-align:center;color:var(--text-dim);font-size:12px">🎬 Inline video player</div>');
case 'audio': return p('<div style="border:1px solid var(--border);border-radius:6px;padding:14px;text-align:center;color:var(--text-dim);font-size:12px">🎵 Inline audio player</div>');
case 'database': return p('<div style="border:1px solid var(--border);border-radius:6px;padding:10px;font-size:12px">📊 Inline database table</div>');
case 'meeting': return p('<div style="border:1px solid var(--border);border-radius:6px;padding:10px;font-size:12px">🎙️ <b>AI Meeting Notes</b><br><span style="color:var(--text-dim)">Record, transcribe and summarize this meeting.</span></div>');
default: return p('<div style="font-size:13px;color:var(--text-dim)">Text block</div>');
}
}
@@ -751,6 +753,28 @@ const PD=(()=>{try{const el=document.getElementById('page-data');return el?JSON.
};
window.AIAC=AIAC;
// ── Code block renderer (hljs: highlight.js syntax highlighting) — literal
// text (esc, real newlines — NOT mdEsc, whose <br> would break
// highlight.js textContent reads) plus a language picker driving
// FDHighlight (~230 file types). Highlight spans are transparent to
// sync(): gt()/gtTok() re-read text only.
function renderCodeBlock(b){
var rawLang=b.language||'Plain Text';
var norm=(window.FDHighlight?FDHighlight.normalizeLang(rawLang):String(rawLang||'plaintext').toLowerCase().replace(/[^a-z0-9+#-]+/g,''))||'plaintext';
var ro=!!(window.E&&E.canEdit===false);
var head;
if(!ro&&window.FDHighlight){
var opts=FDHighlight.PICKER_LANGS.map(function(id){
return '<option value="'+id+'"'+(id===norm?' selected':'')+'>'+esc(FDHighlight.labelFor(id))+'</option>';
}).join('');
head='<div class="code-lang-label code-lang-bar"><select class="code-lang-select" onchange="E.setCodeLang(\''+b.id+'\',this.value)" title="Code language — syntax highlighting">'+opts+'</select></div>';
}else{
var label=(window.FDHighlight?FDHighlight.labelFor(norm):rawLang);
head='<div class="code-lang-label">'+esc(label)+'</div>';
}
return head+'<pre><code data-bid="'+b.id+'" data-hljs data-hljs-lang="'+norm+'" class="language-'+norm+'" contenteditable="'+(ro?'false':'true')+'" spellcheck="false">'+esc(b.content||'')+'</code></pre>';
}
function renderBlock(b,idx){
if(b.type==='meeting')return renderMeetingBlock(b,idx);
if(b.type==='table')return renderTableBlock(b,idx);
@@ -813,7 +837,7 @@ const PD=(()=>{try{const el=document.getElementById('page-data');return el?JSON.
let inner='';
if(b.type==='to_do')inner=`<input type="checkbox" class="todo-checkbox" ${b.checked?'checked':''} onchange="E._doToggle('${b.id}',this.checked)"><div data-bid="${b.id}" contenteditable="true" data-placeholder="${ph}" spellcheck="false">${mdEsc(b.content)}</div>`;
else if(b.type==='toggle')inner=`<div class="toggle-title-row"><button class="toggle-chevron ${b.expanded?'open':''}" onclick="E._doToggleExpand('${b.id}')">▶</button><div class="toggle-title" data-bid="${b.id}" contenteditable="true" data-placeholder="${ph}" spellcheck="false">${mdEsc(b.content)}</div></div><div class="toggle-children" ${(b.expanded===false)?'style="display:none"':''}>${(b.children||[]).map((ch)=>renderNestedBlock(ch)).join('')}</div>`;
else if(b.type==='code')inner=`<div class="code-lang-label">${b.language||'Plain Text'}</div><pre><code data-bid="${b.id}" contenteditable="true" spellcheck="false">${mdEsc(b.content)}</code></pre>`;
else if(b.type==='code')inner=renderCodeBlock(b);
else if(b.type==='callout')inner=`<span class="callout-icon" onclick="E._toggleCalloutIcon('${b.id}')" title="Change icon">${b.icon||'💡'}</span><div data-bid="${b.id}" contenteditable="true" data-placeholder="${ph}" spellcheck="false">${mdEsc(b.content)}</div><div class="callout-icon-picker" id="callout-picker-${b.id}" style="display:none">${CALLOUT_ICONS.map(function(i){return '<button type="button" class="callout-icon-opt" onclick="E._setCalloutIcon(\''+b.id+'\',\''+i+'\')">'+i+'</button>';}).join('')}</div>`;
else inner=`<div data-bid="${b.id}" contenteditable="true" data-placeholder="${ph}" spellcheck="false">${mdEsc(b.content)}</div>`;
const tag=b.type.startsWith('heading_')?'h'+b.type.slice(-1):b.type==='quote'?'blockquote':'div';
@@ -1656,12 +1680,13 @@ const PD=(()=>{try{const el=document.getElementById('page-data');return el?JSON.
if(mime==='application/pdf'||ext==='pdf'){
this.blocks=[this.mkB('embed','',{src:url,embed_type:'pdf'})];this.render();return;
}
const codeExts=['py','js','ts','jsx','tsx','html','htm','css','json','xml','yaml','yml','md','markdown','sql','sh','bash','ps1','bat','cmd','rb','go','rs','java','c','cpp','h','hpp','php','swift','kt','scala','r','toml','ini','cfg','conf','env','txt','log'];
const _fh=(typeof window!=='undefined'&&window.FDHighlight)?window.FDHighlight:null;
const codeExts=_fh?Object.keys(_fh.EXT_TO_LANG):['py','js','ts','jsx','tsx','html','htm','css','json','xml','yaml','yml','md','markdown','sql','sh','bash','ps1','bat','cmd','rb','go','rs','java','c','cpp','h','hpp','php','swift','kt','scala','r','toml','ini','cfg','conf','env','txt','log'];
const langMap={'py':'python','js':'javascript','ts':'typescript','jsx':'javascript','tsx':'typescript','html':'html','htm':'html','css':'css','json':'json','xml':'xml','yaml':'yaml','yml':'yaml','md':'markdown','markdown':'markdown','sql':'sql','sh':'bash','bash':'bash','ps1':'powershell','bat':'plaintext','cmd':'plaintext','rb':'ruby','go':'go','rs':'rust','java':'java','c':'c','cpp':'cpp','h':'c','hpp':'cpp','php':'php','swift':'swift','kt':'kotlin','scala':'scala','r':'r','toml':'toml','ini':'ini','cfg':'ini','conf':'ini','env':'ini','txt':'plaintext','log':'plaintext'};
if(mime.startsWith('text/')||codeExts.includes(ext)){
try{const r=await fetch(url);const text=await r.text();
if(ext==='md'||ext==='markdown'){this.blocks=this.md2b(text);}
else{this.blocks=[this.mkB('code',text,{language:langMap[ext]||'Plain Text'})];}
else{const lang=_fh?_fh.langForFile(name):(langMap[ext]||'Plain Text');this.blocks=[this.mkB('code',text,{language:lang})];}
this.render();
}catch(e){this.blocks=[this.mkB('paragraph','Error loading file: '+e.message)];this.render();}
return;
@@ -1717,6 +1742,9 @@ const PD=(()=>{try{const el=document.getElementById('page-data');return el?JSON.
if(b.type==='columns'&&b.children)b.children.forEach(function(ch){const che=ct.querySelector(`[data-bid="${ch.id}"]`);if(che)ch.content=gtMd(che)||'';});
}},
render(){const ct=document.getElementById('_blocksCt');if(!ct)return;let html='';for(let i=0;i<this.blocks.length;i++)html+=renderBlock(this.blocks[i],i);ct.innerHTML=html;
// hljs: syntax-highlight code blocks (FDHighlight skips unchanged
// blocks via content fingerprint; spans are sync-transparent).
try{if(window.FDHighlight)FDHighlight.highlightAllIn(ct);}catch(e){}
// Render math blocks with KaTeX
var self=this;
ct.querySelectorAll('.math-display').forEach(function(el){var bid=el.dataset.bid;var b=self.blocks.find(function(x){return x.id===bid;});self._renderKatex(el,b?b.content:'');});
@@ -1983,6 +2011,8 @@ replaceBlock(idx,type){const b=this.blocks[idx];if(!b)return;this.sync();this.pu
// ── v5.10.0: Turn into / couleurs / liens / déplacement ──
_blockTargets(idx){const sel=_selIndicesSorted();return(sel.includes(idx))?sel:[idx];},
// hljs: code-block language picker (drives FDHighlight re-render).
setCodeLang(bid,lang){const idx=this.getIdx(bid);if(idx<0)return;const b=this.blocks[idx];if(!b||b.type!=='code')return;this.sync();this.pushHistory();b.language=(window.FDHighlight?FDHighlight.normalizeLang(lang):lang)||'Plain Text';this.dirty=true;this.autoSave();this.render();_rtSync();},
turnInto(idx,type,cols){if(idx<0||idx>=this.blocks.length)return;if(!_BLOCK_TURN_TYPES.some(t=>t.id===type))return;const targets=this._blockTargets(idx);const focusId=this.blocks[idx].id;this.sync();this.pushHistory();targets.forEach(i=>{const b=this.blocks[i];if(!b)return;if(b.type===type)return;const keep=(b.content||'').trim();b.type=type;if(keep&&['image','embed','meeting','divider','table_of_contents','database','button','columns'].indexOf(b.type)<0){b.content=keep;}else{b.content='';}if(type==='toggle'){b.expanded=true;if(!Array.isArray(b.children))b.children=[];}if(type==='to_do')b.checked=false;if(type==='table'&&!Array.isArray(b.rows))b.rows=_tblNew(3,3);if(type==='code'&&!b.language)b.language='Plain Text';if(type==='columns'&&(!Array.isArray(b.children)||!b.children.length))b.children=[{id:genId(),type:'paragraph',content:''}];if(type==='columns'&&cols>=2){b.children=Array.from({length:cols},()=>({id:genId(),type:'paragraph',content:''}));}if(type==='synced'){b.synced_id=0;b._synced_content=[];b._synced_title='';}if(type==='button'){b.automation_id=0;b.automation_name='';}});
if(type==='synced')setTimeout(()=>this._pickSyncedBlock(idx),80);
this.dirty=true;this.autoSave();this.render();_rtSync();_selClear();
@@ -2701,7 +2731,24 @@ applyAIBlocks(text){
return;
}
if(id.indexOf('ai_')===0){ this.aiSlash(id,idx); return; }
if(id==='duplicate'){this.duplicateBlock(idx);return;}
if(id==='duplicate'){this.duplicateBlock(idx);return;}
if(id==='meeting'){
// v7.1.1 : bloc AI Meeting Notes (façon Notion) — même action que
// meetingNote(), mais en place dans le bloc courant (/meet).
const block=this.blocks[idx];this.sync();this.pushHistory();
const today=new Date();const iso=today.toISOString().slice(0,10);
block.type='meeting';block.content='';
block.meeting={v:2,title:'Meeting @'+iso,date:iso,state:'idle',
mode:'mic_only',instruction:'auto',lang:'fr',consent:null,
consentAudio:false,tab:'notes',channels:[],segments:[],segSeq:0,
notes:'',manual:'',interim:'',startedAt:0,elapsedBase:0,pausedMs:0,
steps:[],summaryMd:'',completeness:'',genTitle:'',titleEdited:false,
shareHidden:false,bannerHidden:false,transcriptId:null,error:'',
remoteSilentWarned:false,menuOpen:false,settingsOpen:false};
this.render();this.dirty=true;this.autoSave();_rtSync();
setTimeout(()=>{const el=document.querySelector('[data-meeting-bid="'+block.id+'"]');if(el&&el.scrollIntoView)el.scrollIntoView({behavior:'smooth',block:'center'});},60);
return;
}
if(id==='delete'){this.removeBlock(idx);return;}
if(id==='copy_link'){const blk=this.blocks[idx];if(blk)this.copyBlockLink(blk.id);return;}
if(id==='turn_into'){const blk=this.blocks[idx];if(!blk)return;const el=this.getEl(blk.id);const rk=el?el.getBoundingClientRect():null;blockMenuOpen(rk?rk.left:window.innerWidth/2,rk?rk.bottom:window.innerHeight/2,blk.id);return;}