mirror of
https://github.com/layer-systems/website.git
synced 2026-09-13 14:14:07 +02:00
* new web os frontend * add docs * Add CNAME and restore NIP-05 nostr.json for GitHub Pages The Pages custom domain (layer.systems) is only stored in repo settings; a CNAME file in the build output makes it survive Pages reconfiguration. Restore public/.well-known/nostr.json, which this branch had dropped — removing it would break the existing NIP-05 identifiers on layer.systems. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014YtQoCzkP7Bo8nruhxojPi * Ignore eslint and tsc build caches Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014YtQoCzkP7Bo8nruhxojPi * Rebrand page title and metadata to LAYER.systems The site ships on layer.systems, so the document title, meta and OG description, and the web manifest now carry that name instead of "Nostr OS". OsShell sets the title at runtime, so it is updated too — otherwise the tab would fall back to the old branding after hydration. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014YtQoCzkP7Bo8nruhxojPi * Rename remaining visible "Nostr OS" strings to LAYER.systems Covers the About window heading, the mobile shell header and the app icon's aria-label, so the visible branding matches the page title. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014YtQoCzkP7Bo8nruhxojPi --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
119 lines
3.1 KiB
TypeScript
119 lines
3.1 KiB
TypeScript
import { ReactNode, useEffect } from 'react';
|
|
import { z } from 'zod';
|
|
import { useLocalStorage } from '@/hooks/useLocalStorage';
|
|
import { AppContext, type AppConfig, type AppContextType, type Theme, type RelayMetadata, type BlossomServerMetadata } from '@/contexts/AppContext';
|
|
|
|
interface AppProviderProps {
|
|
children: ReactNode;
|
|
/** Application storage key */
|
|
storageKey: string;
|
|
/** Default app configuration */
|
|
defaultConfig: AppConfig;
|
|
}
|
|
|
|
// Zod schema for RelayMetadata validation
|
|
const RelayMetadataSchema = z.object({
|
|
relays: z.array(z.object({
|
|
url: z.url(),
|
|
read: z.boolean(),
|
|
write: z.boolean(),
|
|
})),
|
|
updatedAt: z.number(),
|
|
}) satisfies z.ZodType<RelayMetadata>;
|
|
|
|
// Zod schema for BlossomServerMetadata validation
|
|
const BlossomServerMetadataSchema = z.object({
|
|
servers: z.array(z.url()),
|
|
updatedAt: z.number(),
|
|
}) satisfies z.ZodType<BlossomServerMetadata>;
|
|
|
|
// Zod schema for AppConfig validation
|
|
const AppConfigSchema = z.object({
|
|
theme: z.enum(['dark', 'light', 'system']),
|
|
relayMetadata: RelayMetadataSchema,
|
|
blossomServerMetadata: BlossomServerMetadataSchema,
|
|
useAppBlossomServers: z.boolean(),
|
|
}) satisfies z.ZodType<AppConfig>;
|
|
|
|
export function AppProvider(props: AppProviderProps) {
|
|
const {
|
|
children,
|
|
storageKey,
|
|
defaultConfig,
|
|
} = props;
|
|
|
|
// App configuration state with localStorage persistence
|
|
const [rawConfig, setConfig] = useLocalStorage<Partial<AppConfig>>(
|
|
storageKey,
|
|
{},
|
|
{
|
|
serialize: JSON.stringify,
|
|
deserialize: (value: string) => {
|
|
const parsed = JSON.parse(value);
|
|
return AppConfigSchema.partial().parse(parsed);
|
|
}
|
|
}
|
|
);
|
|
|
|
// Generic config updater with callback pattern
|
|
const updateConfig = (updater: (currentConfig: Partial<AppConfig>) => Partial<AppConfig>) => {
|
|
setConfig(updater);
|
|
};
|
|
|
|
const config = { ...defaultConfig, ...rawConfig };
|
|
|
|
const appContextValue: AppContextType = {
|
|
config,
|
|
updateConfig,
|
|
};
|
|
|
|
// Apply theme effects to document
|
|
useApplyTheme(config.theme);
|
|
|
|
return (
|
|
<AppContext.Provider value={appContextValue}>
|
|
{children}
|
|
</AppContext.Provider>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Hook to apply theme changes to the document root
|
|
*/
|
|
function useApplyTheme(theme: Theme) {
|
|
useEffect(() => {
|
|
const root = window.document.documentElement;
|
|
|
|
root.classList.remove('light', 'dark');
|
|
|
|
if (theme === 'system') {
|
|
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)')
|
|
.matches
|
|
? 'dark'
|
|
: 'light';
|
|
|
|
root.classList.add(systemTheme);
|
|
return;
|
|
}
|
|
|
|
root.classList.add(theme);
|
|
}, [theme]);
|
|
|
|
// Handle system theme changes when theme is set to "system"
|
|
useEffect(() => {
|
|
if (theme !== 'system') return;
|
|
|
|
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
|
|
const handleChange = () => {
|
|
const root = window.document.documentElement;
|
|
root.classList.remove('light', 'dark');
|
|
|
|
const systemTheme = mediaQuery.matches ? 'dark' : 'light';
|
|
root.classList.add(systemTheme);
|
|
};
|
|
|
|
mediaQuery.addEventListener('change', handleChange);
|
|
return () => mediaQuery.removeEventListener('change', handleChange);
|
|
}, [theme]);
|
|
} |