web: start: multiple pages
Signed-off-by: GnomeZworc <nicolas.boufidjeline@g3e.fr>
This commit is contained in:
parent
7946129a75
commit
b479493ac1
18 changed files with 309 additions and 52 deletions
95
web/build.sh
95
web/build.sh
|
|
@ -109,8 +109,12 @@ download_path() {
|
||||||
|
|
||||||
# ── Read manifest ────────────────────────────────────────────────────────────────
|
# ── Read manifest ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
mapfile -t LOCALS < <(yq '(.local // [])[]' "$MANIFEST")
|
PAGES_DIR="${WEB_DIR}/pages"
|
||||||
|
|
||||||
|
mapfile -t LOCALS < <(yq '(.local_components // [])[]' "$MANIFEST")
|
||||||
mapfile -t REMOTES < <(yq '(.components // [])[] | [.name, .repo, (.ref // "main")] | join("|")' "$MANIFEST")
|
mapfile -t REMOTES < <(yq '(.components // [])[] | [.name, .repo, (.ref // "main")] | join("|")' "$MANIFEST")
|
||||||
|
mapfile -t LOCAL_PAGES < <(yq '(.local_pages // [])[]' "$MANIFEST")
|
||||||
|
mapfile -t REMOTE_PAGES < <(yq '(.remote_pages // [])[] | [.name, .repo, (.ref // "main")] | join("|")' "$MANIFEST")
|
||||||
|
|
||||||
# Ordered list of load paths for components.json
|
# Ordered list of load paths for components.json
|
||||||
declare -a LOAD_PATHS=()
|
declare -a LOAD_PATHS=()
|
||||||
|
|
@ -175,3 +179,92 @@ echo "$LOCK_ENTRIES" | jq '.' > "$LOCKFILE"
|
||||||
|
|
||||||
log "wrote ${OUTPUT} (${#LOAD_PATHS[@]} components)"
|
log "wrote ${OUTPUT} (${#LOAD_PATHS[@]} components)"
|
||||||
log "wrote ${LOCKFILE} ($(jq 'length' <<<"$LOCK_ENTRIES") remote pins)"
|
log "wrote ${LOCKFILE} ($(jq 'length' <<<"$LOCK_ENTRIES") remote pins)"
|
||||||
|
|
||||||
|
# ── Process pages ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
PAGE_ENTRIES="[]"
|
||||||
|
|
||||||
|
# Collect page metadata from a directory into PAGE_ENTRIES.
|
||||||
|
# Detects css/js presence automatically — both are optional.
|
||||||
|
add_page_entry() {
|
||||||
|
local name="$1" dir="$2"
|
||||||
|
local manifest="${dir}/manifest.json"
|
||||||
|
local html="${dir}/index.html"
|
||||||
|
|
||||||
|
[[ -f "$manifest" ]] || die "page '${name}' missing manifest.json in ${dir}"
|
||||||
|
[[ -f "$html" ]] || die "page '${name}' missing index.html in ${dir}"
|
||||||
|
|
||||||
|
local route label icon css_arg js_arg
|
||||||
|
route=$(jq -r '.route' "$manifest")
|
||||||
|
label=$(jq -r '.label' "$manifest")
|
||||||
|
icon=$(jq -r '.icon' "$manifest")
|
||||||
|
css_arg="null"; js_arg="null"
|
||||||
|
[[ -f "${dir}/${name}.css" ]] && css_arg="\"pages/${name}/${name}.css\""
|
||||||
|
[[ -f "${dir}/${name}.js" ]] && js_arg="\"pages/${name}/${name}.js\""
|
||||||
|
|
||||||
|
PAGE_ENTRIES="$(jq \
|
||||||
|
--arg route "$route" --arg label "$label" --arg icon "$icon" \
|
||||||
|
--arg html "pages/${name}/index.html" \
|
||||||
|
--argjson css "$css_arg" --argjson js "$js_arg" \
|
||||||
|
'. + [{route:$route, label:$label, icon:$icon, html:$html, css:$css, js:$js}]' \
|
||||||
|
<<<"$PAGE_ENTRIES")"
|
||||||
|
info " page '${name}' → /#/${route}$([ "$css_arg" != "null" ] && echo " +css")$([ "$js_arg" != "null" ] && echo " +js")"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Local pages
|
||||||
|
for name in "${LOCAL_PAGES[@]}"; do
|
||||||
|
[[ -z "$name" ]] && continue
|
||||||
|
add_page_entry "$name" "${PAGES_DIR}/${name}"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Remote pages — download then register
|
||||||
|
for spec in "${REMOTE_PAGES[@]}"; do
|
||||||
|
[[ -z "$spec" ]] && continue
|
||||||
|
IFS='|' read -r name repo ref <<<"$spec"
|
||||||
|
[[ -z "$name" || -z "$repo" ]] && die "remote_pages entry missing name or repo"
|
||||||
|
|
||||||
|
read -r server owner gitrepo <<<"$(parse_repo_url "$repo")"
|
||||||
|
dest="${PAGES_DIR}/${name}"
|
||||||
|
|
||||||
|
if [[ "$CHECK_ONLY" == true ]]; then
|
||||||
|
info "would fetch page ${name} from ${repo}@${ref}"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "fetching page ${name} ← ${owner}/${gitrepo}@${ref}"
|
||||||
|
commit="$(resolve_commit "$server" "$owner" "$gitrepo" "$ref")"
|
||||||
|
[[ -z "$commit" ]] && die "cannot resolve ref '${ref}' in ${owner}/${gitrepo}"
|
||||||
|
|
||||||
|
rm -rf "$dest"
|
||||||
|
download_path "$server" "$owner" "$gitrepo" "$ref" "" "$dest"
|
||||||
|
add_page_entry "$name" "$dest"
|
||||||
|
info " pinned ${commit:0:12}"
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "$CHECK_ONLY" == false ]]; then
|
||||||
|
# Apply menu order if defined — reorder PAGE_ENTRIES and filter nav visibility
|
||||||
|
mapfile -t MENU_ORDER < <(yq '(.menu // [])[]' "$MANIFEST")
|
||||||
|
|
||||||
|
if [[ ${#MENU_ORDER[@]} -gt 0 ]]; then
|
||||||
|
ORDERED="[]"
|
||||||
|
for route in "${MENU_ORDER[@]}"; do
|
||||||
|
[[ -z "$route" ]] && continue
|
||||||
|
entry="$(echo "$PAGE_ENTRIES" | jq --arg r "$route" '.[] | select(.route == $r)')"
|
||||||
|
[[ -z "$entry" ]] && warn "menu: route '${route}' not found in pages, skipping"
|
||||||
|
[[ -n "$entry" ]] && ORDERED="$(echo "$ORDERED" | jq --argjson e "$entry" '. + [$e]')"
|
||||||
|
done
|
||||||
|
NAV_ENTRIES="$ORDERED"
|
||||||
|
else
|
||||||
|
NAV_ENTRIES="$PAGE_ENTRIES"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# pages.json — full list (all pages, original discovery order)
|
||||||
|
echo "$PAGE_ENTRIES" | jq '.' > "${WEB_DIR}/pages.json"
|
||||||
|
|
||||||
|
# navigation.json — ordered + filtered by menu:
|
||||||
|
echo "$NAV_ENTRIES" | \
|
||||||
|
jq '[.[] | {label: .label, href: ("#/" + .route), icon: .icon}]' \
|
||||||
|
> "${WEB_DIR}/navigation.json"
|
||||||
|
|
||||||
|
log "wrote pages.json ($(echo "$PAGE_ENTRIES" | jq 'length') pages) + navigation.json ($(echo "$NAV_ENTRIES" | jq 'length') in menu)"
|
||||||
|
fi
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
"components/login-gate/login-gate.js",
|
"components/login-gate/login-gate.js",
|
||||||
"components/api-client/api-client.js",
|
"components/api-client/api-client.js",
|
||||||
"components/side-nav/side-nav.js",
|
"components/side-nav/side-nav.js",
|
||||||
"components/side-panel/side-panel.js",
|
|
||||||
"components/logout-button/logout-button.js",
|
"components/logout-button/logout-button.js",
|
||||||
|
"components/side-panel/side-panel.js",
|
||||||
"components/date-display/date-display.js"
|
"components/date-display/date-display.js"
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -9,13 +9,13 @@
|
||||||
|
|
||||||
# Local components shipped inside this repo. Not downloaded — listed here only
|
# Local components shipped inside this repo. Not downloaded — listed here only
|
||||||
# so build.sh can place them in the generated load order.
|
# so build.sh can place them in the generated load order.
|
||||||
local:
|
local_components:
|
||||||
- login-gate
|
- login-gate
|
||||||
- api-client
|
- api-client
|
||||||
- side-nav
|
- side-nav
|
||||||
- logout-button
|
- logout-button
|
||||||
- date-display
|
|
||||||
- side-panel
|
- side-panel
|
||||||
|
- date-display
|
||||||
|
|
||||||
# Remote components fetched from git at build time.
|
# Remote components fetched from git at build time.
|
||||||
# Each is its own repo; its root must contain <name>.js and manifest.json.
|
# Each is its own repo; its root must contain <name>.js and manifest.json.
|
||||||
|
|
@ -24,3 +24,34 @@ local:
|
||||||
# repo: https://git.g3e.fr/team-reseau/vpc-panel
|
# repo: https://git.g3e.fr/team-reseau/vpc-panel
|
||||||
# ref: v1.2.0 # tag, branch or commit (default: main)
|
# ref: v1.2.0 # tag, branch or commit (default: main)
|
||||||
components: []
|
components: []
|
||||||
|
|
||||||
|
# Pages — each page is a self-contained directory (JS + CSS + manifest.json).
|
||||||
|
# Same model as components: local pages ship in this repo, remote pages are
|
||||||
|
# downloaded from git at build time. build.sh reads each manifest.json to
|
||||||
|
# generate navigation.json and pages.json.
|
||||||
|
#
|
||||||
|
# Page manifest.json must contain: tag, route, label, icon.
|
||||||
|
# Page JS must: customElements.define('<tag>', ...) and use display:contents.
|
||||||
|
|
||||||
|
local_pages:
|
||||||
|
- dashboard
|
||||||
|
- vpcs
|
||||||
|
- subnets
|
||||||
|
- vms
|
||||||
|
|
||||||
|
# Remote pages fetched from git — same mechanism as components.
|
||||||
|
#
|
||||||
|
# - name: vpcs
|
||||||
|
# repo: https://git.g3e.fr/team-reseau/page-vpcs
|
||||||
|
# ref: v1.0.0
|
||||||
|
remote_pages: []
|
||||||
|
|
||||||
|
# Menu — order and visibility in the side-nav.
|
||||||
|
# List routes in desired display order.
|
||||||
|
# A page not listed here is accessible via /#/route but hidden from the nav.
|
||||||
|
menu:
|
||||||
|
- dashboard
|
||||||
|
- vpcs
|
||||||
|
- subnets
|
||||||
|
- vms
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ class SideNav extends HTMLElement {
|
||||||
#hamburger = null
|
#hamburger = null
|
||||||
#backdrop = null
|
#backdrop = null
|
||||||
#mqHandler = null
|
#mqHandler = null
|
||||||
|
#hashHandler = null
|
||||||
#isMobile = false
|
#isMobile = false
|
||||||
|
|
||||||
async connectedCallback() {
|
async connectedCallback() {
|
||||||
|
|
@ -44,20 +45,35 @@ class SideNav extends HTMLElement {
|
||||||
this.#render()
|
this.#render()
|
||||||
|
|
||||||
this.#mqHandler = () => this.#applyMode()
|
this.#mqHandler = () => this.#applyMode()
|
||||||
|
this.#hashHandler = () => this.#updateActive()
|
||||||
MQ.addEventListener('change', this.#mqHandler)
|
MQ.addEventListener('change', this.#mqHandler)
|
||||||
|
window.addEventListener('hashchange', this.#hashHandler)
|
||||||
this.#applyMode()
|
this.#applyMode()
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback() {
|
disconnectedCallback() {
|
||||||
MQ.removeEventListener('change', this.#mqHandler)
|
MQ.removeEventListener('change', this.#mqHandler)
|
||||||
|
window.removeEventListener('hashchange', this.#hashHandler)
|
||||||
this.#removeHamburger()
|
this.#removeHamburger()
|
||||||
this.#removeBackdrop()
|
this.#removeBackdrop()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Rendu des items ─────────────────────────────────────────────────────────
|
// ── Rendu des items ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#currentHref() {
|
||||||
|
return window.location.hash || `#/${this.#items[0]?.href?.replace(/^#\//, '') ?? 'dashboard'}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update active class without re-rendering the whole nav.
|
||||||
|
#updateActive() {
|
||||||
|
const current = this.#currentHref()
|
||||||
|
this.shadowRoot?.querySelectorAll('.item').forEach(a =>
|
||||||
|
a.classList.toggle('active', a.getAttribute('href') === current)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
#render() {
|
#render() {
|
||||||
const current = window.location.pathname.split('/').pop() || 'index.html'
|
const current = this.#currentHref()
|
||||||
|
|
||||||
const links = this.#items.map(item => {
|
const links = this.#items.map(item => {
|
||||||
const active = item.href === current
|
const active = item.href === current
|
||||||
|
|
|
||||||
54
web/core/router.js
Normal file
54
web/core/router.js
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
// Hash-based SPA router.
|
||||||
|
// Reads pages.json (generated by build.sh from components.yml).
|
||||||
|
// Each page is an HTML fragment (index.html) + optional CSS + optional JS module.
|
||||||
|
//
|
||||||
|
// Load order enforced by index.html:
|
||||||
|
// await import('./core/registry.js') ← defines all components
|
||||||
|
// await import('./core/router.js') ← this file
|
||||||
|
|
||||||
|
const response = await fetch('./pages.json')
|
||||||
|
if (!response.ok) throw new Error('router: failed to load pages.json — run build.sh')
|
||||||
|
const PAGES = await response.json()
|
||||||
|
|
||||||
|
// Cache of imported JS modules — modules are only fetched once per session.
|
||||||
|
const MODULE_CACHE = new Map()
|
||||||
|
|
||||||
|
// Currently active page CSS <link> — removed on navigation.
|
||||||
|
let activeCSS = null
|
||||||
|
|
||||||
|
function currentRoute() {
|
||||||
|
return window.location.hash.replace(/^#\//, '') || PAGES[0]?.route || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
async function render(routeName) {
|
||||||
|
const page = PAGES.find(p => p.route === routeName) ?? PAGES[0]
|
||||||
|
const main = document.querySelector('main')
|
||||||
|
if (!main || !page) return
|
||||||
|
|
||||||
|
// ── HTML ────────────────────────────────────────────────────────────────────
|
||||||
|
const htmlRes = await fetch(page.html)
|
||||||
|
if (!htmlRes.ok) throw new Error(`router: failed to load ${page.html}`)
|
||||||
|
main.innerHTML = await htmlRes.text()
|
||||||
|
|
||||||
|
// ── CSS ─────────────────────────────────────────────────────────────────────
|
||||||
|
activeCSS?.remove()
|
||||||
|
activeCSS = null
|
||||||
|
if (page.css) {
|
||||||
|
const link = document.createElement('link')
|
||||||
|
link.rel = 'stylesheet'
|
||||||
|
link.href = page.css
|
||||||
|
document.head.appendChild(link)
|
||||||
|
activeCSS = link
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── JS ──────────────────────────────────────────────────────────────────────
|
||||||
|
if (page.js) {
|
||||||
|
if (!MODULE_CACHE.has(page.js)) {
|
||||||
|
MODULE_CACHE.set(page.js, await import(`../${page.js}`))
|
||||||
|
}
|
||||||
|
MODULE_CACHE.get(page.js)?.init?.(main)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await render(currentRoute())
|
||||||
|
window.addEventListener('hashchange', () => render(currentRoute()))
|
||||||
|
|
@ -92,48 +92,22 @@
|
||||||
<div class="layout">
|
<div class="layout">
|
||||||
<side-nav></side-nav>
|
<side-nav></side-nav>
|
||||||
|
|
||||||
<main>
|
<main></main>
|
||||||
<div id="error"></div>
|
|
||||||
<date-display></date-display>
|
|
||||||
|
|
||||||
<!-- Dev test: open stacked panels -->
|
|
||||||
<div style="display:flex;gap:8px;margin-top:8px;">
|
|
||||||
<button onclick="openTestPanel('Panel A', '400px')"
|
|
||||||
style="background:#313244;border:1px solid #45475a;border-radius:6px;
|
|
||||||
padding:7px 14px;color:#89b4fa;font-size:13px;cursor:pointer;">
|
|
||||||
Open panel A
|
|
||||||
</button>
|
|
||||||
<button onclick="openTestPanel('Panel B', '520px')"
|
|
||||||
style="background:#313244;border:1px solid #45475a;border-radius:6px;
|
|
||||||
padding:7px 14px;color:#a6e3a1;font-size:13px;cursor:pointer;">
|
|
||||||
Open panel B
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import './core/registry.js'
|
|
||||||
import { SidePanel } from './components/side-panel/side-panel.js'
|
|
||||||
|
|
||||||
window.openTestPanel = (title, width) => {
|
|
||||||
SidePanel.open({
|
|
||||||
title,
|
|
||||||
width,
|
|
||||||
content: `<p style="color:#a6adc8;font-size:14px;line-height:1.6">
|
|
||||||
Contenu du panneau <strong style="color:#cdd6f4">${title}</strong>.<br>
|
|
||||||
Largeur : ${width}.<br><br>
|
|
||||||
Tu peux ouvrir plusieurs panneaux — ils s'empilent vers la gauche.
|
|
||||||
Ferme avec ✕, Échap, ou en cliquant le fond.
|
|
||||||
</p>`,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener('unhandledrejection', e => {
|
window.addEventListener('unhandledrejection', e => {
|
||||||
const el = document.getElementById('error')
|
const el = document.getElementById('error')
|
||||||
|
if (el) {
|
||||||
el.style.display = 'block'
|
el.style.display = 'block'
|
||||||
el.textContent = `Error: ${e.reason?.message ?? e.reason}`
|
el.textContent = `Error: ${e.reason?.message ?? e.reason}`
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Sequential: registry defines all custom elements,
|
||||||
|
// then router renders the current route.
|
||||||
|
await import('./core/registry.js')
|
||||||
|
await import('./core/router.js')
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,22 @@
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"label": "Dashboard",
|
"label": "Dashboard",
|
||||||
"href": "index.html",
|
"href": "#/dashboard",
|
||||||
"icon": "dashboard"
|
"icon": "dashboard"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "VPCs",
|
"label": "VPCs",
|
||||||
"href": "vpc.html",
|
"href": "#/vpcs",
|
||||||
"icon": "vpc"
|
"icon": "vpc"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Subnets",
|
"label": "Subnets",
|
||||||
"href": "subnet.html",
|
"href": "#/subnets",
|
||||||
"icon": "subnet"
|
"icon": "subnet"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "VMs",
|
"label": "VMs",
|
||||||
"href": "vm.html",
|
"href": "#/vms",
|
||||||
"icon": "vm"
|
"icon": "vm"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
34
web/pages.json
Normal file
34
web/pages.json
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"route": "dashboard",
|
||||||
|
"label": "Dashboard",
|
||||||
|
"icon": "dashboard",
|
||||||
|
"html": "pages/dashboard/index.html",
|
||||||
|
"css": "pages/dashboard/dashboard.css",
|
||||||
|
"js": "pages/dashboard/dashboard.js"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"route": "vpcs",
|
||||||
|
"label": "VPCs",
|
||||||
|
"icon": "vpc",
|
||||||
|
"html": "pages/vpcs/index.html",
|
||||||
|
"css": null,
|
||||||
|
"js": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"route": "subnets",
|
||||||
|
"label": "Subnets",
|
||||||
|
"icon": "subnet",
|
||||||
|
"html": "pages/subnets/index.html",
|
||||||
|
"css": null,
|
||||||
|
"js": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"route": "vms",
|
||||||
|
"label": "VMs",
|
||||||
|
"icon": "vm",
|
||||||
|
"html": "pages/vms/index.html",
|
||||||
|
"css": null,
|
||||||
|
"js": null
|
||||||
|
}
|
||||||
|
]
|
||||||
2
web/pages/dashboard/dashboard.css
Normal file
2
web/pages/dashboard/dashboard.css
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
/* Styles scoped to the dashboard page.
|
||||||
|
Applied when the route is active, removed on navigation. */
|
||||||
8
web/pages/dashboard/dashboard.js
Normal file
8
web/pages/dashboard/dashboard.js
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
// Dashboard page — called by the router each time this route is activated.
|
||||||
|
// Export init(main) to run logic after the HTML fragment is injected.
|
||||||
|
//
|
||||||
|
// `main` is the <main> DOM element containing the injected HTML.
|
||||||
|
|
||||||
|
export function init(main) {
|
||||||
|
// Page is ready — add event listeners, fetch data, etc.
|
||||||
|
}
|
||||||
33
web/pages/dashboard/index.html
Normal file
33
web/pages/dashboard/index.html
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
<date-display></date-display>
|
||||||
|
|
||||||
|
<!-- Dev test: open stacked panels -->
|
||||||
|
<div style="display:flex;gap:8px;margin-top:8px;">
|
||||||
|
<button onclick="openTestPanel('Panel A', '400px')"
|
||||||
|
style="background:#313244;border:1px solid #45475a;border-radius:6px;
|
||||||
|
padding:7px 14px;color:#89b4fa;font-size:13px;cursor:pointer;">
|
||||||
|
Open panel A
|
||||||
|
</button>
|
||||||
|
<button onclick="openTestPanel('Panel B', '520px')"
|
||||||
|
style="background:#313244;border:1px solid #45475a;border-radius:6px;
|
||||||
|
padding:7px 14px;color:#a6e3a1;font-size:13px;cursor:pointer;">
|
||||||
|
Open panel B
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<script type="module">
|
||||||
|
import { SidePanel } from '../components/side-panel/side-panel.js'
|
||||||
|
|
||||||
|
window.openTestPanel = (title, width) => {
|
||||||
|
SidePanel.open({
|
||||||
|
title,
|
||||||
|
width,
|
||||||
|
content: `<p style="color:#a6adc8;font-size:14px;line-height:1.6">
|
||||||
|
Contenu du panneau <strong style="color:#cdd6f4">${title}</strong>.<br>
|
||||||
|
Largeur : ${width}.<br><br>
|
||||||
|
Tu peux ouvrir plusieurs panneaux — ils s'empilent vers la gauche.
|
||||||
|
Ferme avec ✕, Échap, ou en cliquant le fond.
|
||||||
|
</p>`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
6
web/pages/dashboard/manifest.json
Normal file
6
web/pages/dashboard/manifest.json
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"route": "dashboard",
|
||||||
|
"label": "Dashboard",
|
||||||
|
"icon": "dashboard",
|
||||||
|
"version": "0.1.0"
|
||||||
|
}
|
||||||
1
web/pages/subnets/index.html
Normal file
1
web/pages/subnets/index.html
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<p>Subnets — à venir</p>
|
||||||
1
web/pages/subnets/manifest.json
Normal file
1
web/pages/subnets/manifest.json
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{ "route": "subnets", "label": "Subnets", "icon": "subnet", "version": "0.1.0" }
|
||||||
1
web/pages/vms/index.html
Normal file
1
web/pages/vms/index.html
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<p>VMs — à venir</p>
|
||||||
1
web/pages/vms/manifest.json
Normal file
1
web/pages/vms/manifest.json
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{ "route": "vms", "label": "VMs", "icon": "vm", "version": "0.1.0" }
|
||||||
1
web/pages/vpcs/index.html
Normal file
1
web/pages/vpcs/index.html
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<p>VPCs — à venir</p>
|
||||||
1
web/pages/vpcs/manifest.json
Normal file
1
web/pages/vpcs/manifest.json
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{ "route": "vpcs", "label": "VPCs", "icon": "vpc", "version": "0.1.0" }
|
||||||
Loading…
Add table
Add a link
Reference in a new issue