fix: /local-workspace — chemin du header + clic sur un dossier du sidebar (v7.69.9)
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:
+122
-14
@@ -540,41 +540,42 @@ html.fd-sidebar-collapsed .editor-statusbar { left: 0; }
|
||||
.sidebar-item.empty-hint:hover { background: none; }
|
||||
.sidebar-item.empty-hint .page-name { font-size: 12px; }
|
||||
|
||||
/* Shared subgroups (Par moi / Avec moi) */
|
||||
/* Shared subgroups (Par moi / Avec moi) — compact */
|
||||
.sidebar-subgroup {
|
||||
padding-top: 2px;
|
||||
padding-top: 1px;
|
||||
}
|
||||
.sidebar-subgroup + .sidebar-subgroup {
|
||||
margin-top: 6px;
|
||||
padding-top: 6px;
|
||||
margin-top: 4px;
|
||||
padding-top: 4px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
.sidebar-subgroup-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 8px;
|
||||
font-size: 11px;
|
||||
padding: 2px 8px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
/* Sections */
|
||||
/* Sections — compact (v7.70 : une seule ouverte à la fois, paddings réduits) */
|
||||
.sidebar-section {
|
||||
padding: var(--space-xs) var(--space-md);
|
||||
padding: 1px 6px;
|
||||
}
|
||||
|
||||
.sidebar-section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 4px 6px;
|
||||
padding: 3px 4px;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
transition: background var(--transition);
|
||||
user-select: none;
|
||||
min-height: 24px;
|
||||
}
|
||||
|
||||
.sidebar-section-header:hover {
|
||||
@@ -589,8 +590,8 @@ html.fd-sidebar-collapsed .editor-statusbar { left: 0; }
|
||||
.sidebar-section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
font-size: 12px;
|
||||
gap: 2px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--text-dim);
|
||||
text-transform: none;
|
||||
@@ -754,6 +755,10 @@ html.fd-sidebar-collapsed .editor-statusbar { left: 0; }
|
||||
/* Customize Sidebar Panel */
|
||||
.sidebar-customize-panel {
|
||||
padding: 8px 12px;
|
||||
/* v7.69.6 : au-dessus de .customize-overlay (z 998) — sinon chaque clic,
|
||||
même sur le panneau, touche l'overlay qui referme le mode customize. */
|
||||
position: relative;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.scp-header {
|
||||
@@ -853,14 +858,14 @@ html.fd-sidebar-collapsed .editor-statusbar { left: 0; }
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 2px 4px;
|
||||
padding: 1px 4px;
|
||||
padding-left: 4px;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 13px;
|
||||
font-size: 12.5px;
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
transition: background var(--transition);
|
||||
min-height: 24px;
|
||||
min-height: 22px;
|
||||
}
|
||||
|
||||
.sidebar-item:hover {
|
||||
@@ -1037,6 +1042,80 @@ html.fd-sidebar-collapsed .editor-statusbar { left: 0; }
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* ── Bouton "New" + popover "Create new" (v7.70, style Notion) ──
|
||||
Popover ouvert vers le haut, bouton pilule en bas de la sidebar. */
|
||||
.fd-new-wrap {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.fd-new-btn {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
border: 1px solid transparent;
|
||||
cursor: pointer;
|
||||
transition: background 150ms, border-color 150ms;
|
||||
}
|
||||
.fd-new-btn:hover,
|
||||
.fd-new-btn.open {
|
||||
background: var(--bg-active);
|
||||
border-color: var(--border-light);
|
||||
}
|
||||
.fd-new-popover {
|
||||
position: absolute;
|
||||
bottom: calc(100% + 8px);
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: var(--bg-modal);
|
||||
border: 1px solid var(--border-light);
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,.35);
|
||||
padding: 6px;
|
||||
z-index: 500;
|
||||
animation: fd-new-pop-in 150ms ease-out;
|
||||
transform-origin: bottom center;
|
||||
}
|
||||
@keyframes fd-new-pop-in {
|
||||
from { opacity: 0; transform: translateY(6px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
.fd-new-pop-header {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--text-dim);
|
||||
padding: 6px 10px 4px;
|
||||
}
|
||||
.fd-new-pop-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
padding: 8px 10px;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-secondary);
|
||||
font-size: 14px;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition: background 120ms, color 120ms;
|
||||
}
|
||||
.fd-new-pop-item:hover,
|
||||
.fd-new-pop-item.active {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
.fd-new-pop-item svg { flex-shrink: 0; }
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════
|
||||
CONTEXT MENU
|
||||
═══════════════════════════════════════════════════════════ */
|
||||
@@ -2654,6 +2733,35 @@ button.breadcrumb-current { max-width: 260px; }
|
||||
background: var(--bg-tertiary);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.code-lang-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.code-lang-select {
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--text-dim);
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
max-width: 220px;
|
||||
}
|
||||
.code-lang-select:hover {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
.code-lang-select option {
|
||||
background: var(--bg-modal, #1f1f1f);
|
||||
color: var(--text-primary, #eee);
|
||||
font-weight: 400;
|
||||
}
|
||||
/* highlight.js inside editor code blocks: theme colors the tokens,
|
||||
the block keeps its app surface (transparent hljs background). */
|
||||
.block-code code.hljs {
|
||||
background: transparent;
|
||||
}
|
||||
.block-code pre {
|
||||
margin: 0;
|
||||
padding: 12px 16px;
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
/* FlowDeck — highlight.js themes (cdnjs 11.9.0): github (light) + github-dark scoped on html[data-theme="dark"] */
|
||||
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*!
|
||||
Theme: GitHub
|
||||
Description: Light theme as seen on github.com
|
||||
Author: github.com
|
||||
Maintainer: @Hirse
|
||||
Updated: 2021-05-15
|
||||
|
||||
Outdated base version: https://github.com/primer/github-syntax-light
|
||||
Current colors taken from GitHub's CSS
|
||||
*/.hljs{color:#24292e;background:#fff}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#d73a49}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#6f42c1}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#005cc5}.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#032f62}.hljs-built_in,.hljs-symbol{color:#e36209}.hljs-code,.hljs-comment,.hljs-formula{color:#6a737d}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#22863a}.hljs-subst{color:#24292e}.hljs-section{color:#005cc5;font-weight:700}.hljs-bullet{color:#735c0f}.hljs-emphasis{color:#24292e;font-style:italic}.hljs-strong{color:#24292e;font-weight:700}.hljs-addition{color:#22863a;background-color:#f0fff4}.hljs-deletion{color:#b31d28;background-color:#ffeef0}
|
||||
html[data-theme="dark"] pre code.hljs{display:block;overflow-x:auto;padding:1em}html[data-theme="dark"] code.hljs{padding:3px 5px}html[data-theme="dark"] /*!
|
||||
Theme: GitHub Dark
|
||||
Description: Dark theme as seen on github.com
|
||||
Author: github.com
|
||||
Maintainer: @Hirse
|
||||
Updated: 2021-05-15
|
||||
|
||||
Outdated base version: https://github.com/primer/github-syntax-dark
|
||||
Current colors taken from GitHub's CSS
|
||||
*/.hljs{color:#c9d1d9;background:#0d1117}html[data-theme="dark"] .hljs-doctag, html[data-theme="dark"] .hljs-keyword, html[data-theme="dark"] .hljs-meta .hljs-keyword, html[data-theme="dark"] .hljs-template-tag, html[data-theme="dark"] .hljs-template-variable, html[data-theme="dark"] .hljs-type, html[data-theme="dark"] .hljs-variable.language_{color:#ff7b72}html[data-theme="dark"] .hljs-title, html[data-theme="dark"] .hljs-title.class_, html[data-theme="dark"] .hljs-title.class_.inherited__, html[data-theme="dark"] .hljs-title.function_{color:#d2a8ff}html[data-theme="dark"] .hljs-attr, html[data-theme="dark"] .hljs-attribute, html[data-theme="dark"] .hljs-literal, html[data-theme="dark"] .hljs-meta, html[data-theme="dark"] .hljs-number, html[data-theme="dark"] .hljs-operator, html[data-theme="dark"] .hljs-selector-attr, html[data-theme="dark"] .hljs-selector-class, html[data-theme="dark"] .hljs-selector-id, html[data-theme="dark"] .hljs-variable{color:#79c0ff}html[data-theme="dark"] .hljs-meta .hljs-string, html[data-theme="dark"] .hljs-regexp, html[data-theme="dark"] .hljs-string{color:#a5d6ff}html[data-theme="dark"] .hljs-built_in, html[data-theme="dark"] .hljs-symbol{color:#ffa657}html[data-theme="dark"] .hljs-code, html[data-theme="dark"] .hljs-comment, html[data-theme="dark"] .hljs-formula{color:#8b949e}html[data-theme="dark"] .hljs-name, html[data-theme="dark"] .hljs-quote, html[data-theme="dark"] .hljs-selector-pseudo, html[data-theme="dark"] .hljs-selector-tag{color:#7ee787}html[data-theme="dark"] .hljs-subst{color:#c9d1d9}html[data-theme="dark"] .hljs-section{color:#1f6feb;font-weight:700}html[data-theme="dark"] .hljs-bullet{color:#f2cc60}html[data-theme="dark"] .hljs-emphasis{color:#c9d1d9;font-style:italic}html[data-theme="dark"] .hljs-strong{color:#c9d1d9;font-weight:700}html[data-theme="dark"] .hljs-addition{color:#aff5b4;background-color:#033a16}html[data-theme="dark"] .hljs-deletion{color:#ffdcd7;background-color:#67060c}
|
||||
/* FlowDeck tweaks: keep code blocks integrated with app surfaces */
|
||||
.hljs{background:transparent;}
|
||||
@@ -0,0 +1,180 @@
|
||||
/* FlowDeck — AI Meeting Notes, panneau façon Notion (v7.1.1).
|
||||
* Coquille fixe : en-tête / rangée onglets+contrôles / corps / pied (§3B.9).
|
||||
* Tout le contenu du corps est aligné sur la marge gauche des onglets ;
|
||||
* tout ce qui est « action globale » est aligné à droite. */
|
||||
|
||||
.fm-tabs { display: flex; gap: 2px; align-items: center; }
|
||||
.fm-tab {
|
||||
background: none; border: none; color: var(--text-dim); font-size: 12.5px;
|
||||
cursor: pointer; padding: 6px 12px; border-radius: 99px; font-weight: 500;
|
||||
display: inline-flex; align-items: center; gap: 6px; white-space: nowrap;
|
||||
}
|
||||
.fm-tab.active { background: var(--bg-tertiary); color: var(--text-primary); }
|
||||
.fm-mic {
|
||||
font-size: 10px; border: 1px solid var(--border); border-radius: 4px;
|
||||
padding: 0 3px; font-style: normal;
|
||||
}
|
||||
.fm-tab.active .fm-mic { border-color: var(--accent); color: var(--accent); }
|
||||
|
||||
.fm-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; flex-wrap: wrap; }
|
||||
.fm-controls { display: flex; align-items: center; gap: 6px; margin-left: auto; position: relative; }
|
||||
.fm-icon {
|
||||
background: none; border: none; color: var(--text-dim); cursor: pointer;
|
||||
font-size: 14px; padding: 5px 7px; border-radius: 6px; line-height: 1;
|
||||
}
|
||||
.fm-icon:hover { background: var(--bg-hover); color: var(--text-primary); }
|
||||
|
||||
/* Bouton de démarrage à menu (split button) */
|
||||
.fm-split { display: flex; position: relative; }
|
||||
.fm-start { border-radius: 8px 0 0 8px !important; }
|
||||
.fm-chevron { border-radius: 0 8px 8px 0 !important; border-left: 1px solid rgba(255,255,255,.35) !important; padding: 7px 9px !important; }
|
||||
.fm-menu {
|
||||
position: absolute; top: calc(100% + 6px); right: 0; z-index: 60;
|
||||
background: var(--bg-modal); border: 1px solid var(--border);
|
||||
border-radius: 10px; box-shadow: var(--shadow-modal);
|
||||
padding: 8px; min-width: 280px;
|
||||
}
|
||||
.fm-menu-label { font-size: 11px; color: var(--text-dim); text-transform: uppercase; letter-spacing: .4px; padding: 6px 8px 2px; }
|
||||
.fm-menu-item {
|
||||
display: block; width: 100%; text-align: left; background: none; border: none;
|
||||
color: var(--text-primary); font-size: 13px; padding: 7px 8px; border-radius: 6px; cursor: pointer;
|
||||
}
|
||||
.fm-menu-item:hover { background: var(--bg-hover); }
|
||||
.fm-menu-item.sel { color: var(--accent); font-weight: 600; }
|
||||
.fm-menu-row { display: flex; gap: 6px; }
|
||||
.fm-menu-row .fm-menu-item { width: auto; }
|
||||
.fm-menu-check { display: flex; gap: 8px; align-items: center; font-size: 12.5px; color: var(--text-secondary); padding: 8px; cursor: pointer; }
|
||||
.fm-setup {
|
||||
display: flex; flex-direction: column; gap: 3px;
|
||||
font-size: 12.5px; color: var(--text-secondary);
|
||||
padding: 2px 8px 6px;
|
||||
}
|
||||
/* Bande de vérification pré-enregistrement (toujours visible à l'état idle) */
|
||||
.fm-setupstrip {
|
||||
display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
|
||||
font-size: 12px; color: var(--text-secondary);
|
||||
border: 1px dashed var(--border); border-radius: 8px; padding: 6px 10px;
|
||||
}
|
||||
.fm-dim { color: var(--text-dim); }
|
||||
|
||||
/* Enregistrement : onde + chrono + pastilles */
|
||||
.fm-wave { height: 30px; width: 140px; }
|
||||
.fm-timer { font-family: monospace; font-size: 12px; color: var(--text-secondary); min-width: 44px; text-align: center; }
|
||||
.fm-btn-stop { color: #e5484d; }
|
||||
.fm-paused-badge {
|
||||
font-size: 11px; padding: 3px 10px; border-radius: 99px;
|
||||
background: #f5a62333; color: #f5a623; white-space: nowrap;
|
||||
}
|
||||
.fm-thinking {
|
||||
font-size: 11.5px; padding: 4px 12px; border-radius: 99px;
|
||||
background: var(--bg-tertiary); color: var(--text-primary); white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Panneau réglages */
|
||||
.fm-settings {
|
||||
border: 1px solid var(--border); border-radius: 8px; padding: 8px 10px;
|
||||
display: flex; flex-direction: column; gap: 8px; background: var(--bg-tertiary);
|
||||
}
|
||||
.fm-set-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; font-size: 12.5px; color: var(--text-secondary); }
|
||||
.fm-set-row select {
|
||||
background: var(--bg-tertiary); color: var(--text-primary);
|
||||
border: 1px solid var(--border); border-radius: 6px; padding: 4px 8px; font-size: 12.5px;
|
||||
}
|
||||
.fm-chip {
|
||||
background: var(--bg-tertiary); border: 1px solid var(--border);
|
||||
color: var(--text-secondary); border-radius: 99px; padding: 4px 12px;
|
||||
font-size: 12px; cursor: pointer;
|
||||
}
|
||||
.fm-chip.active { color: var(--text-primary); border-color: var(--accent); }
|
||||
|
||||
/* Corps */
|
||||
.fm-body { display: flex; flex-direction: column; gap: 8px; }
|
||||
.fm-promise { color: var(--text-dim); font-size: 13px; padding: 2px 0; }
|
||||
.fm-hint { color: var(--text-dim); font-size: 12px; }
|
||||
.fm-head { display: flex; align-items: center; gap: 6px; }
|
||||
.fm-cal { color: var(--text-dim); font-size: 16px; }
|
||||
.fm-chev { color: var(--text-dim); font-size: 11px; }
|
||||
.fm-rule { height: 1px; background: var(--border); margin: 0 -2px; }
|
||||
|
||||
/* Étapes Thinking */
|
||||
.fm-steps { display: flex; flex-direction: column; gap: 6px; padding: 4px 0; }
|
||||
.fm-step { display: flex; gap: 10px; align-items: baseline; font-size: 13px; color: var(--text-dim); }
|
||||
.fm-step.done { color: var(--text-secondary); }
|
||||
.fm-step.running { color: var(--text-primary); font-weight: 600; }
|
||||
.fm-step.failed { color: #e5484d; }
|
||||
.fm-step-ico { width: 18px; text-align: center; }
|
||||
|
||||
/* Barre de diffusion */
|
||||
.fm-sharebar {
|
||||
border: 1px solid var(--border); border-radius: 8px; padding: 8px 12px;
|
||||
display: flex; align-items: center; justify-content: space-between; gap: 8px; flex-wrap: wrap;
|
||||
font-size: 13px; font-weight: 600;
|
||||
}
|
||||
.fm-share-btns { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
|
||||
.fm-chipbtn { font-size: 12px; padding: 5px 10px; font-weight: 500; }
|
||||
|
||||
/* Résumé structuré */
|
||||
.fm-summary { display: flex; flex-direction: column; }
|
||||
.fm-sum-sec h4 { font-size: 13.5px; font-weight: 700; margin: 10px 0 4px; color: var(--text-primary); }
|
||||
.fm-bullet { display: flex; gap: 8px; padding: 3px 0; font-size: 13px; line-height: 1.5; }
|
||||
.fm-bullet > span { flex: 1; }
|
||||
.fm-cite {
|
||||
background: none; border: none; color: var(--accent); cursor: pointer;
|
||||
font-size: 11px; vertical-align: super; padding: 0 1px; font-weight: 700;
|
||||
}
|
||||
.fm-cite:hover { text-decoration: underline; }
|
||||
.fm-cite-static { color: var(--accent); font-size: 11px; vertical-align: super; }
|
||||
.fm-sum-note { color: var(--text-dim); font-size: 12px; font-style: italic; margin-top: 8px; }
|
||||
.fm-class {
|
||||
font-size: 12px; color: #f5a623; background: #f5a62314;
|
||||
border: 1px solid #f5a62344; border-radius: 8px; padding: 6px 10px;
|
||||
}
|
||||
|
||||
/* Transcript groupé par source + rail temporel */
|
||||
.fm-src { color: var(--text-dim); font-size: 12.5px; margin-top: 8px; }
|
||||
.fm-seg { display: flex; gap: 8px; padding: 2px 0; font-size: 13px; border-radius: 6px; }
|
||||
.fm-seg .fm-ts { color: var(--text-dim); font-family: monospace; font-size: 11px; flex-shrink: 0; width: 34px; padding-top: 3px; }
|
||||
.fm-rail { width: 2px; background: var(--border); border-radius: 2px; flex-shrink: 0; }
|
||||
.fm-seg-body { flex: 1; min-width: 0; }
|
||||
.fm-who {
|
||||
background: none; border: none; color: var(--accent); font-weight: 600;
|
||||
cursor: pointer; font-size: 12.5px; padding: 0 6px 0 0;
|
||||
}
|
||||
.fm-who:hover { text-decoration: underline; }
|
||||
.fm-flash { background: #2383e233; }
|
||||
|
||||
/* Pied de bloc + bandeau */
|
||||
.fm-foot {
|
||||
display: flex; align-items: center; gap: 10px; font-size: 12px;
|
||||
color: var(--text-dim); flex-wrap: wrap;
|
||||
}
|
||||
.fm-foot select {
|
||||
background: transparent; color: var(--text-primary);
|
||||
border: 1px solid transparent; border-radius: 6px; font-size: 12px; cursor: pointer;
|
||||
}
|
||||
.fm-foot select:hover { border-color: var(--border); }
|
||||
.fm-sep { opacity: .5; }
|
||||
.fm-consent { flex: 1; min-width: 180px; }
|
||||
.fm-foot-icons { display: flex; gap: 2px; margin-left: auto; }
|
||||
.fm-banner {
|
||||
display: flex; align-items: center; justify-content: space-between; gap: 8px;
|
||||
font-size: 12px; color: var(--text-secondary);
|
||||
border-top: 1px solid var(--border); padding-top: 8px;
|
||||
}
|
||||
.fm-warn {
|
||||
font-size: 12.5px; color: #f5a623; background: #f5a62314;
|
||||
border: 1px solid #f5a62344; border-radius: 8px; padding: 7px 10px;
|
||||
}
|
||||
.fm-manual { font-size: 12.5px; color: var(--text-secondary); }
|
||||
.fm-manual summary { cursor: pointer; color: var(--text-dim); padding: 4px 0; }
|
||||
.fm-manual-row { display: flex; gap: 8px; align-items: center; margin-top: 6px; flex-wrap: wrap; }
|
||||
|
||||
/* Hub Meetings : recherche */
|
||||
.sb-search-row { padding: 0 8px 6px; }
|
||||
.sb-search-input {
|
||||
width: 100%; box-sizing: border-box; background: var(--bg-tertiary);
|
||||
border: 1px solid var(--border); border-radius: 6px; color: var(--text-primary);
|
||||
font-size: 12px; padding: 5px 9px; outline: none;
|
||||
}
|
||||
.sb-search-input:focus { border-color: var(--accent); }
|
||||
.sb-search-input::-webkit-search-cancel-button { cursor: pointer; }
|
||||
@@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
}
|
||||
|
||||
/** 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,
|
||||
};
|
||||
})();
|
||||
@@ -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',
|
||||
|
||||
Vendored
+776
File diff suppressed because one or more lines are too long
Vendored
+1213
File diff suppressed because one or more lines are too long
+67
-9
@@ -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
@@ -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,'&').replace(/</g,'<').replace(/>/g,'>');
|
||||
},
|
||||
|
||||
// ── 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
@@ -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;}
|
||||
|
||||
+4
-2
@@ -20,7 +20,7 @@ const PRECACHE_URLS = [
|
||||
'/static/css/design-tokens.css?v=5.2.0',
|
||||
'/static/css/components.css?v=5.2.0',
|
||||
'/static/css/katex.min.css?v=0.16.11',
|
||||
'/static/css/prism.css',
|
||||
'/static/css/highlight.css',
|
||||
// JS
|
||||
'/static/js/htmx.min.js',
|
||||
'/static/js/alpine.csp.min.js',
|
||||
@@ -28,7 +28,9 @@ const PRECACHE_URLS = [
|
||||
'/static/js/app.js?v=2.4.8',
|
||||
'/static/js/offline.js?v=6.0.0',
|
||||
'/static/js/katex.min.js?v=0.16.11',
|
||||
'/static/js/prism.min.js',
|
||||
'/static/js/highlight.min.js',
|
||||
'/static/js/highlight-extra.min.js',
|
||||
'/static/js/code-highlight.js',
|
||||
// Icônes & métadonnées
|
||||
'/static/favicon.ico',
|
||||
'/static/manifest.json',
|
||||
|
||||
Reference in New Issue
Block a user