@clauzier/admin-kit (0.24.1)

Published 2026-09-10 12:06:34 +00:00 by 0x346e3730

Installation

@clauzier:registry=
npm install @clauzier/admin-kit@0.24.1
"@clauzier/admin-kit": "0.24.1"

About this package

@clauzier/admin-kit

Back-office React Admin généré depuis les schémas servis par cms-core. Une coquille cliente ne décrit que son identité : titre, palette, URL d'API. Les ressources, les formulaires, les libellés, l'écran de réglages et le menu viennent du contrat /api/content-schemas — un type de contenu déclaré côté socle apparaît dans l'admin sans qu'une ligne y soit écrite.

Installation

bun add @clauzier/admin-kit

Prérequis : cms-core ≥ 0.22 (brouillon, publication, contrat de sections v2 et menus liés aux pages). Face à un socle plus ancien, l'admin reste lisible mais n'ouvre ni les pages ni les menus.

Les paquets react-admin, @mui/material, @mui/icons-material, @api-platform/admin, @tanstack/react-query, react, react-dom et react-router-dom sont des peerDependencies : deux instances de react-admin dans un même bundle donnent deux contextes et plus rien ne fonctionne. La coquille les déclare donc explicitement.

Utilisation

import { createRoot } from 'react-dom/client';
import { AdminKitApp } from '@clauzier/admin-kit/app';
import '@clauzier/admin-kit/styles/admin.css';
import './palette.css';

createRoot(document.getElementById('root')!).render(
  <AdminKitApp
    apiUrl="https://api.exemple.tld/api"
    title="Mon Club"
    subtitle="Espace d'administration"
    tokenKey="monclub_admin_token"
    palette={{
      primary: '#4f7cff',
      secondary: '#e63946',
      light: {
        background: '#f4f4f8',
        paper: '#ffffff',
        text: '#12121a',
        textMuted: '#5c5c70',
        border: '#dcdce6',
      },
      dark: {
        background: '#0b0b10',
        paper: '#14141c',
        text: '#f2f2f5',
        textMuted: '#8a8a99',
        border: '#24242e',
      },
      fontBody: "'Inter', system-ui, sans-serif",
    }}
  />,
);

Props de AdminKitApp

