Compare commits

...

2 commits

Author SHA1 Message Date
f49eae6c31
rename components.yml → config.yml
The manifest covers components, pages, vendors and menu — config.yml
is a more accurate name. Updated build.sh (both project and template),
router.js comment, and README.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: GnomeZworc <nicolas.boufidjeline@g3e.fr>
2026-06-07 13:08:50 +02:00
6796e102e0
add template + document framework in root README
template/README.md: stripped to a one-liner bootstrap command.
README.md: full project docs (structure, add page, add component, build.sh).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: GnomeZworc <nicolas.boufidjeline@g3e.fr>
2026-06-07 13:06:16 +02:00
19 changed files with 667 additions and 5 deletions

View file

@ -1,3 +1,61 @@
# Syn # Syn
Ce projet est un framwork web base sur un javascript tres simple. Framework web minimaliste basé sur JavaScript vanilla et les Web Components.
Routage hash-based, composants déclarés dans `config.yml`, build via `build.sh`.
## Structure d'un projet
```
├── sources/ # Code servi — déployer ce dossier
│ ├── index.html
│ ├── favicon.svg
│ ├── config.json # Config runtime (api_url, tokens…)
│ ├── core/ # Framework core
│ ├── components/ # Téléchargés par build.sh (gitignored)
│ ├── vendor/ # Libs tierces (gitignored)
│ └── pages/
│ └── <nom>/ # Une page = un dossier
├── config.yml # Source de vérité
└── build.sh # Télécharge les remotes + génère les JSON
```
## Démarrer un projet
```bash
cp -r template/ mon-projet/
cd mon-projet/
./build.sh
python3 -m http.server 8000 --directory sources/
```
## Ajouter une page
1. Créer `sources/pages/<nom>/` avec :
- `manifest.json``{ "route", "label", "icon" }`
- `index.html` — fragment HTML (ex. `<ma-page></ma-page>`)
- `<nom>.js``customElements.define` + `export function init(main) {}`
- `<nom>.css` — styles scopés à la page (optionnel)
2. Déclarer sous `local_pages:` dans `config.yml`
3. Ajouter la route à `menu:` si visible dans la nav
4. `./build.sh`
## Ajouter un composant
1. Déclarer sous `components:` dans `config.yml` :
```yaml
components:
- name: mon-composant
repo: https://git.example.com/org/mon-composant
ref: main
```
2. `./build.sh` — télécharge dans `sources/components/mon-composant/`
## build.sh
```
./build.sh # build complet
./build.sh --check # valide le manifest sans télécharger
```
Dépendances : `yq` (mikefarah v4), `jq`, `curl`.
Variable optionnelle : `GIT_TOKEN` pour les repos privés.

View file

@ -1,10 +1,10 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Build the web UI component set from components.yml. # Build the web UI component set from config.yml.
# #
# ./build.sh build (download remote components + generate components.json) # ./build.sh build (download remote components + generate components.json)
# ./build.sh --check validate manifest and entry files without downloading # ./build.sh --check validate manifest and entry files without downloading
# #
# Reads components.yml, downloads each remote component into components/<name>/ # Reads config.yml, downloads each remote component into components/<name>/
# at the requested ref, then generates components.json (runtime load list) and # at the requested ref, then generates components.json (runtime load list) and
# components.lock.json (resolved commit pins). # components.lock.json (resolved commit pins).
# #
@ -16,7 +16,7 @@ set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
WEB_DIR="${SCRIPT_DIR}/sources" WEB_DIR="${SCRIPT_DIR}/sources"
MANIFEST="${SCRIPT_DIR}/components.yml" MANIFEST="${SCRIPT_DIR}/config.yml"
COMPONENTS_DIR="${WEB_DIR}/components" COMPONENTS_DIR="${WEB_DIR}/components"
OUTPUT="${WEB_DIR}/components.json" OUTPUT="${WEB_DIR}/components.json"
LOCKFILE="${WEB_DIR}/components.lock.json" LOCKFILE="${WEB_DIR}/components.lock.json"

View file

@ -1,5 +1,5 @@
// Hash-based SPA router. // Hash-based SPA router.
// Reads pages.json (generated by build.sh from components.yml). // Reads pages.json (generated by build.sh from config.yml).
// Each page is an HTML fragment (index.html) + optional CSS + optional JS module. // Each page is an HTML fragment (index.html) + optional CSS + optional JS module.
// //
// Load order enforced by index.html: // Load order enforced by index.html:

