feat(outils): page Outils V1 + composants TreeASCII/OutilCard/SimulateurFeature + nav premier onglet

This commit is contained in:
Jules Neny
2026-05-22 10:58:39 +02:00
parent f5732bf336
commit 422f45116f
9 changed files with 1255 additions and 0 deletions

201
components/TreeASCII.vue Normal file
View File

@@ -0,0 +1,201 @@
<template>
<ul class="tree-ascii" :class="{ 'tree-ascii--root': depth === 0 }" :style="{ '--depth': depth }">
<li
v-for="(node, i) in tree.children"
:key="i"
class="tree-ascii__node"
>
<!-- Nœud avec enfants : bouton toggle -->
<template v-if="node.children && node.children.length">
<button
class="tree-ascii__branch"
:aria-expanded="!!open[i]"
@click="toggle(i)"
>
<span class="tree-ascii__chevron" aria-hidden="true">{{ open[i] ? '▼' : '▶' }}</span>
<span class="tree-ascii__label">{{ node.name }}</span>
<span class="tree-ascii__count">({{ node.children.length }})</span>
</button>
<TreeASCII
v-if="open[i]"
:tree="node"
:depth="depth + 1"
/>
</template>
<!-- Feuille avec URL : lien cliquable -->
<template v-else-if="node.url">
<a
:href="node.url"
target="_blank"
rel="noopener noreferrer"
class="tree-ascii__leaf tree-ascii__leaf--link"
>
<span class="tree-ascii__prefix" aria-hidden="true"></span>
<span class="tree-ascii__label">{{ node.name }}</span>
<span v-if="node.desc" class="tree-ascii__desc"> {{ node.desc }}</span>
<svg class="tree-ascii__ext" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
<polyline points="15 3 21 3 21 9"/>
<line x1="10" y1="14" x2="21" y2="3"/>
</svg>
</a>
</template>
<!-- Feuille sans URL -->
<template v-else>
<span class="tree-ascii__leaf">
<span class="tree-ascii__prefix" aria-hidden="true"></span>
<span class="tree-ascii__label">{{ node.name }}</span>
<span v-if="node.desc" class="tree-ascii__desc"> {{ node.desc }}</span>
</span>
</template>
</li>
</ul>
</template>
<script setup lang="ts">
export interface TreeNode {
name: string
url?: string
desc?: string
children?: TreeNode[]
}
export interface TreeData {
name?: string
children?: TreeNode[]
}
const props = withDefaults(defineProps<{
tree: TreeData
depth?: number
}>(), {
depth: 0
})
// Toutes les branches fermées par défaut
const open = ref<Record<number, boolean>>({})
function toggle(i: number) {
open.value[i] = !open.value[i]
}
</script>
<style scoped>
.tree-ascii {
list-style: none;
padding: 0;
margin: 0;
font-family: 'JetBrains Mono', 'Fira Code', 'Courier New', monospace;
font-size: 0.82rem;
padding-left: calc(var(--depth, 0) * 1.25rem + 0.5rem);
}
.tree-ascii--root {
padding-left: 0;
}
.tree-ascii__node {
margin: 2px 0;
line-height: 1.6;
}
/* Bouton branche (nœud avec enfants) */
.tree-ascii__branch {
display: inline-flex;
align-items: center;
gap: 0.35rem;
background: none;
border: none;
cursor: pointer;
padding: 2px 6px;
border-radius: 4px;
color: var(--nav-text);
font-family: inherit;
font-size: inherit;
font-weight: 600;
transition: background 0.1s, color 0.1s;
text-align: left;
}
.tree-ascii__branch:hover {
background: var(--nav-bg-alt);
color: var(--nav-primary-solid);
}
.tree-ascii__chevron {
font-size: 0.65rem;
color: var(--nav-text-muted);
width: 12px;
text-align: center;
flex-shrink: 0;
}
.tree-ascii__count {
font-size: 0.7rem;
color: var(--nav-text-muted);
font-weight: 400;
}
/* Feuille */
.tree-ascii__leaf {
display: inline-flex;
align-items: baseline;
gap: 0.25rem;
padding: 1px 6px;
border-radius: 4px;
text-decoration: none;
color: var(--nav-text-muted);
}
.tree-ascii__leaf--link {
color: var(--nav-text);
cursor: pointer;
transition: background 0.1s, color 0.1s;
}
.tree-ascii__leaf--link:hover {
background: var(--nav-bg-alt);
color: var(--nav-primary-solid);
text-decoration: underline;
}
.tree-ascii__prefix {
color: var(--nav-text-muted);
opacity: 0.5;
font-size: 0.75rem;
flex-shrink: 0;
}
.tree-ascii__label {
font-weight: 500;
}
.tree-ascii__leaf--link .tree-ascii__label {
font-weight: 400;
}
.tree-ascii__desc {
color: var(--nav-text-muted);
font-size: 0.78rem;
font-style: italic;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 60ch;
}
.tree-ascii__ext {
opacity: 0.4;
flex-shrink: 0;
margin-left: 2px;
vertical-align: middle;
}
@media (max-width: 640px) {
.tree-ascii__desc {
display: none;
}
}
</style>