Initial commit: ArchiTools modular dashboard platform
Complete Next.js 16 application with 13 fully implemented modules: Email Signature, Word XML Generator, Registratura, Dashboard, Tag Manager, IT Inventory, Address Book, Password Vault, Mini Utilities, Prompt Generator, Digital Signatures, Word Templates, and AI Chat. Includes core platform systems (module registry, feature flags, storage abstraction, i18n, theming, auth stub, tagging), 16 technical documentation files, Docker deployment config, and legacy HTML tool reference. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
'use client';
|
||||
|
||||
import { useFeatureFlag } from './use-feature-flag';
|
||||
|
||||
interface FeatureGateProps {
|
||||
flag: string;
|
||||
children: React.ReactNode;
|
||||
fallback?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function FeatureGate({ flag, children, fallback = null }: FeatureGateProps) {
|
||||
const enabled = useFeatureFlag(flag);
|
||||
return enabled ? <>{children}</> : <>{fallback}</>;
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
'use client';
|
||||
|
||||
import { createContext, useContext, useMemo, useState, useCallback } from 'react';
|
||||
import type { FeatureFlag } from './types';
|
||||
import { resolveAllFlags, loadRuntimeOverrides, saveRuntimeOverride, clearRuntimeOverrides } from './flag-service';
|
||||
|
||||
interface FeatureFlagContextValue {
|
||||
flags: Record<string, boolean>;
|
||||
isEnabled: (key: string) => boolean;
|
||||
setOverride: (key: string, value: boolean) => void;
|
||||
clearOverrides: () => void;
|
||||
}
|
||||
|
||||
const FeatureFlagContext = createContext<FeatureFlagContextValue | null>(null);
|
||||
|
||||
interface FeatureFlagProviderProps {
|
||||
flagDefinitions: FeatureFlag[];
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function FeatureFlagProvider({ flagDefinitions, children }: FeatureFlagProviderProps) {
|
||||
const [runtimeOverrides, setRuntimeOverrides] = useState<Record<string, boolean>>(() => {
|
||||
if (typeof window === 'undefined') return {};
|
||||
return loadRuntimeOverrides();
|
||||
});
|
||||
|
||||
const envOverrides = useMemo(() => {
|
||||
const env: Record<string, string> = {};
|
||||
if (typeof window !== 'undefined') {
|
||||
// Collect NEXT_PUBLIC_FLAG_* env vars injected at build time
|
||||
for (const [key, value] of Object.entries(process.env)) {
|
||||
if (key.startsWith('NEXT_PUBLIC_FLAG_') && value) {
|
||||
env[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return env;
|
||||
}, []);
|
||||
|
||||
const flags = useMemo(
|
||||
() => resolveAllFlags(flagDefinitions, envOverrides, runtimeOverrides),
|
||||
[flagDefinitions, envOverrides, runtimeOverrides]
|
||||
);
|
||||
|
||||
const isEnabled = useCallback((key: string) => flags[key] ?? false, [flags]);
|
||||
|
||||
const setOverride = useCallback(
|
||||
(key: string, value: boolean) => {
|
||||
saveRuntimeOverride(key, value);
|
||||
setRuntimeOverrides((prev) => ({ ...prev, [key]: value }));
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleClearOverrides = useCallback(() => {
|
||||
clearRuntimeOverrides();
|
||||
setRuntimeOverrides({});
|
||||
}, []);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({ flags, isEnabled, setOverride, clearOverrides: handleClearOverrides }),
|
||||
[flags, isEnabled, setOverride, handleClearOverrides]
|
||||
);
|
||||
|
||||
return (
|
||||
<FeatureFlagContext.Provider value={value}>
|
||||
{children}
|
||||
</FeatureFlagContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useFeatureFlags(): FeatureFlagContextValue {
|
||||
const ctx = useContext(FeatureFlagContext);
|
||||
if (!ctx) throw new Error('useFeatureFlags must be used within FeatureFlagProvider');
|
||||
return ctx;
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import type { FeatureFlag } from './types';
|
||||
|
||||
const RUNTIME_OVERRIDE_KEY = 'architools:flag-overrides';
|
||||
|
||||
export function resolveFlag(
|
||||
flag: FeatureFlag,
|
||||
envOverrides: Record<string, string>,
|
||||
runtimeOverrides: Record<string, boolean>
|
||||
): boolean {
|
||||
// 1. Check environment variable override
|
||||
const envKey = `NEXT_PUBLIC_FLAG_${flag.key.toUpperCase().replace(/\./g, '_')}`;
|
||||
const envVal = envOverrides[envKey];
|
||||
if (envVal !== undefined) {
|
||||
return envVal === 'true';
|
||||
}
|
||||
|
||||
// 2. Check runtime override
|
||||
if (flag.overridable && runtimeOverrides[flag.key] !== undefined) {
|
||||
return runtimeOverrides[flag.key]!;
|
||||
}
|
||||
|
||||
// 3. Fall back to default
|
||||
return flag.enabled;
|
||||
}
|
||||
|
||||
export function resolveAllFlags(
|
||||
flags: FeatureFlag[],
|
||||
envOverrides: Record<string, string>,
|
||||
runtimeOverrides: Record<string, boolean>
|
||||
): Record<string, boolean> {
|
||||
const resolved: Record<string, boolean> = {};
|
||||
|
||||
// First pass: resolve without dependency checks
|
||||
for (const flag of flags) {
|
||||
resolved[flag.key] = resolveFlag(flag, envOverrides, runtimeOverrides);
|
||||
}
|
||||
|
||||
// Second pass: enforce dependencies
|
||||
for (const flag of flags) {
|
||||
if (resolved[flag.key] && flag.dependencies) {
|
||||
for (const dep of flag.dependencies) {
|
||||
if (!resolved[dep]) {
|
||||
resolved[flag.key] = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
export function loadRuntimeOverrides(): Record<string, boolean> {
|
||||
if (typeof window === 'undefined') return {};
|
||||
try {
|
||||
const stored = window.localStorage.getItem(RUNTIME_OVERRIDE_KEY);
|
||||
if (stored) return JSON.parse(stored);
|
||||
} catch {
|
||||
// Ignore parse errors
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
export function saveRuntimeOverride(key: string, value: boolean): void {
|
||||
if (typeof window === 'undefined') return;
|
||||
const overrides = loadRuntimeOverrides();
|
||||
overrides[key] = value;
|
||||
window.localStorage.setItem(RUNTIME_OVERRIDE_KEY, JSON.stringify(overrides));
|
||||
}
|
||||
|
||||
export function clearRuntimeOverrides(): void {
|
||||
if (typeof window === 'undefined') return;
|
||||
window.localStorage.removeItem(RUNTIME_OVERRIDE_KEY);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export type { FeatureFlag, FlagCategory } from './types';
|
||||
export { FeatureFlagProvider, useFeatureFlags } from './flag-provider';
|
||||
export { useFeatureFlag } from './use-feature-flag';
|
||||
export { FeatureGate } from './feature-gate';
|
||||
@@ -0,0 +1,11 @@
|
||||
export type FlagCategory = 'module' | 'experimental' | 'system';
|
||||
|
||||
export interface FeatureFlag {
|
||||
key: string;
|
||||
enabled: boolean;
|
||||
label: string;
|
||||
description: string;
|
||||
category: FlagCategory;
|
||||
dependencies?: string[];
|
||||
overridable: boolean;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { useFeatureFlags } from './flag-provider';
|
||||
|
||||
export function useFeatureFlag(key: string): boolean {
|
||||
const { isEnabled } = useFeatureFlags();
|
||||
return isEnabled(key);
|
||||
}
|
||||
Reference in New Issue
Block a user