5
template/.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
sources/components.json
sources/navigation.json
sources/pages.json
sources/vendor/
sources/components/

6
template/README.md Normal file
View file

@ -0,0 +1,6 @@
# Syn — Project Template
```bash
./build.sh
python3 -m http.server 8000 --directory sources/
```

333
template/build.sh Executable file
View file

@ -0,0 +1,333 @@
#!/usr/bin/env bash
# Build the web UI component set from config.yml.
#
# ./build.sh build (download remote components + generate components.json)
# ./build.sh --check validate manifest and entry files without downloading
#
# Reads config.yml, downloads each remote component into components/<name>/
# at the requested ref, then generates components.json (runtime load list) and
# components.lock.json (resolved commit pins).
#
# Dependencies: yq (mikefarah/yq v4), jq, curl
# Optional env:
# GIT_TOKEN forge token for private repos (sent as "Authorization: token …")
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
WEB_DIR="${SCRIPT_DIR}/sources"
MANIFEST="${SCRIPT_DIR}/config.yml"
COMPONENTS_DIR="${WEB_DIR}/components"
OUTPUT="${WEB_DIR}/components.json"
LOCKFILE="${WEB_DIR}/components.lock.json"
GIT_TOKEN="${GIT_TOKEN:-}"
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; NC='\033[0m'
log() { echo -e "${GREEN}[+]${NC} $*"; }
warn() { echo -e "${YELLOW}[!]${NC} $*"; }
info() { echo -e "${BLUE}[i]${NC} $*"; }
die() { echo -e "${RED}[-]${NC} $*" >&2; exit 1; }
CHECK_ONLY=false
[[ "${1:-}" == "--check" ]] && CHECK_ONLY=true
# ── Dependency + manifest guards ────────────────────────────────────────────────
command -v yq &>/dev/null || die "yq is required (mikefarah/yq v4)"
command -v jq &>/dev/null || die "jq is required"
command -v curl &>/dev/null || die "curl is required"
[[ -f "$MANIFEST" ]] || die "manifest not found: $MANIFEST"
# ── Forge API helpers ───────────────────────────────────────────────────────────
api_get() {
local url="$1"
local args=(-sf -H "Accept: application/json")
[[ -n "$GIT_TOKEN" ]] && args+=(-H "Authorization: token ${GIT_TOKEN}")
curl "${args[@]}" "$url" || die "API request failed: $url"
}
raw_dl() {
local url="$1" out="$2"
local args=(-sfL)
[[ -n "$GIT_TOKEN" ]] && args+=(-H "Authorization: token ${GIT_TOKEN}")
curl "${args[@]}" "$url" -o "$out" || die "download failed: $url"
}
# Split a repo URL into "server owner repo".
# https://git.g3e.fr/team-reseau/vpc-panel → https://git.g3e.fr team-reseau vpc-panel
parse_repo_url() {
local url="$1"
url="${url%.git}"; url="${url%/}"
local proto="${url%%://*}"
local rest="${url#*://}"
local host="${rest%%/*}"
local path="${rest#*/}"
[[ "$path" == "$rest" || -z "$path" ]] && die "invalid repo URL (need owner/repo): $1"
local owner="${path%/*}" repo="${path##*/}"
[[ -z "$owner" || -z "$repo" ]] && die "invalid repo URL (need owner/repo): $1"
echo "${proto}://${host}" "$owner" "$repo"
}
# Build the commits API URL — handles GitHub (api.github.com) vs Gitea (/api/v1).
forge_commits_url() {
local server="$1" owner="$2" repo="$3" ref="$4"
if [[ "$server" == "https://github.com" ]]; then
echo "https://api.github.com/repos/${owner}/${repo}/commits?sha=${ref}&per_page=1"
else
echo "${server}/api/v1/repos/${owner}/${repo}/commits?sha=${ref}&limit=1"
fi
}
# Build the contents API URL for a given path (empty = repo root).
forge_contents_url() {
local server="$1" owner="$2" repo="$3" ref="$4" path="$5"
if [[ "$server" == "https://github.com" ]]; then
if [[ -z "$path" ]]; then
echo "https://api.github.com/repos/${owner}/${repo}/contents?ref=${ref}"
else
echo "https://api.github.com/repos/${owner}/${repo}/contents/${path}?ref=${ref}"
fi
else
if [[ -z "$path" ]]; then
echo "${server}/api/v1/repos/${owner}/${repo}/contents?ref=${ref}"
else
echo "${server}/api/v1/repos/${owner}/${repo}/contents/${path}?ref=${ref}"
fi
fi
}
resolve_commit() {
local server="$1" owner="$2" repo="$3" ref="$4"
api_get "$(forge_commits_url "$server" "$owner" "$repo" "$ref")" \
| jq -r '.[0].sha // empty'
}
# Recursively download a repo path (relative to repo root) into dest dir.
download_path() {
local server="$1" owner="$2" repo="$3" ref="$4" rpath="$5" dest="$6"
local api
api="$(forge_contents_url "$server" "$owner" "$repo" "$ref" "$rpath")"
local listing
listing="$(api_get "$api")"
mkdir -p "$dest"
while IFS= read -r entry; do
local type name dl path
type="$(jq -r '.type' <<<"$entry")"
name="$(jq -r '.name' <<<"$entry")"
dl="$( jq -r '.download_url // ""' <<<"$entry")"
path="$(jq -r '.path' <<<"$entry")"
if [[ "$type" == "dir" ]]; then
download_path "$server" "$owner" "$repo" "$ref" "$path" "${dest}/${name}"
else
[[ -z "$dl" ]] && die "no download_url for ${path} in ${owner}/${repo}@${ref}"
raw_dl "$dl" "${dest}/${name}"
fi
done < <(echo "$listing" | jq -c 'if type=="array" then .[] else . end')
}
# ── Read 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 LOCAL_PAGES < <(yq '(.local_pages // [])[]' "$MANIFEST")
mapfile -t REMOTE_PAGES < <(yq '(.remote_pages // [])[] | [.name, .repo, (.ref // "main")] | join("|")' "$MANIFEST")
mapfile -t VENDORS < <(yq '(.vendors // [])[] | [.name, .repo, (.ref // "main"), (.path // ""), (.dest // "")] | join("|")' "$MANIFEST")
# ── Download vendors ────────────────────────────────────────────────────────────
# Vendors are third-party libraries downloaded into web/vendor/ at build time.
# They are gitignored and never loaded by components.json — components import
# them directly via relative paths (e.g. ../../vendor/novnc/core/rfb.js).
for spec in "${VENDORS[@]}"; do
[[ -z "$spec" ]] && continue
IFS='|' read -r name repo ref vpath dest <<<"$spec"
[[ -z "$name" || -z "$repo" ]] && die "vendor entry missing name or repo: '$spec'"
read -r server owner gitrepo <<<"$(parse_repo_url "$repo")"
[[ -z "$dest" ]] && dest="vendor/${name}"
local_dest="${WEB_DIR}/${dest}"
if [[ "$CHECK_ONLY" == true ]]; then
info "would fetch vendor ${name} from ${repo}@${ref} path=${vpath:-/}"
continue
fi
log "fetching vendor ${name}${owner}/${gitrepo}@${ref}${vpath:+ path=}${vpath}"
commit="$(resolve_commit "$server" "$owner" "$gitrepo" "$ref")"
[[ -z "$commit" ]] && die "cannot resolve ref '${ref}' in ${owner}/${gitrepo}"
rm -rf "$local_dest"
download_path "$server" "$owner" "$gitrepo" "$ref" "$vpath" "$local_dest"
info " vendor '${name}' → ${dest} (${commit:0:12})"
done
# Ordered list of load paths for components.json
declare -a LOAD_PATHS=()
# ── Validate locals ──────────────────────────────────────────────────────────────
for name in "${LOCALS[@]}"; do
[[ -z "$name" ]] && continue
entry="${COMPONENTS_DIR}/${name}/${name}.js"
if [[ ! -f "$entry" ]]; then
die "local component '${name}' missing entry file: components/${name}/${name}.js"
fi
LOAD_PATHS+=("components/${name}/${name}.js")
done
# ── Download remotes ─────────────────────────────────────────────────────────────
LOCK_ENTRIES="[]"
for spec in "${REMOTES[@]}"; do
[[ -z "$spec" ]] && continue
IFS='|' read -r name repo ref <<<"$spec"
[[ -z "$name" || -z "$repo" ]] && die "manifest entry missing name or repo: '$spec'"
read -r server owner gitrepo <<<"$(parse_repo_url "$repo")"
dest="${COMPONENTS_DIR}/${name}"
if [[ "$CHECK_ONLY" == true ]]; then
info "would fetch ${name} from ${repo}@${ref}"
commit="$(resolve_commit "$server" "$owner" "$gitrepo" "$ref" || true)"
[[ -z "$commit" ]] && warn " could not resolve ref '${ref}' in ${owner}/${gitrepo}"
LOAD_PATHS+=("components/${name}/${name}.js")
continue
fi
log "fetching ${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"
entry="${dest}/${name}.js"
[[ -f "$entry" ]] || die "component '${name}' has no ${name}.js at repo root"
LOAD_PATHS+=("components/${name}/${name}.js")
LOCK_ENTRIES="$(jq \
--arg name "$name" --arg repo "$repo" --arg ref "$ref" --arg commit "$commit" \
'. + [{name:$name, repo:$repo, ref:$ref, commit:$commit}]' <<<"$LOCK_ENTRIES")"
info " pinned ${commit:0:12}"
done
# ── Generate components.json ─────────────────────────────────────────────────────
if [[ "$CHECK_ONLY" == true ]]; then
log "check passed: ${#LOAD_PATHS[@]} components, manifest valid"
exit 0
fi
printf '%s\n' "${LOAD_PATHS[@]}" | jq -R . | jq -s . > "$OUTPUT"
echo "$LOCK_ENTRIES" | jq '.' > "$LOCKFILE"
log "wrote ${OUTPUT} (${#LOAD_PATHS[@]} components)"
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
# Build navigation.json from menu: section.
# Convert full manifest to JSON once (single yq call) then jq handles all logic.
# Menu items: string → page lookup, object → pass-through (separator/section).
manifest_json=$(yq -o json '.' "$MANIFEST")
menu_json=$(echo "$manifest_json" | jq -c '.menu // []')
menu_len=$(echo "$menu_json" | jq 'length')
if [[ "$menu_len" -gt 0 ]]; then
NAV_ENTRIES=$(jq -n \
--argjson menu "$menu_json" \
--argjson pages "$PAGE_ENTRIES" \
'def resolve($pages):
. as $r | $pages[] | select(.route == $r) | { label, href: ("#/" + .route), icon };
[ $menu[] |
if type == "string"
then resolve($pages)
elif .type == "group"
then { type: "group", label: .label, icon: (.icon // ""),
children: [ .children[]? |
if type == "string" then resolve($pages) else . end ] }
else .
end
]')
else
# No menu defined — use all pages in discovery order
NAV_ENTRIES=$(echo "$PAGE_ENTRIES" | \
jq '[.[] | { label, href: ("#/" + .route), icon }]')
fi
# pages.json — full list (all pages, original discovery order)
echo "$PAGE_ENTRIES" | jq '.' > "${WEB_DIR}/pages.json"
# navigation.json — write NAV_ENTRIES as-is (already correctly shaped)
echo "$NAV_ENTRIES" | jq '.' > "${WEB_DIR}/navigation.json"
log "wrote pages.json ($(echo "$PAGE_ENTRIES" | jq 'length') pages) + navigation.json ($(echo "$NAV_ENTRIES" | jq 'length') in menu)"
fi

39
template/config.yml Normal file
View file

@ -0,0 +1,39 @@
# Component manifest — source of truth for the web UI.
#
# Edit this file, then run ./build.sh to:
# - download every remote component into sources/components/<name>/
# - generate sources/components.json (the runtime load list) in declared order
#
# Load order matters: service components (login-gate, api-client) must come
# before the components that consume them.
# Local components shipped inside this repo. Not downloaded — listed here only
# so build.sh can place them in the generated load order.
local_components: []
# Remote components fetched from git at build time.
# Each is its own repo; its root must contain <name>.js and manifest.json.
#
# - name: side-nav
# repo: https://git.g3e.fr/syonad/syn-side-nav
# ref: main
components: []
# Vendor libraries — downloaded into sources/vendor/ at build time (gitignored).
# Components import them via relative paths, never via components.json.
vendors: []
# Pages — each page is a self-contained directory (JS + CSS + index.html + manifest.json).
# Page manifest.json must contain: route, label, icon.
# Page JS must export an optional init(main) function.
local_pages:
- home
remote_pages: []
# Menu — order and visibility in the navigation.
# List routes in desired display order.
# A page not listed here is accessible via /#/route but hidden from the nav.
menu:
- home

View file

@ -0,0 +1,3 @@
{
"api_url": "http://localhost:8080"
}

View file

@ -0,0 +1,4 @@
const response = await fetch('./config.json')
if (!response.ok) throw new Error('Failed to load config.json')
export const config = await response.json()

View file

@ -0,0 +1,10 @@
// Dynamic menu loader.
// Fetches data for groups declared with empty children in navigation.json
// and injects them into side-nav after it is ready.
//
// Imported non-blocking from index.html — does not delay routing.
//
// Example usage:
// const nav = document.querySelector('side-nav')
// await nav.ready
// nav.setGroupChildren('My Group', [{ label: 'Item', href: '#/route', icon: 'icon' }])

View file

@ -0,0 +1,8 @@
const response = await fetch('./components.json')
if (!response.ok) throw new Error('Failed to load components.json')
const components = await response.json()
for (const path of components) {
await import(`../${path}`)
}

View file

@ -0,0 +1,41 @@
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()
const MODULE_CACHE = new Map()
let activeCSS = null
function currentRoute() {
const hash = window.location.hash.replace(/^#\//, '') || PAGES[0]?.route || ''
return hash.split('?')[0]
}
async function render(routeName) {
const page = PAGES.find(p => p.route === routeName) ?? PAGES[0]
const main = document.querySelector('main')
if (!main || !page) return
const htmlRes = await fetch(page.html)
if (!htmlRes.ok) throw new Error(`router: failed to load ${page.html}`)
main.innerHTML = await htmlRes.text()
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
}
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()))

View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect width="32" height="32" rx="6" fill="#1e1e2e"/>
<text x="16" y="22" font-family="monospace" font-size="18" font-weight="bold"
text-anchor="middle" fill="#89b4fa">S</text>
</svg>

After

Width:  |  Height:  |  Size: 256 B

106
template/sources/index.html Normal file
View file

@ -0,0 +1,106 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mon Projet</title>
<link rel="icon" type="image/svg+xml" href="./favicon.svg">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
background: #181825;
color: #cdd6f4;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
height: 100vh;
display: flex;
flex-direction: column;
}
header {
display: flex;
align-items: center;
gap: 12px;
padding: 14px 24px;
border-bottom: 1px solid #313244;
background: #1e1e2e;
flex-shrink: 0;
}
header h1 {
font-size: 17px;
font-weight: 600;
color: #89b4fa;
letter-spacing: 0.02em;
}
header span {
font-size: 12px;
color: #6c7086;
}
header .spacer { flex: 1; }
.layout {
display: flex;
flex: 1;
overflow: hidden;
}
main {
flex: 1;
padding: 28px;
overflow-y: auto;
display: flex;
flex-wrap: wrap;
align-content: flex-start;
gap: 20px;
}
@media (max-width: 768px) {
main { padding: 16px; }
}
#error {
display: none;
background: #313244;
border: 1px solid #f38ba8;
border-radius: 8px;
padding: 14px 18px;
color: #f38ba8;
font-family: monospace;
font-size: 13px;
width: 100%;
}
</style>
</head>
<body>
<header>
<h1>Mon Projet</h1>
<span>powered by Syn</span>
<div class="spacer"></div>
</header>
<div class="layout">
<side-nav></side-nav>
<main></main>
</div>
<div id="error"></div>
<script type="module">
window.addEventListener('unhandledrejection', e => {
const el = document.getElementById('error')
if (el) {
el.style.display = 'block'
el.textContent = `Error: ${e.reason?.message ?? e.reason}`
}
})
await import('./core/registry.js')
await import('./core/router.js')
import('./core/menu.js')
</script>
</body>
</html>

View file

@ -0,0 +1,24 @@
.home {
padding: 8px;
}
.home h2 {
font-size: 20px;
font-weight: 600;
color: #cdd6f4;
margin-bottom: 8px;
}
.home p {
color: #6c7086;
font-size: 14px;
}
.home code {
background: #313244;
padding: 2px 6px;
border-radius: 4px;
font-family: monospace;
font-size: 13px;
color: #89b4fa;
}

View file

@ -0,0 +1,14 @@
class HomePage extends HTMLElement {
connectedCallback() {
this.innerHTML = `
<div class="home">
<h2>Bienvenue</h2>
<p>Modifiez cette page dans <code>sources/pages/home/</code>.</p>
</div>
`
}
}
customElements.define('home-page', HomePage)
export function init(_main) {}

View file

@ -0,0 +1 @@
<home-page></home-page>

View file

@ -0,0 +1,5 @@
{
"route": "home",
"label": "Home",
"icon": "home"
}