Prop Type Défaut Rôle
apiUrl string point d'entrée de l'API (…/api)
title string titre de l'admin et de l'écran de connexion
subtitle string sous-titre de l'écran de connexion
tokenKey string clé localStorage du jeton Sanctum
palette AdminPalette couleurs de marque et surfaces des deux modes (voir Thème)
defaultTheme 'light' | 'dark' 'dark' mode au premier chargement
messages object {} surcharges de traduction, fusionnées avec celles du kit et de react-admin
children ReactNode <Resource> et <CustomRoutes> supplémentaires, transmis tels quels
dashboard DashboardComponent SiteDashboard composant de la page d'accueil
menuExtras readonly MenuExtra[] [] entrées de menu ajoutées à une section (Administration par défaut)
mediaPage boolean true page Médias (grille, téléversement, suppression, copie d'URL)

Ce que le kit construit

  • Écran de connexion : lit GET /auth/methods et propose ce que l'API annonce — formulaire email/mot de passe, bouton « Se connecter avec Discord » (CMS_ADMIN_URL posé côté API), ou les deux. Le retour Discord arrive sur /?code&nonce : le nonce est comparé à celui gardé en localStorage (<tokenKey>.nonce), le code échangé via POST /auth/exchange, la query retirée de l'URL. ?error=forbidden (compte non admin) et les autres erreurs s'affichent sur l'écran de connexion.
  • Tableau de bord « Votre site » (/, SiteDashboard) : quatre blocs, chacun masqué quand le socle ne sait pas l'alimenter.
    • Votre site en quelques étapes : la checklist de GET /site/checklist (barre « 2 étapes sur 5 », un lien « Faire maintenant » par étape restante ; settings/site, page:<id>/pages/<id>, menu:<location> → l'écran des menus du menu latéral, pages/pages). Affichée seulement avec capabilities.checklist et une réponse autre que 404.
    • Modifications à publier : les pages dont draft_updated_at est renseigné dans GET /pages, avec « Tout publier » (une publication par page, en série, après confirmation). Absente tant que la liste n'expose pas ce champ.
    • Mise en ligne (PublishCard, ex-RebuildCard) : état de la dernière mise en ligne, bouton « Mettre le site en ligne » (désactivé et expliqué quand le socle n'est pas configuré), lien « Voir le site » quand GET /site donne site_url. Masquée face à un socle sans GET /site/rebuild.
    • Raccourcis vers chaque écran (ShortcutGrid).
  • Pages (/pages, PagesList) : les pages rangées en arbre d'après leur adresse (association/les-membres sous association, page réelle ou groupe virtuel en italique, repliable), l'accueil (home_slug de GET /site) en tête avec son badge, le statut (Brouillon / Publié / Modifications non publiées d'après draftUpdatedAt), la date de modification et les actions Modifier, Réglages, Dupliquer (capabilities.duplicate, POST /pages/{id}/duplicate), Voir en ligne (site_url, page publiée seulement) et Supprimer (confirmation nommée, désactivée sur l'accueil).
    • Nouvelle page (NewPageDialog) : choix d'un modèle (étape sautée sans capabilities.templates ou sans modèle servi par GET /page-templates), titre, « Placer sous » (pages de premier niveau) et adresse dérivée du titre jusqu'à saisie manuelle (slugify, deriveSlug, validateSlug). La disponibilité passe par GET /pages?slug= après 300 ms, avec repli sur la liste chargée quand le socle ignore le filtre ; api, admin, _astro, _apercu, auth et membres sont refusés d'office. Création par POST /pages/from-template avec la capacité, sinon par la ressource Hydra (status: draft, visibility: public).
    • Réglages de la page (PageSettingsDialog, aussi exporté pour l'éditeur) : adresse (avertissement quand une page publiée en change), Référencement (Google) généré depuis page_fields.seo du contrat de sections (repli titre, description à compteur, image de partage), Accès (visibilité et rôles). N'écrit que slug, seo, visibility et roles par-dessus le record courant, débarrassé de ses clés de lecture seule (writablePage).
    • Ouvrir une page mène à l'éditeur (/pages/:id/editer, /pages/:id y redirige par BuilderRedirect). Sans capabilities.draft (socle plus ancien), la liste reste lisible mais rien ne s'ouvre : un bandeau demande la mise à jour du socle et la création est masquée.
  • Composer une page (/pages/:id/editer, PageBuilder, chargé à la demande par LazyPageBuilder avec @dnd-kit) : hors de la mise en page habituelle, trois volets sous une barre haute.
    • Barre haute : retour aux pages, titre modifiable en place, statut (PageStatusChip), indicateur d'enregistrement (aria-live), Annuler / Rétablir, Publier, menu « Plus d'actions » (Dupliquer la page avec capabilities.duplicate, Réglages de la page — PageSettingsDialog —, Annuler les modifications, Voir en ligne, Supprimer la page), aide.
    • Sections (gauche) : la pile de sections, glissable (poignée, clavier) et dotée d'un menu Monter / Descendre / Dupliquer / Masquer / Supprimer, puis la liste des sections à ajouter (recherche sans accents, groupée par categories du contrat, clic ou glisser vers la pile).
    • Centre : l'aperçu en direct (PreviewFrame) dès que le socle annonce capabilities.preview et une preview_url (GET /site). La page d'aperçu du site (/_apercu/, astro-kit ≥ 0.17) est chargée dans un cadre sandbox et reçoit le brouillon par postMessage après sa poignée de main cms:ready — envoyé à la seule origine de preview_url, accepté de la seule fenêtre du cadre, attachée dès la pose du cadre puisque le site répond avant son load. Les rendus complets sont regroupés (une image puis 120 ms) ; un simple changement de sélection part seul, et un clic sur une section dans l'aperçu la sélectionne dans l'éditeur. Ordinateur (1280 px réduit à la colonne) ou mobile (390 px) depuis la barre haute ; « Aperçu » ouvre la même page dans une fenêtre à part, nourrie par le même pont. Sans réponse du site sous 6 s, ou face à un site trop ancien, les sections s'affichent en cartes cliquables avec « Réessayer ». Le site doit connaître l'origine de l'admin à son build (CMS_ADMIN_URL). Une coquille remplace le composant par la prop preview de PageBuilder (BuilderPreviewProps).
    • Section sélectionnée (droite) : ses champs générés par inputFor (type: listListInput, lignes de sous-champs), un onglet Style depuis settings_fields restreint à schemas[].settings (image de fond visible seulement avec un fond « image »), masqué sans capabilities.settings. Le formulaire ne se remonte que sur un changement de contenu extérieur à la saisie (chargement, Annuler, Rétablir) : jamais sur une frappe.
    • Brouillon : chaque modification est enregistrée après 1,5 s par PUT /pages/{id}/draft (useAutosave : jamais deux envois en parallèle, envoi immédiat avant Publier, à la fermeture de l'onglet et quand il passe en arrière-plan). Les issues renvoyées s'affichent sous les champs et en badge sur la section ; un 409 bloque l'éditeur derrière « Recharger ».
    • Publier : confirmation nommée, puis POST /pages/{id}/publish ; un 422 marque chaque section à corriger sans rien publier.
    • Raccourcis : Ctrl+Z / Ctrl+Y (ou Ctrl+Maj+Z), Suppr, Alt+↑ / Alt+↓, Échap ; inactifs dans un champ ou un dialogue. Historique plafonné à 100 étapes, les frappes rapprochées sur une même section comptant pour une.
    • Sous 1100 px, les volets deviennent des tiroirs.
  • Menus (/menus, MenusPage, dès qu'un type menu existe dans le contrat) : « Menu principal » et « Menu du pied de page » en onglets, chaque menu un arbre de deux niveaux à glisser-déposer (@dnd-kit) ou à déplacer au clavier (Monter, Descendre, Placer sous l'entrée précédente, Remonter au niveau principal). Une entrée est une page du site (kind: page, page_id), un lien extérieur (kind: url, option « Ouvrir dans un nouvel onglet » → target) ou une ancre d'une section de l'accueil (kind: anchor, ancres lues dans settings.anchor de GET /pages/{id}/document). Les entrées { label, url } déjà en place sont lues telles quelles ; celles dont l'adresse désigne une page (/, /contact/, /#equipe) sont reliées à cette page et un bandeau l'annonce jusqu'à l'enregistrement. À l'écriture, url accompagne toujours page_id avec l'adresse résolue : un socle ou un site plus ancien continue de la lire, le socle qui résout par page_id l'ignore. Libellé vide = titre de la page ; une page en brouillon porte un badge, une page disparue un avertissement. Enregistrer écrit la fiche menu (data.location, data.items) par la ressource content_menu, ou la crée (menu-<emplacement>, publiée) ; le site la reprend à la prochaine mise en ligne. Sans type menu dans le contrat, l'écran demande la mise à jour du socle.
  • Aide contextuelle : un bouton « ? » dans la barre haute ouvre un panneau (HelpPanel) dont le titre et les phrases viennent de admin.help.screens.<écran>.{title, body[]} ; helpKeyFor(pathname) associe la route courante à un écran (dashboard, pages, builder, menus, media, users, settings, content) et le bouton s'efface sur une route inconnue. Une coquille surcharge une fiche par messages, phrases comprises.
  • Vocabulaire non technique dans toutes les chaînes du kit : « section » plutôt que bloc, « adresse de la page » plutôt que slug, « mise en ligne » plutôt que reconstruction. tests/wording.test.ts échoue si un mot technique revient.
  • Menu par sections : Pages / Contenu / Site / Administration. Un singleton et un type category: site vont dans Site, category: content dans Contenu ; sans catégorie, menu rejoint Site (vers /menus) et le reste Contenu.
  • Réglages (/<type> pour chaque singleton, « Réglages du site » pour site) : un onglet par groupe du contrat, « Général » pour les champs sans groupe, création de la fiche à la première sauvegarde.
  • Textes d'interface (ui: labels) : une ligne par clé avec sa description, le défaut en placeholder et les variables disponibles ; seules les surcharges non vides sont enregistrées.
  • Saisie libre à suggestions (ui: suggest) : un champ text dont les options se proposent en liste déroulante sans fermer la saisie — toute autre valeur reste acceptée, et la valeur enregistrée revient à l'identique à la ré-ouverture.
  • Catalogue de rôles (ui: roles) : liste { slug, label, discord_role_id, admin, public } éditée en lignes ; l'identifiant se dérive du libellé tant qu'il n'a pas été saisi à la main, un doublon bloque l'enregistrement. « Administration » ouvre le back-office aux porteurs du rôle Discord et le referme à ceux qui le perdent ; « Annuaire public » en fait une section de la page des membres, dans l'ordre de la liste.
  • Lots d'un giveaway (ui: prizes) : liste { prize, winner } éditée en lignes ; un gagnant vide vaut « tirage pas encore fait ».
  • Classement d'une compétition (ui: standings) : liste { place, pseudo, team } éditée en lignes ; une place vide vaut « participant non classé », et un classement renseigné ouvre la fiche de la compétition sur le site.
  • Accès d'une page : section « Accès » des réglages de la page, visibilité public ou members, et pour cette dernière les rôles autorisés choisis dans le catalogue de la fiche Site (vide : tout membre connecté).
  • Écran Utilisateurs : comptes liés (une puce par fournisseur — Discord, Steam — le survol donnant pseudo, identifiant et, pour Steam, heures de jeu et bannissement VAC), présence sur le serveur Discord, rôles attribués à la main (envoyés en PUT /users/{id} sous roles) et rôles synchronisés depuis Discord en lecture seule. Un compte sans identité se connecte par mot de passe.
  • Le toast « Contenu publié — le site sera reconstruit » ne promet une reconstruction que si GET /site/rebuild répond configured: true.

Points d'extension

import { AdminKitApp, registerInput } from '@clauzier/admin-kit';
import { CustomRoutes, Resource } from 'react-admin';
import { Route } from 'react-router-dom';
import BarChartIcon from '@mui/icons-material/BarChart';

registerInput('geo', (field, source) => <CarteInput source={source} label={field.label} />);

<AdminKitApp
  {...identite}
  dashboard={MonAccueil}
  menuExtras={[{ label: 'Statistiques', to: '/stats', icon: <BarChartIcon /> }]}
>
  <Resource name="commandes" list={CommandesList} />
  <CustomRoutes>
    <Route path="/stats" element={<Stats />} />
  </CustomRoutes>
</AdminKitApp>;
  • registerInput(type, factory) : un éditeur dédié pour un type ou un ui de champ, sans patcher le socle.
  • useAdminKit() : apiUrl, token(), siteClient, pagesClient, blockContract, menuExtras, mediaPage depuis n'importe quel écran rendu sous <AdminKitApp>.
  • useSiteInfo() (@clauzier/admin-kit/data) : { siteUrl, previewUrl, homeSlug } lus dans GET /site (home_slug vaut accueil par défaut).
  • ConfirmNamed et EmptyState (@clauzier/admin-kit/help) : une confirmation qui injecte %{name} dans son titre et son contenu, un état vide avec action.
  • useMemberRoles() (@clauzier/admin-kit/members) : le catalogue de rôles de la fiche Site, mis en cache par react-query.
  • menuSections(schemas, options) est pur : une coquille qui remplace le menu ou l'accueil part des mêmes sections.
  • @clauzier/admin-kit/builder : builderReducer et ses helpers purs (serializeDraft, sameDraft, errorsFromIssues, catalogOf, searchCatalog, settingsFieldsFor, shortcutFor), useAutosave, PageBuilder et ses volets (StructureList, SectionPalette, Inspector, StructureFallback, PreviewFrame, ViewportToggle), usePreviewBridge et usePreviewWindow avec le protocole pur d'aperçu (encodePreview, parseSiteMessage, isTrustedSource, previewFrameUrl) pour une coquille qui recompose l'écran.
  • pagesResource() (@clauzier/admin-kit/pages) : la ressource pages complète (liste en arbre, ouverture dans l'éditeur). pageTree, flattenTree, toPageSummary, pageStatus, slugify, deriveSlug, validateSlug et pageEditPath sont purs et exportés.

Thème

Le socle ne fournit aucune palette par défaut : une valeur de repli ferait hériter l'identité d'un client à tous les autres. createAdminTheme construit le thème MUI et émet les mêmes valeurs en custom properties --admin-*, pour que la feuille de style et les composants partent d'une source unique.

Les couleurs d'état sont optionnelles : accents: { success, warning, danger, info } complète ou remplace celles que MUI fournit pour le mode (createTheme().palette), et alimente aussi palette.success/warning/error/info du thème. Les tokens dérivés n'ont aucune valeur littérale : --admin-selection (= --admin-primary), --admin-drop-indicator, --admin-drag-ghost, --admin-focus-ring, --admin-overlay, --admin-success/-warning/-danger/-info et leurs variantes -soft, toutes par color-mix() sur les tokens de base.

tests/socle.test.ts échoue si une couleur littérale, un var(--x, repli) ou le nom d'un client apparaît dans src/.

Traductions

L'interface est en français (ra-language-french + les chaînes du kit). Une coquille surcharge n'importe quelle chaîne, y compris celles de react-admin, en passant un arbre partiel :

<AdminKitApp messages={{ ra: { action: { create: 'Ajouter' } } }}  />

Saisir un site multilingue

L'interface reste en français : ce que les langues ajoutent, c'est de quoi saisir chaque version du contenu. Rien de tout cela n'apparaît tant que le socle ne déclare qu'une langue (locales absent ou à un seul élément dans les contrats) : l'écran est celui d'avant, au pixel.

Le socle marque au schéma les champs traduisibles (translatable: true, sur text, richtext et media seulement). Le kit en tire tout le reste :

  • un onglet de langue par formulaire — fiche de contenu, réglages, réglages d'une page, menus — et un sélecteur dans le bandeau de l'éditeur de pages ;
  • dans une langue traduite, la source de l'entrée devient data.translations.<langue>.<champ>, et rien n'y est obligatoire : un champ vide laisse voir la version d'origine ;
  • les champs communs à toutes les langues (liens, images, choix dans une liste) ne s'y affichent pas, et l'onglet Style de l'éditeur disparaît : les régler là laisserait croire qu'on en tient une version par langue ;
  • une liste dont un sous-champ se traduit reste saisissable, mais sa structure se fige : les traductions vivent dans l'élément, l'ajout et le retrait se font dans la langue d'origine ;
  • l'aperçu de l'éditeur pointe la page d'aperçu de la langue (/en/_apercu/) ;
  • une pastille signale ce qui reste à traduire : par section dans l'éditeur, par page dans la liste.

Les textes d'interface du site (ui: labels) se saisissent langue par langue : la valeur stockée devient {fr: {…}, en: {…}}, et une carte plate héritée est lue comme celle de la langue d'origine puis migrée à la première saisie.

Contrat de schémas

Toutes les clés au-delà du socle publié sont optionnelles et lues défensivement : un type de champ inconnu se replie sur une saisie texte plutôt que de casser l'écran. Le kit reste donc utilisable face à une API plus ancienne que lui.

Clé Effet
label_plural libellé du menu — un pluriel français ne se dérive pas en ajoutant un s
singleton une seule fiche, slug forcé au type : écran de réglages au lieu d'une liste
category content ou site : section du menu ; inconnu → null, repli sur le type
groups [{ key, label }] ordonnés : un onglet par groupe dans l'écran de réglages
help texte d'aide sous le champ
ui force un éditeur dédié (menu, labels, suggest, roles, prizes, standings…)
group groupe du champ ; absent ou inconnu → « Général »
in_list affiche le champ en colonne de liste
reference_type type cible d'un champ reference, alimente la liste déroulante
options pour ui: labels, les clés de libellé dans l'ordre d'affichage ; pour color, les teintes proposées
option_labels pour ui: labels, description d'une clé ; sans point, titre d'un espace de noms ; pour color, le nom d'une teinte
defaults pour ui: labels, défaut de chaque clé, montré en placeholder
translatable le champ se saisit dans chaque langue : onglets de langue et écriture sous translations
defaults_locales pour ui: labels, défaut de chaque clé par langue
locales langues du site, la première servie à la racine ; une seule ou aucune → aucun onglet de langue
capabilities.search affiche la boîte de recherche ; sans le paramètre q côté API elle ne ferait rien
capabilities.statuses statuts acceptés par le socle

Contrat de sections (GET /block-schemas)

fetchBlockContract lit le contrat entier et le garde dans useAdminKit().blockContract ; fetchBlockSchemas reste disponible et n'en rend que schemas. Face à un socle qui ne sert que schemas, tout le reste est neutre : categories déduites des blocs, settings_fields: [], page_fields: {} et chaque capacité à false.

Clé Effet
schemas[].description, icon, category, default_data, settings, preview métadonnées d'une section, lues telles quelles
schemas[].fields[].help, ui, group, default, reference_type, item_fields, min_items, max_items forme alignée sur les champs d'entité ; type: list connu
categories [{ key, label }] pour grouper la palette de sections
settings_fields réglages de style communs, même forme que fields
page_fields.seo champs de référencement d'une page
capabilities.checklist affiche la checklist du tableau de bord
capabilities.draft ouvre les pages dans l'éditeur (sinon mise à jour demandée)
capabilities.settings, duplicate, preview onglet Style, action Dupliquer, aperçu en direct
capabilities.publish, list_fields, templates, menus lus et exposés ; pilotent les écrans des versions suivantes
locales langues du site : sélecteur de langue de l'éditeur, adresse de l'aperçu, pastilles de traduction

Endpoints consommés

Route Usage
GET /content-schemas contrat des types de contenu
GET /block-schemas contrat des blocs de page (absent → éditeur sans blocs)
GET /auth/methods moyens de connexion à afficher ({ password, discord })
POST /login, POST /logout session Sanctum
GET /auth/discord/redirect?target=admin&nonce=… connexion Discord du back-office (retour sur /?code&nonce)
POST /auth/exchange échange du code de retour Discord contre le jeton
/content_entities, /pages, /blocks, /media ressources API Platform (Hydra)
POST /media, DELETE /media/{id} téléversement avec progression, suppression
GET /site catalogue de rôles (data.member_roles), site_url, preview_url, home_slug
GET /site/checklist étapes du tableau de bord (404 → carte masquée)
GET /site/rebuild état de la mise en ligne (404 → carte masquée)
POST /site/rebuild demande de mise en ligne (409 si non configurée)
GET /pages/{id}/document, PUT /pages/{id}/draft document et brouillon d'une page (pagesClient)
POST /pages/{id}/publish, /discard, /unpublish, /duplicate actions de page (pagesClient)
POST /pages/from-template, GET /page-templates création guidée (404 sur les modèles → étape sautée, création par la ressource)
GET /pages?slug=… disponibilité d'une adresse (null si le filtre est ignoré)
GET/POST /users, PUT/DELETE /users/{id} écran Utilisateurs

Développement

bun install
bun run lint && bun run format:check && bun run typecheck && bun run test

Dependencies

Dependencies

ID Version
@dnd-kit/core ^6.3
@dnd-kit/sortable ^10
@dnd-kit/utilities ^3.2
ra-i18n-polyglot ^5.15.0
ra-input-rich-text ^5.15.1
ra-language-french ^5.15.0

Development dependencies

ID Version
@eslint/js 10.0.1
@testing-library/react 16.3.3
@types/node 26.4.1
@types/react 19.2.18
@types/react-dom 19.2.7
eslint 10.10.0
eslint-plugin-react-hooks 7.1.1
jsdom 30.0.1
prettier 3.9.6
typescript 6.0.3
typescript-eslint 8.69.0
vitest 5.0.0

Peer dependencies

ID Version
@api-platform/admin ^4.0.0
@mui/icons-material ^9.0.0
@mui/material ^9.0.0
@tanstack/react-query ^5.83.0
react ^19.0.0
react-admin ^5.15.0
react-dom ^19.0.0
react-router-dom ^7.0.0
Details
npm
2026-09-10 12:06:34 +00:00
120
UNLICENSED
latest
126 KiB
Assets (1)
Versions (24) View all
0.24.1 2026-09-10
0.24.0 2026-09-10
0.23.0 2026-09-09
0.22.0 2026-09-07
0.21.0 2026-09-04