@ff-admin/core (1.5.0)
Installation
@ff-admin:registry=npm install @ff-admin/core@1.5.0"@ff-admin/core": "1.5.0"About this package
ff-admin-core
Basis-UI-Funktionalität für ff-admin Anwendungen als wiederverwendbares npm-Package.
📋 Inhaltsverzeichnis
- Übersicht
- Was übernimmt das Package?
- Installation
- Konfiguration
- Verwendung
- Erweiterung
- Push-Benachrichtigungen
- CSS-Klassen & Theming
Übersicht
@ff-admin/core stellt Basis-UI-Komponenten, Layouts, Stores, Router, Enums und Helper für FF-Admin Anwendungen bereit. Das Package sorgt für eine konsistente UI-Struktur und wiederverwendbare Funktionalität in allen Anwendungen.
Was übernimmt das Package?
🖥️ Basis-UI-Inhalte
- Wiederverwendbare UI-Komponenten (z.B. Layouts, Tabs, Modals, Icons, Progress Bars)
- Stores für globale State-Verwaltung (Pinia)
- Router-Konfiguration und Guards
- Enum-Definitionen für Permissions, Module, Sections etc.
- Helper-Funktionen für UI und Kommunikation
- Socket.IO Client-Integration
- PWA-Unterstützung
- Push-Benachrichtigungen (Web Push mit VAPID)
- Konsistente Styles und globale CSS
Das Package stellt die Basis für alle FF-Admin Anwendungen bereit und sorgt für ein einheitliches UI und Verhalten.
Installation
npm install @ff-admin/core
Wichtig: in der viteConfig der Anwendung muss die Dependency Optimization für das package deaktiviert werden.
export default defineConfig({
plugins: [
vue(),
VitePWA({ /* deine config */ }), // falls pwa benötigt
// ... andere plugins
],
optimizeDeps: {
exclude: ['@ff-admin/core']
}
})
Peer Dependencies
Das Package benötigt folgende Peer Dependencies:
vue>= 3.0.0pinia>= 2.0.0vue-router>= 4.0.0
Verwendung & Erweiterung
Das Package wird typischerweise in den zentralen Dateien der Anwendung (z.B. core-setup.ts, main.ts, router/index.ts) eingebunden und konfiguriert. Die Erweiterung erfolgt über TypeScript-Interfaces, um projektspezifische Funktionalität und Enums zu ergänzen.
Einbindung und Initialisierung
- Importiere das Package und initialisiere zentrale Services (z.B. HTTP, Socket, Konfigurationswerte).
- Binde globale Komponenten, Stores und Router ein.
- Konfiguriere Namespaces, Enums und weitere projektspezifische Typen über Interface-Erweiterungen.
Beispielhafter Ablauf (vereinfacht):
// Importiere das Package und zentrale Services
import "@ff-admin/core";
import { configureSocketNamespace, HTTPManager, NavigationConfigVals, PackageConfigVals, SocketManager } from "@ff-admin/core";
// (Optional) Erweiterung von Typen für projektspezifische Enums und Namespaces
declare module "@ff-admin/core" {
interface PermissionSectionMap {
// Eigene Berechtigungs-Section hinzufügen
mySection: void;
}
interface PermissionModuleMap {
// Eigene Berechtigungs-Module hinzufügen
myModule: void;
anotherModule: void;
}
interface SettingTopicMap {
myApp: void;
}
interface SettingValueMapping {
"myApp.apiKey": string;
"myApp.timeout": number;
"myApp.baseUrl": string;
}
interface ModuleSettingTopicMap {
myModule: void;
}
interface ModuleSettingValueMapping {
"myModule.setting1": string;
"myModule.setting2": number;
"myModule.enabled": boolean;
}
interface SocketNamespaceMap {
myCustomNamespace: "/custom";
anotherNamespace: "/another";
}
}
// Konfigurationswerte setzen
PackageConfigVals.serverAddress = "...";
PackageConfigVals.devMode = true;
PackageConfigVals.clientVersion = ""; // Alternativ package JSON auslesen
// Initialisierung und Konfiguration
HTTPManager.init();
SocketManager.init();
configureSocketNamespace({
myCustomNamespace: "/custom",
anotherNamespace: "/another",
});
// Angabe der Navigationsstruktur
NavigationConfigVals.defaultActiveNavigation = "configuration"
NavigationConfigVals.topLevel = [/*...*/]
// direktes setzen der Struktur
NavigationConfigVals.navigation = {/*...*/}
// oder einzelne Keys setzen
NavigationConfigVals.setNavigationKey("configuration", {/*...*/})
Werte wie im Backend müssen nur für den SocketNamespace definiert werden. Alle anderen Konfigurationswerte werden über den Server bezogen.
Integration in die App
- Binde das Package in die Hauptdatei deiner App ein (z.B. in
main.ts). - Verwende die bereitgestellten Komponenten, Stores, Router und Helper in deiner Anwendung.
- Nutze die Basisrouten und Guards aus dem Package und ergänze sie bei Bedarf um eigene Routen.
Beispiel (allgemein):
import "./core-setup";
import { createApp } from "vue";
import { createPinia } from "pinia";
import router from "./router";
import App from "./App.vue";
const app = createApp(App);
app.use(createPinia());
app.use(router);
// Weitere globale Properties oder Plugins
PackageConfigVals.router = router;
app.mount("#app");
Geplante Benachrichtigungen (Scheduled Notifications)
Das Package stellt eine vollständige UI bereit, über die Benutzer ihre geplanten Benachrichtigungen konfigurieren können. Die verfügbaren Notification-Typen werden dynamisch vom Server geladen (registriert via registerScheduledNotificationGenerator im Backend).
Route
Die Seite ist unter /account/notification-schedule verfügbar (Route-Name: account-notification_schedule).
Store
Der useNotificationScheduleStore (Pinia) verwaltet den State und die API-Kommunikation:
import { useNotificationScheduleStore } from "@ff-admin/core";
const store = useNotificationScheduleStore();
// Schedules und verfügbare Typen laden
store.fetchSchedules();
store.fetchAvailable();
// Schedule erstellen/aktualisieren
await store.upsertSchedule({
reportKey: "unit/inspection-remind",
weekdays: [1, 2, 3, 4, 5], // Mo-Fr
time: "08:00",
channels: ["inApp", "email"],
active: true,
});
// Schedule löschen
await store.deleteSchedule("unit/inspection-remind");
Konfigurationsoptionen für Benutzer
- Wochentage: Auswahl von Sonntag bis Samstag
- Uhrzeit: Konfigurierbare Sendezeit (HH:MM)
- Kanäle: In-App, E-Mail, Web-Push (Mehrfachauswahl)
- Aktiv/Inaktiv: Einzelne Schedules ein-/ausschalten
Tägliche Benachrichtigungen
Benachrichtigungstypen, die vom Backend mit daily: true registriert wurden, werden im Frontend ohne Wochentag-Auswahl dargestellt. Der Benutzer kann bei diesen nur die Uhrzeit und die Kanäle einstellen. Die Wochentage werden automatisch auf alle Tage gesetzt, da der Inhalt tagesaktuell ist und bei Auslassung verloren ginge.
Push-Benachrichtigungen
Das Package stellt Web-Push-Funktionalität bereit (VAPID-basiert). Die Integration erfordert:
- Einen Push-Service-Worker im
public/-Verzeichnis der Anwendung - Einbindung des SW in die PWA- oder App-Konfiguration
- Verwendung der bereitgestellten Composables für Subscribe/Unsubscribe
Service Worker einrichten
Das Package liefert ein Vite-Plugin mit, das den Push-Service-Worker automatisch im Dev-Server bereitstellt und beim Build in den Output kopiert. Kein manuelles Kopieren nötig.
Variante A: Vite-Plugin (empfohlen)
// vite.config.ts
import { vitePushServiceWorker } from "@ff-admin/core/vite-push-sw";
export default defineConfig({
plugins: [
vitePushServiceWorker(),
// ... andere Plugins
],
});
Das Plugin:
- Stellt
/sw-push.jsim Dev-Server bereit (middleware) - Kopiert
sw-push.jsautomatisch in den Build-Output
Kombination mit vite-plugin-pwa (generateSW-Modus)
Wenn die App vite-plugin-pwa im Standard-Modus (generateSW) nutzt, wird der Push-Handler über importScripts in den generierten Service Worker eingebunden:
// vite.config.ts
import { vitePushServiceWorker } from "@ff-admin/core/vite-push-sw";
export default defineConfig({
plugins: [
vitePushServiceWorker(),
VitePWA({
workbox: {
importScripts: ["/sw-push.js"],
// ... weitere workbox-Konfiguration
},
}),
],
});
Der generierte Workbox-SW führt importScripts('/sw-push.js') aus, wodurch die Push-Event-Listener aktiv werden.
Variante B: Manuell (ohne Plugin)
Alternativ kann sw-push.js direkt aus dem Package in das public/-Verzeichnis kopiert werden:
node_modules/@ff-admin/core/sw-push.js → public/sw-push.js
Integration ohne PWA
Falls die App kein vite-plugin-pwa nutzt, gibt es keinen automatisch generierten Workbox-SW. In diesem Fall wird sw-push.js als eigenständiger Service Worker registriert.
1. Vite-Plugin einbinden (stellt /sw-push.js bereit)
// vite.config.ts
import { vitePushServiceWorker } from "@ff-admin/core/vite-push-sw";
export default defineConfig({
plugins: [
vitePushServiceWorker(),
// ... andere Plugins (KEIN VitePWA nötig)
],
});
2. Service Worker beim App-Start registrieren
// main.ts
import { registerPushServiceWorker } from "@ff-admin/core";
// Registriert /sw-push.js als eigenständigen Service Worker
await registerPushServiceWorker();
Die Funktion prüft, ob bereits ein Service Worker aktiv ist (z.B. von einer PWA) und registriert nur dann einen neuen, wenn keiner vorhanden ist.
Hinweise
- Der SW wird unter dem Root-Scope (
/) registriert — damit ist er für die gesamte App aktiv registerPushServiceWorker()sollte vor dem erstensubscribe()-Aufruf aufgerufen werden (z.B. inmain.ts)- Das Composable
usePushNotification()nutztnavigator.serviceWorker.ready, das auf den registrierten SW wartet - Optional kann ein anderer Dateiname übergeben werden:
registerPushServiceWorker("/my-sw.js")
Push-Composable verwenden
import { usePushNotification } from "@ff-admin/core";
const { isSupported, subscribe, unsubscribe, resubscribe } = usePushNotification();
// Prüfen ob Push verfügbar ist
if (isSupported) {
// Benutzer für Push registrieren (fragt Notification-Permission ab)
const success = await subscribe();
// Abmelden
await unsubscribe();
// Bestehende Subscription verlängern (z.B. nach Token-Refresh)
await resubscribe();
}
Push-Subscription-Store
import { usePushSubscriptionStore } from "@ff-admin/core";
const store = usePushSubscriptionStore();
// Alle Subscriptions des Users laden
await store.fetchSubscriptions();
// Eine Subscription remote deaktivieren (z.B. von einem anderen Gerät)
await store.deactivateSubscription(endpoint);
Automatische Integration
Das Package integriert Push automatisch an folgenden Stellen:
| Stelle | Aktion |
|---|---|
| Login (Passwort/TOTP + Passkey) | subscribe() — registriert das Gerät |
| Token-Refresh | resubscribe() — verlängert die Laufzeit |
| Logout | unsubscribe() — meldet das Gerät ab |
Account-View
Die Route push-subscriptions (Name: account-push_subscriptions) zeigt eine Übersicht aller registrierten Push-Subscriptions mit:
- Geräte-Icon (Mobile/Tablet/Desktop)
- Aktiv/Inaktiv-Status
- Button zum Deaktivieren einzelner Subscriptions
- "Push aktivieren"-Button zum Registrieren des aktuellen Geräts
Backend-Voraussetzungen
Der Server muss folgende Umgebungsvariablen bereitstellen:
| Variable | Beschreibung |
|---|---|
VAPID_PUBLIC_KEY |
VAPID Public Key (Base64url) |
VAPID_PRIVATE_KEY |
VAPID Private Key (Base64url) |
VAPID_SUBJECT |
Kontakt-URL oder mailto: (z.B. mailto:admin@example.com) |
Werden keine Keys angegeben, generiert der Server beim Start ein Schlüsselpaar und gibt es in der Konsole aus.
CSS-Klassen & Theming
Einbindung in der Anwendung
Damit Styling und Tailwind-Utilities korrekt funktionieren, sind drei Schritte notwendig:
1. vite.config.ts — Tailwind-Plugin einbinden
import tailwindcss from "@tailwindcss/vite";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [
tailwindcss(),
// ... weitere Plugins
],
optimizeDeps: {
exclude: ["@ff-admin/core"],
},
});
2. src/main.css — CSS in der richtigen Reihenfolge importieren
/* 1. Tailwind selbst */
@import "tailwindcss";
/* 2. Rohe Theme-Konfiguration des Package, damit der App-Build
die Tokens und Variants kennt */
@import "@ff-admin/core/theme.css";
/* 3. Fertig kompilierte Package-Styles (Komponenten, Base-Layer) */
@import "@ff-admin/core/style.css";
/* 4. Scan-Pfad für Package-Templates, damit Tailwind deren
verwendete Klassen generiert */
@source "../node_modules/@ff-admin/core/dist/**/*.{js,vue}";
3. src/main.ts — CSS-Datei laden
import "./main.css";
Was die drei Dateien leisten
| Import | Typ | Zweck |
|---|---|---|
@ff-admin/core/theme.css |
Roh (unkompiliert) | Gibt dem Tailwind-Compiler der App die Custom-Tokens (--color-primary etc.) und die hover-Variant ohne @media (hover: hover) |
tailwindcss |
Tailwind | Generiert alle Utility-Klassen für die App inkl. der Package-Tokens |
@ff-admin/core/style.css |
Kompiliert | Fertige Styles der Package-Komponenten (Base-Layer, Component-Klassen) |
Theming — CSS-Variablen
Das gesamte Farbschema leitet sich aus einem einzigen Hue-Wert (0–360, HSL-Farbkreis) ab. Der Wert wird über die Admin-Einstellungen gesetzt und per Pinia-Store zur Laufzeit auf document.documentElement angewendet.
| Variable | Berechnung | Verwendung |
|---|---|---|
--hue |
0–360 (Standard: 4 = Rot) |
Einziger konfigurierbarer Wert |
--primary |
hsl(var(--hue) 100% 30%) |
Hauptfarbe (Buttons, Rahmen, Header) |
--primary-hover |
hsl(var(--hue) 100% 25%) |
Hover-Zustand von Primary |
--primary-subtle |
hsl(var(--hue) 100% 93%) |
Heller Hintergrund (aktive Tabs, Hover-Flächen) |
--accent |
hsl(var(--hue) 80% 40%); |
Akzent |
--success |
#7ac142 (fix) |
Erfolg |
--failed |
#ff0000 (fix) |
Fehler |
Alle Variablen sind über Tailwind als Farbklassen verfügbar (z. B. bg-primary, text-primary-subtle, border-primary-hover).
Navigation
.nav-tab
Basis-Klasse für einen Tab in einer Tab-Leiste (z. B. Passwort / TOTP). Wird mit .nav-tab-active oder .nav-tab-inactive kombiniert.
<p class="w-1/2 nav-tab" :class="tab === 'pw' ? 'nav-tab-active' : 'nav-tab-inactive'" @click="tab = 'pw'">
Passwort
</p>
| Klasse | Beschreibung |
|---|---|
.nav-tab |
Gemeinsame Basis: flex items-center justify-center, Padding, Schriftgröße |
.nav-tab-active |
Aktiver Tab: bg-primary-subtle, Unterstrich in Primary-Farbe |
.nav-tab-inactive |
Inaktiver Tab: Hover bg-primary-subtle |
.nav-toplevel-active / .nav-toplevel-inactive
Für Top-Level-Navigationslinks in der Hauptnavigation (Desktop: ausgefüllter Primary-Hintergrund, Mobil: nur Textfarbe).
.sidebar-link / .sidebar-link-active
Für Einträge in der Seitenleiste. .sidebar-link-active fügt einen linken Primary-Balken und subtilen Hintergrund hinzu.
<div class="sidebar-link" :class="isActive ? 'sidebar-link-active' : ''">Eintrag</div>
Karten & Panels
.card
Weißes, abgerundetes Panel mit horizontalen Trennlinien (divide-y-2 divide-gray-300). Eignet sich für mehrteilige Bereiche.
<div class="card">
<div class="card-header">…</div>
<div class="p-4">Inhalt</div>
</div>
.card-plain
Wie .card, aber ohne Trennlinien. Für schlichte weiße Panels.
.card-header
Kopfzeile innerhalb einer .card: horizontale Flex-Zeile mit justify-between, Innenabstand.
Listen-Einträge
Alle Listeneinträge (Benutzer, Rollen, Web-API-Keys, Backups, Sitzungen …) folgen diesem Schema:
<div class="list-card">
<div class="list-card-header">
<span>Titel</span>
<span>Aktion</span>
</div>
<div class="list-card-body">
<div class="list-card-row">
<span class="list-card-row-label">Label</span>
<span class="list-card-row-value">Wert</span>
</div>
</div>
</div>
| Klasse | Beschreibung |
|---|---|
.list-card |
Äußerer Rahmen mit Primary-Border, abgerundet |
.list-card-header |
Primary-Hintergrund, weißer Text, justify-between |
.list-card-body |
Innenbereich mit Padding und vertikalem Gap |
.list-card-row |
Horizontale Zeile für Label + Wert |
.list-card-row-label |
Feste Mindestbreite (min-w-16) für linksbündige Labels |
.list-card-row-value |
Wächst flexibel, schneidet zu langen Text ab |
Modals
Alle Modals verwenden eine einheitliche Struktur:
<div class="modal-wrapper">
<div class="modal-title">
<p>Modal-Titel</p>
</div>
<!-- Formularfelder -->
<div class="modal-footer">
<div class="modal-footer-inner">
<button>Abbrechen</button>
<div class="modal-submit-row">
<button primary>Bestätigen</button>
</div>
</div>
</div>
</div>
| Klasse | Beschreibung |
|---|---|
.modal-wrapper |
Max-Breite md:max-w-md, volle Breite |
.modal-title |
Zentrierte Flex-Spalte; > p bekommt text-xl font-medium |
.modal-footer |
Rechtsbündige Fußzeile |
.modal-footer-inner |
Horizontale Zeile mit Gap und vertikalem Padding |
.modal-submit-row |
Flex-Zeile für mehrstufige Submit-Aktionen (z. B. Bestätigung + Spinner) |
Weitere Utility-Klassen
.badge
Kleines Tag/Chip-Pill für Labels, Rollen, Kanäle o. Ä.
<span class="badge">Admin</span>
Stil: px-2 py-0.5 bg-gray-200 border border-gray-400 rounded-full text-xs
.menu-item
Eintrag in einem Kontext- oder Dropdown-Menü. Hover verwendet bg-primary-subtle.
<div class="menu-item" @click="copy">
<CopyIcon />
<span>Kopieren</span>
</div>
.pagination-btn
Einzelne Zelle in einer Pagination-Leiste. first: und last: runden die Enden ab.
<ul class="flex">
<li class="pagination-btn">«</li>
<li class="pagination-btn">1</li>
<li class="pagination-btn">»</li>
</ul>
Öffentliche Seiten (Login, Setup, Reset, Invite)
Für vollseitige zentrierte Seiten ohne Sidebar:
<div class="public-page">
<div class="public-page-inner">
<div class="public-page-header">
<img src="…" alt="Logo" class="h-20 w-auto" />
<h1 class="public-page-title">Seitentitel</h1>
</div>
<!-- Formular oder Inhalt -->
</div>
</div>
| Klasse | Beschreibung |
|---|---|
.public-page |
grow flex items-center justify-center, responsives Padding |
.public-page-inner |
max-w-md w-full space-y-8 |
.public-page-header |
Flex-Spalte, zentriert, gap-4 |
.public-page-title |
text-4xl font-extrabold text-gray-900 text-center |
Buttons
Buttons bekommen ihre Variante über HTML-Attribute (kein separates CSS von außen nötig):
<button primary>Primär-Aktion</button>
<button primary-outline>Sekundär-Aktion</button>
<button disabled>Deaktiviert</button>
| Attribut | Stil |
|---|---|
primary |
Ausgefüllter Primary-Hintergrund, weißer Text, Hover dunkler |
primary-outline |
Primary-Rahmen, transparenter Hintergrund; Hover füllt sich mit Primary |
disabled / :disabled |
opacity-75, kein Klick möglich |
Lizenz
AGPL-3.0-only
Author
JK Effects
Repository
Dependencies
Dependencies
| ID | Version |
|---|---|
| @headlessui/vue | ^1.7.23 |
| @heroicons/vue | ^2.2.0 |
| @simplewebauthn/browser | ^13.3.0 |
| @tailwindcss/vite | ^4.3.0 |
| @vuepic/vue-datepicker | ^13.0.0 |
| @vueuse/core | ^14.3.0 |
| axios | ^1.16.1 |
| event-source-polyfill | ^1.0.31 |
| jwt-decode | ^4.0.0 |
| lodash.clonedeep | ^4.5.0 |
| lodash.difference | ^4.5.0 |
| lodash.differencewith | ^4.5.0 |
| lodash.isequal | ^4.5.0 |
| ms | ^2.1.3 |
| qrcode | ^1.5.4 |
| qs | ^6.15.2 |
| socket.io-client | ^4.8.3 |
| ua-parser-js | ^2.0.10 |
| uuid | ^14.0.0 |
| vue-qrcode-reader | ^5.7.3 |
Development dependencies
| ID | Version |
|---|---|
| @rushstack/eslint-patch | ^1.16.1 |
| @tailwindcss/postcss | ^4.3.0 |
| @tsconfig/node20 | ^20.1.9 |
| @types/eslint | ~9.6.1 |
| @types/event-source-polyfill | ^1.0.5 |
| @types/lodash.clonedeep | ^4.5.9 |
| @types/lodash.difference | ^4.5.9 |
| @types/lodash.differencewith | ^4.5.9 |
| @types/lodash.isequal | ^4.5.8 |
| @types/ms | ^2.1.0 |
| @types/node | ^25.9.1 |
| @types/nprogress | ^0.2.3 |
| @types/qrcode | ^1.5.6 |
| @types/qs | ^6.15.1 |
| @types/uuid | ^11.0.0 |
| @vitejs/plugin-vue | ^6.0.7 |
| @vue/eslint-config-typescript | ^14.7.0 |
| @vue/tsconfig | ^0.9.1 |
| eslint | ^10.4.0 |
| eslint-plugin-vue | ^10.9.1 |
| npm-run-all2 | ^9.0.1 |
| tailwindcss | ^4.3.0 |
| typescript | ^6.0.3 |
| vite | ^7.3.2 |
| vite-plugin-dts | ^4.5.4 |
| vite-plugin-pwa | ^1.3.0 |
| vue-tsc | ^3.3.2 |
Peer dependencies
| ID | Version |
|---|---|
| nprogress | >=0.2.0 |
| pinia | >=3.0.4 |
| quill | ^2.0.3 |
| quill-cursors | ^4.0.4 |
| vite | >=7.3.0 |
| vue | >=3.5.27 |
| vue-router | >=5.0.2 |
