AI Implementation feature(886): Admin Area General Settings and Categories 1.04 (#26)

This commit was merged in pull request #26.
This commit is contained in:
2026-07-22 13:41:09 +00:00
parent b2f0a4736d
commit 98fee8f7ee
10 changed files with 332 additions and 96 deletions
@@ -1,21 +1,21 @@
import { Injectable, signal, computed } from '@angular/core';
import {
applyThemeToCss,
BootstrapPayload,
PasswordPolicyInfo,
Theme,
} from './bootstrap.types';
export interface PasswordPolicyInfo {
minLength: number;
requireMixed: boolean;
description: string;
}
export interface BootstrapPayload {
initialized: boolean;
pageTitle: string;
logo: string;
welcomeMarkdown: string;
theme: any;
defaultChallengeIp: string;
registrationsEnabled: boolean;
passwordPolicy: PasswordPolicyInfo;
}
export {
applyThemeToCss,
clearThemeCss,
THEME_CSS_PROPERTIES,
Theme,
ThemeTokens,
ThemeCssTarget,
BootstrapPayload,
PasswordPolicyInfo,
} from './bootstrap.types';
const DEFAULT_POLICY: PasswordPolicyInfo = {
minLength: 12,
@@ -42,11 +42,7 @@ export class BootstrapService {
private async _load(): Promise<BootstrapPayload | null> {
try {
const res = await fetch('/api/v1/bootstrap', { credentials: 'include' });
const data = (await res.json()) as BootstrapPayload;
this.payload.set(data);
this.initialized.set(data.initialized);
this.applyTheme(data.theme);
const data = await this.fetchAndApply('/api/v1/bootstrap');
return data;
} catch (e) {
console.error('Bootstrap load failed', e);
@@ -61,25 +57,28 @@ export class BootstrapService {
return this.loadPromise;
}
async refresh(): Promise<BootstrapPayload> {
const data = await this.fetchAndApply('/api/v1/bootstrap');
return data;
}
markInitialized(): void {
this.initialized.set(true);
}
private applyTheme(theme: any): void {
if (!theme?.tokens) return;
const root = document.documentElement.style;
const t = theme.tokens;
root.setProperty('--color-primary', t.primary);
root.setProperty('--color-secondary', t.secondary);
root.setProperty('--color-accent', t.accent);
root.setProperty('--color-surface', t.surface);
root.setProperty('--color-text', t.text);
root.setProperty('--color-success', t.success);
root.setProperty('--color-warning', t.warning);
root.setProperty('--color-danger', t.danger);
root.setProperty('--font-family', t.fontFamily);
root.setProperty('--radius-sm', t.radii.sm);
root.setProperty('--radius-md', t.radii.md);
root.setProperty('--radius-lg', t.radii.lg);
applyTheme(theme: Theme | null | undefined): void {
applyThemeToCss(theme);
}
private async fetchAndApply(url: string): Promise<BootstrapPayload> {
const res = await fetch(url, { credentials: 'include' });
if (!res.ok) {
throw new Error(`Bootstrap request failed: ${res.status}`);
}
const data = (await res.json()) as BootstrapPayload;
this.payload.set(data);
this.initialized.set(data.initialized);
this.applyTheme(data.theme);
return data;
}
}
@@ -0,0 +1,77 @@
export interface PasswordPolicyInfo {
minLength: number;
requireMixed: boolean;
description: string;
}
export interface ThemeTokens {
primary: string;
secondary: string;
accent: string;
surface: string;
text: string;
success: string;
warning: string;
danger: string;
fontFamily: string;
radii: { sm: string; md: string; lg: string };
spacingScale: number[];
}
export interface Theme {
id: string;
name: string;
tokens: ThemeTokens;
}
export interface BootstrapPayload {
initialized: boolean;
pageTitle: string;
logo: string;
welcomeMarkdown: string;
theme: Theme;
defaultChallengeIp: string;
registrationsEnabled: boolean;
passwordPolicy: PasswordPolicyInfo;
}
export type ThemeCssTarget = Pick<CSSStyleDeclaration, 'setProperty' | 'removeProperty'>;
export const THEME_CSS_PROPERTIES = [
'--color-primary',
'--color-secondary',
'--color-accent',
'--color-surface',
'--color-text',
'--color-success',
'--color-warning',
'--color-danger',
'--font-family',
'--radius-sm',
'--radius-md',
'--radius-lg',
] as const;
export function applyThemeToCss(theme: Theme | null | undefined, root: ThemeCssTarget = document.documentElement.style): boolean {
if (!theme?.tokens) return false;
const t = theme.tokens;
root.setProperty('--color-primary', t.primary);
root.setProperty('--color-secondary', t.secondary);
root.setProperty('--color-accent', t.accent);
root.setProperty('--color-surface', t.surface);
root.setProperty('--color-text', t.text);
root.setProperty('--color-success', t.success);
root.setProperty('--color-warning', t.warning);
root.setProperty('--color-danger', t.danger);
root.setProperty('--font-family', t.fontFamily);
root.setProperty('--radius-sm', t.radii.sm);
root.setProperty('--radius-md', t.radii.md);
root.setProperty('--radius-lg', t.radii.lg);
return true;
}
export function clearThemeCss(root: ThemeCssTarget = document.documentElement.style): void {
for (const prop of THEME_CSS_PROPERTIES) {
root.removeProperty(prop);
}
}