Extraction des 7 templates dont le JS n'est PAS interpolé Jinja → 9 fichiers
static/js/*.js (4 243 lignes, -30 % du JS inline : 13 904 → 9 661) :
- agent_panel_1/_2 (bloc de 1 788 L livré sur CHAQUE page), library (1 039),
gitea_workspace (626), _icon_picker_1/_2, _ctx_menu, import, workspaces
- UN fichier par bloc : ordre/timing identiques (pas de defer, attributs
conservés dont data-cfasync), cache-busting via ?v={{ asset_version }}
(source unique A40), scripts externes = 'self' en CSP (pas de nonce requis)
- garde-fou : le script refuse tout bloc contenant {{ ou {%
- vérifs : node --check vert sur les 9, 0 script inline restant dans les
cibles, suite complète 1089/1089
Lint (la moitié « ajouter les templates à eslint » de l'audit) :
- eslint.config.mjs existait (flat v9, sans dépendances npm) mais AUCUN
binaire eslint n'était installé → npm i -g eslint
- `eslint static/js` → 0 erreur, 120 warnings (no-unused-vars 69,
no-empty 36, no-undef 15) sur 8 fichiers = baseline à nettoyer
- les extraits sont couverts d'office par la config (static/js/**/*.js)
Reste A27 : blocs interpolés Jinja (page_editor 2 517, local_workspace 2 031,
base 1 523, database_table 1 323, settings 1 093, realtime 531 ≈ 9 661 L)
→ extraction en 2 temps (config JSON injectée + script statique).
suite **1089/1089** · ruff OK · docs à jour
171 lines
16 KiB
HTML
171 lines
16 KiB
HTML
{###############################################################################
|
||
_icon_picker.html — Sélecteur d'icône/emoji façon Notion (v5.6.0)
|
||
═══════════════════════════════════════════════════════════════════════════
|
||
Popover global (inclus par base.html) utilisé par :
|
||
• le menu contextuel (library / local-workspace) via fdCtx.setIcon
|
||
• l'éditeur de page (clic sur l'icône du titre)
|
||
Expose le store Alpine `fdIconPicker` + `window.FDIconPicker`.
|
||
Onglets Emoji / Icons / Upload, recherche, aléatoire, teintes de peau,
|
||
récents (localStorage) et emojis personnalisés du workspace (API).
|
||
############################################################################}
|
||
{% set picker_icons = ['folder','file','calendar','clock','star','bot','users','globe','lock','book','check-square','trash','help-circle','settings','refresh','log-out','message-square','home','search','link','plus','bell','image','download','list','bar-chart','grid','align-left','corner-down-right','copy','key','inbox','edit','eye','share','x','paperclip','external-link','sparkles','lightbulb','tag','file-text','save','upload','trending-up','zap','alert-triangle','user'] %}
|
||
|
||
<script data-cfasync="false" nonce="{{ csp_nonce() }}" src="/static/js/_icon_picker_1.js?v={{ asset_version }}"></script>
|
||
|
||
<script data-cfasync="false" nonce="{{ csp_nonce() }}" src="/static/js/_icon_picker_2.js?v={{ asset_version }}"></script>
|
||
|
||
<style>
|
||
.fd-icon-picker{position:fixed;top:0;left:0;width:344px;background:var(--bg-modal);border:1px solid var(--border);border-radius:10px;box-shadow:0 8px 32px rgba(0,0,0,.28);z-index:2200;display:flex;flex-direction:column;max-height:440px;overflow:hidden;padding:0;}
|
||
.fd-icon-picker .fdip-tabs{display:flex;align-items:center;gap:2px;padding:8px 10px 6px;border-bottom:1px solid var(--border);}
|
||
.fd-icon-picker .fdip-tab{background:transparent;border:none;color:var(--text-secondary);font-size:13px;padding:5px 10px;border-radius:6px;cursor:pointer;}
|
||
.fd-icon-picker .fdip-tab:hover{background:var(--bg-hover);}
|
||
.fd-icon-picker .fdip-tab.active{background:var(--bg-hover);color:var(--text-primary);font-weight:600;}
|
||
.fd-icon-picker .fdip-remove{margin-left:auto;background:transparent;border:none;color:var(--text-secondary);font-size:12px;padding:5px 8px;border-radius:6px;cursor:pointer;}
|
||
.fd-icon-picker .fdip-remove:hover{background:var(--bg-hover);color:var(--danger);}
|
||
.fd-icon-picker .fdip-tools{display:flex;align-items:center;gap:6px;padding:8px 10px;}
|
||
.fd-icon-picker .fdip-search{flex:1;min-width:0;font-size:13px;padding:6px 10px;background:var(--bg-primary);border:1px solid var(--border);border-radius:6px;color:var(--text-primary);outline:none;}
|
||
.fd-icon-picker .fdip-search:focus{border-color:var(--accent);}
|
||
.fd-icon-picker .fdip-tool{width:30px;height:30px;flex-shrink:0;display:flex;align-items:center;justify-content:center;background:transparent;border:1px solid var(--border);border-radius:6px;cursor:pointer;font-size:15px;color:var(--text-primary);}
|
||
.fd-icon-picker .fdip-tool:hover{background:var(--bg-hover);}
|
||
.fd-icon-picker .fdip-tone-wrap{position:relative;}
|
||
.fd-icon-picker .fdip-tone-menu{position:absolute;right:0;top:34px;display:flex;gap:2px;padding:6px;background:var(--bg-modal);border:1px solid var(--border);border-radius:8px;box-shadow:0 6px 20px rgba(0,0,0,.25);z-index:5;}
|
||
.fd-icon-picker .fdip-tone{width:26px;height:26px;border:none;background:transparent;border-radius:6px;cursor:pointer;font-size:16px;}
|
||
.fd-icon-picker .fdip-tone:hover,.fd-icon-picker .fdip-tone.selected{background:var(--bg-hover);}
|
||
.fd-icon-picker .fdip-body{flex:1;overflow-y:auto;padding:0 10px 6px;}
|
||
.fd-icon-picker .fdip-section{font-size:11px;text-transform:uppercase;letter-spacing:.04em;color:var(--text-dim);padding:6px 2px 2px;}
|
||
.fd-icon-picker .fdip-grid{display:grid;grid-template-columns:repeat(8,1fr);gap:2px;}
|
||
.fd-icon-picker .fdip-emoji,.fd-icon-picker .fdip-icon{width:100%;aspect-ratio:1;display:flex;align-items:center;justify-content:center;background:transparent;border:none;border-radius:6px;cursor:pointer;font-size:21px;line-height:1;color:var(--text-primary);position:relative;}
|
||
.fd-icon-picker .fdip-emoji:hover,.fd-icon-picker .fdip-icon:hover{background:var(--bg-hover);}
|
||
.fd-icon-picker .fdip-custom img{width:24px;height:24px;object-fit:contain;}
|
||
.fd-icon-picker .fdip-del{position:absolute;top:-2px;right:-2px;width:14px;height:14px;border-radius:50%;background:var(--danger);color:#fff;font-size:10px;line-height:14px;text-align:center;display:none;}
|
||
.fd-icon-picker .fdip-custom:hover .fdip-del{display:block;}
|
||
.fd-icon-picker .fdip-empty{color:var(--text-dim);font-size:12px;text-align:center;padding:24px 0;}
|
||
.fd-icon-picker .fdip-cats{display:flex;align-items:center;gap:2px;padding:6px 10px;border-top:1px solid var(--border);}
|
||
.fd-icon-picker .fdip-cat{flex:1;height:30px;display:flex;align-items:center;justify-content:center;background:transparent;border:none;border-radius:6px;cursor:pointer;color:var(--text-secondary);}
|
||
.fd-icon-picker .fdip-cat:hover{background:var(--bg-hover);}
|
||
.fd-icon-picker .fdip-cat.active{background:var(--bg-hover);color:var(--accent);}
|
||
.fd-icon-picker .fdip-cat svg{width:16px;height:16px;}
|
||
.fdip-modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:2300;display:flex;align-items:center;justify-content:center;}
|
||
.fdip-modal{width:420px;max-width:92vw;background:var(--bg-modal);border:1px solid var(--border);border-radius:12px;padding:20px;box-shadow:0 16px 48px rgba(0,0,0,.35);}
|
||
.fdip-modal-title{font-size:16px;font-weight:600;color:var(--text-primary);margin-bottom:6px;}
|
||
.fdip-modal-desc{font-size:13px;color:var(--text-secondary);margin-bottom:16px;}
|
||
.fdip-upload-btn{display:inline-block;padding:8px 14px;font-size:13px;border:1px solid var(--border);border-radius:8px;cursor:pointer;color:var(--text-primary);background:var(--bg-secondary);}
|
||
.fdip-upload-btn:hover{background:var(--bg-hover);}
|
||
.fdip-preview-boxes{display:flex;gap:10px;margin:14px 0;}
|
||
.fdip-preview{width:64px;height:64px;border-radius:8px;display:flex;align-items:center;justify-content:center;border:1px solid var(--border);}
|
||
.fdip-preview.dark{background:#111;}
|
||
.fdip-preview.light{background:#fff;}
|
||
.fdip-preview img{max-width:44px;max-height:44px;object-fit:contain;}
|
||
.fdip-modal-label{display:block;font-size:12px;color:var(--text-secondary);margin:8px 0 4px;}
|
||
.fdip-modal-input{width:100%;font-size:14px;padding:8px 10px;background:var(--bg-primary);border:1px solid var(--border);border-radius:8px;color:var(--text-primary);outline:none;box-sizing:border-box;}
|
||
.fdip-modal-input:focus{border-color:var(--accent);}
|
||
.fdip-modal-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:18px;}
|
||
.fdip-modal-actions button{padding:8px 16px;font-size:13px;border-radius:8px;cursor:pointer;border:1px solid var(--border);background:var(--bg-secondary);color:var(--text-primary);}
|
||
.fdip-modal-actions button.primary{background:var(--accent);border-color:var(--accent);color:#fff;}
|
||
.fdip-modal-actions button:disabled{opacity:.5;cursor:default;}
|
||
</style>
|
||
|
||
<div x-data>
|
||
<div class="fd-icon-picker" x-show="$store.fdIconPicker.open" x-cloak
|
||
@click.outside="$store.fdIconPicker.customModal || $store.fdIconPicker.close()"
|
||
@keydown.escape.window="$store.fdIconPicker.close()"
|
||
:style="'top:' + $store.fdIconPicker.y + 'px;left:' + $store.fdIconPicker.x + 'px'">
|
||
<div class="fdip-tabs">
|
||
<button class="fdip-tab" :class="{ active: $store.fdIconPicker.tab === 'emoji' }" @click="$store.fdIconPicker.setTab('emoji')">Emoji</button>
|
||
<button class="fdip-tab" :class="{ active: $store.fdIconPicker.tab === 'icons' }" @click="$store.fdIconPicker.setTab('icons')">Icons</button>
|
||
<button class="fdip-tab" :class="{ active: $store.fdIconPicker.tab === 'upload' }" @click="$store.fdIconPicker.setTab('upload')">Upload</button>
|
||
<button class="fdip-remove" @click="$store.fdIconPicker.remove()">Remove</button>
|
||
</div>
|
||
|
||
<div class="fdip-tools" x-show="$store.fdIconPicker.tab !== 'upload'">
|
||
<input class="fdip-search" placeholder="Filter..." x-model="$store.fdIconPicker.query">
|
||
<button class="fdip-tool" title="Random" @click="$store.fdIconPicker.random()">🎲</button>
|
||
<div class="fdip-tone-wrap" x-show="$store.fdIconPicker.tab === 'emoji'">
|
||
<button class="fdip-tool" title="Select skin tone" @click.stop="$store.fdIconPicker.toneOpen = !$store.fdIconPicker.toneOpen">👋</button>
|
||
<div class="fdip-tone-menu" x-show="$store.fdIconPicker.toneOpen" @click.outside="$store.fdIconPicker.toneOpen = false">
|
||
<template x-for="(t, ti) in ['👋','👋🏻','👋🏼','👋🏽','👋🏾','👋🏿']" :key="ti">
|
||
<button class="fdip-tone" :class="{ selected: $store.fdIconPicker.skinTone === ti }" @click="$store.fdIconPicker.setTone(ti)" x-text="t"></button>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="fdip-body">
|
||
<template x-if="$store.fdIconPicker.tab === 'emoji'">
|
||
<div>
|
||
<div class="fdip-section" x-show="$store.fdIconPicker.category === 'recent' && !$store.fdIconPicker.query">Recent</div>
|
||
<div class="fdip-grid">
|
||
<template x-for="e in $store.fdIconPicker.items()" :key="e">
|
||
<button class="fdip-emoji" @click="$store.fdIconPicker.pick($store.fdIconPicker.withTone(e))" x-text="$store.fdIconPicker.withTone(e)"></button>
|
||
</template>
|
||
</div>
|
||
<div class="fdip-empty" x-show="!$store.fdIconPicker.items().length">No emoji found</div>
|
||
</div>
|
||
</template>
|
||
|
||
<template x-if="$store.fdIconPicker.tab === 'icons'">
|
||
<div>
|
||
<div class="fdip-grid">
|
||
{% for name in picker_icons %}
|
||
<button class="fdip-icon" x-show="$store.fdIconPicker.matches('{{ name }}')" @click="$store.fdIconPicker.pick('{{ name }}')" title="{{ name }}">{{ fd_icon(name, 20) }}</button>
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<template x-if="$store.fdIconPicker.tab === 'upload'">
|
||
<div>
|
||
<div class="fdip-custom-head" style="display:flex;align-items:center;justify-content:space-between;padding:6px 2px;">
|
||
<span class="fdip-section" style="padding:0;">Custom emojis</span>
|
||
<button class="fdip-tool" style="width:auto;padding:0 10px;" @click="$store.fdIconPicker.openCustomModal()">+ Add</button>
|
||
</div>
|
||
<div class="fdip-grid">
|
||
<template x-for="ce in $store.fdIconPicker.custom" :key="ce.id">
|
||
<button class="fdip-emoji fdip-custom" @click="$store.fdIconPicker.pick(ce.url)" :title="ce.name">
|
||
<img :src="ce.url" :alt="ce.name">
|
||
<span class="fdip-del" @click.stop="$store.fdIconPicker.deleteCustom(ce.id)">×</span>
|
||
</button>
|
||
</template>
|
||
</div>
|
||
<div class="fdip-empty" x-show="!$store.fdIconPicker.custom.length">No custom emoji yet</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
|
||
<div class="fdip-cats" x-show="$store.fdIconPicker.tab === 'emoji'">
|
||
<button class="fdip-cat" :class="{ active: $store.fdIconPicker.category === 'recent' }" title="Recent" @click="$store.fdIconPicker.setCategory('recent')"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><polyline points="12 7 12 12 15 14"/></svg></button>
|
||
<button class="fdip-cat" :class="{ active: $store.fdIconPicker.category === 'people' }" title="People" @click="$store.fdIconPicker.setCategory('people')"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><path d="M8.5 14s1.3 2 3.5 2 3.5-2 3.5-2"/><line x1="9" y1="9.5" x2="9.01" y2="9.5"/><line x1="15" y1="9.5" x2="15.01" y2="9.5"/></svg></button>
|
||
<button class="fdip-cat" :class="{ active: $store.fdIconPicker.category === 'nature' }" title="Nature" @click="$store.fdIconPicker.setCategory('nature')"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 20A7 7 0 0 1 4 13c0-6 7-10 16-10 0 9-4 16-10 16z"/><path d="M4 21c2-4 5-7 9-9"/></svg></button>
|
||
<button class="fdip-cat" :class="{ active: $store.fdIconPicker.category === 'food' }" title="Food" @click="$store.fdIconPicker.setCategory('food')"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 21l6-6"/><path d="M14 3s5 1 7 7c-4 4-8 3-10 1s-1-6 3-8z"/><path d="M9 12l-4 4"/></svg></button>
|
||
<button class="fdip-cat" :class="{ active: $store.fdIconPicker.category === 'activity' }" title="Activity" @click="$store.fdIconPicker.setCategory('activity')"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 11V6a2 2 0 0 1 4 0v5"/><path d="M10 11V4a2 2 0 0 1 4 0v7"/><path d="M14 11V6a2 2 0 0 1 4 0v9a7 7 0 0 1-7 7H9a6 6 0 0 1-6-6v-3a2 2 0 0 1 3-1.7"/></svg></button>
|
||
<button class="fdip-cat" :class="{ active: $store.fdIconPicker.category === 'travel' }" title="Travel" @click="$store.fdIconPicker.setCategory('travel')"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M2 16l20-6-3-3-7 1-5-5-2 1 3 5-4 1-2-2-1 1z"/></svg></button>
|
||
<button class="fdip-cat" :class="{ active: $store.fdIconPicker.category === 'objects' }" title="Objects" @click="$store.fdIconPicker.setCategory('objects')"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18h6"/><path d="M10 22h4"/><path d="M12 2a6 6 0 0 0-4 10.5c.8.7 1 1.5 1 2.5h6c0-1 .2-1.8 1-2.5A6 6 0 0 0 12 2z"/></svg></button>
|
||
<button class="fdip-cat" :class="{ active: $store.fdIconPicker.category === 'symbols' }" title="Symbols" @click="$store.fdIconPicker.setCategory('symbols')"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 11 12 14 22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg></button>
|
||
<button class="fdip-cat" :class="{ active: $store.fdIconPicker.category === 'flags' }" title="Flags" @click="$store.fdIconPicker.setCategory('flags')"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 22V4"/><path d="M4 4h13l-2 4 2 4H4"/></svg></button>
|
||
<button class="fdip-cat" title="Add custom emoji" @click="$store.fdIconPicker.openCustomModal()"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg></button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="fdip-modal-overlay" x-show="$store.fdIconPicker.customModal" x-cloak @click.self="$store.fdIconPicker.closeCustomModal()">
|
||
<div class="fdip-modal">
|
||
<div class="fdip-modal-title">Add custom emoji</div>
|
||
<div class="fdip-modal-desc">Custom emoji can be used by anyone in your workspace.</div>
|
||
<label class="fdip-upload-btn">
|
||
<span x-show="!$store.fdIconPicker.customPreview">Upload an image</span>
|
||
<span x-show="$store.fdIconPicker.customPreview">Replace</span>
|
||
<input type="file" accept="image/*" @change="$store.fdIconPicker.onCustomFile($event)" style="display:none">
|
||
</label>
|
||
<div class="fdip-preview-boxes" x-show="$store.fdIconPicker.customPreview">
|
||
<div class="fdip-preview dark"><img :src="$store.fdIconPicker.customPreview" alt=""></div>
|
||
<div class="fdip-preview light"><img :src="$store.fdIconPicker.customPreview" alt=""></div>
|
||
</div>
|
||
<label class="fdip-modal-label">Emoji name</label>
|
||
<input class="fdip-modal-input" x-model="$store.fdIconPicker.customName" placeholder="pushup">
|
||
<div class="fdip-modal-actions">
|
||
<button @click="$store.fdIconPicker.closeCustomModal()">Cancel</button>
|
||
<button class="primary" :disabled="$store.fdIconPicker.customBusy || !$store.fdIconPicker.customFile" @click="$store.fdIconPicker.saveCustom()">Save</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|