Make Cobble a local install: setup flow, bots-first admin, settings
Local, password-only auth: - No public pages; everything outside /admin and /auth redirects in - First run (no users) sends every page to /auth/signup, which creates the super admin (user #1) once and then locks - Login is email/username + password; removed the emailed-code flow, /auth/sso and email password reset - Google login kept for existing accounts only, never creates them - seed-super-admin now requires SEED_SUPER_ADMIN_PASSWORD Bots-first admin: - /admin and closed sections (people, logs, backups) redirect to /admin/bots; only bots and settings are live (AdminSections) - Removed the old admin layout; every /admin page uses the bots rail - Coordinator bot created at signup/seed and by /admin/bots (ensureCoordinatorBot), which redirects to its chat - bots.system_prompt (markdown) with a default coordinator prompt - seed-bots reuses the existing coordinator Settings: - Sub pages general (default), profile, change-password, listed in the rail with a back link; unknown settings paths redirect to general - Redesigned settings UI: pane header, grouped rows, sticky save bar - General: System/Light/Dark theme, saved per device Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
import Button from "@moduletrace/twui/layout/Button";
|
||||
import Span from "@moduletrace/twui/layout/Span";
|
||||
import Stack from "@moduletrace/twui/layout/Stack";
|
||||
import { Check } from "lucide-react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import type { ThemePreferences } from "@/src/dict/theme-preferences-dict";
|
||||
|
||||
type Props = (typeof ThemePreferences)[number] & {
|
||||
selected: boolean;
|
||||
onSelect: () => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* One radio tile. Selected is a strong border plus a check, so it never
|
||||
* depends on color alone.
|
||||
*/
|
||||
export default function ThemeOption({
|
||||
title,
|
||||
description,
|
||||
Icon,
|
||||
selected,
|
||||
onSelect,
|
||||
}: Props) {
|
||||
return (
|
||||
<Button
|
||||
title={title}
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={selected}
|
||||
variant="ghost"
|
||||
color="gray"
|
||||
onClick={onSelect}
|
||||
className={twMerge(
|
||||
"relative w-full min-w-0 h-auto p-4! pr-10! justify-start! rounded-default text-left",
|
||||
"bg-surface dark:bg-surface hover:bg-sunken hover:dark:bg-sunken text-ink dark:text-ink",
|
||||
"border border-solid border-hairline transition-colors duration-[var(--m-fast)]",
|
||||
"focus-visible:ring-2 focus-visible:ring-ink focus-visible:ring-offset-2 focus-visible:ring-offset-paper",
|
||||
selected && "border-ink dark:border-ink bg-ink/5 dark:bg-ink/5",
|
||||
)}
|
||||
>
|
||||
<Stack className="w-full min-w-0 gap-3! items-start">
|
||||
<Icon size={20} aria-hidden />
|
||||
<Stack className="w-full min-w-0 gap-0! items-start">
|
||||
<Span className="text-sm font-bold leading-5 text-ink">
|
||||
{title}
|
||||
</Span>
|
||||
<Span className="text-[13px] leading-[18px] text-ink-secondary">
|
||||
{description}
|
||||
</Span>
|
||||
</Stack>
|
||||
</Stack>
|
||||
{selected ? (
|
||||
<Span className="absolute top-3 right-3 size-5 rounded-full bg-ink text-on-ink flex items-center justify-center">
|
||||
<Check size={12} strokeWidth={3} aria-hidden />
|
||||
</Span>
|
||||
) : null}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import Row from "@moduletrace/twui/layout/Row";
|
||||
import { ThemePreferences } from "@/src/dict/theme-preferences-dict";
|
||||
import useThemePreference from "@/src/hooks/use-theme-preference";
|
||||
import ThemeOption from "./theme-option";
|
||||
|
||||
/**
|
||||
* Applies on click; there is nothing to save.
|
||||
*/
|
||||
export default function ThemePicker() {
|
||||
const { preference, setPreference } = useThemePreference();
|
||||
|
||||
return (
|
||||
<Row
|
||||
role="radiogroup"
|
||||
aria-label="Theme"
|
||||
className="w-full grid grid-cols-1 sm:grid-cols-3 gap-3!"
|
||||
>
|
||||
{ThemePreferences.map((option) => (
|
||||
<ThemeOption
|
||||
key={option.value}
|
||||
{...option}
|
||||
selected={preference == option.value}
|
||||
onSelect={() => setPreference(option.value)}
|
||||
/>
|
||||
))}
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import SettingsGroup from "../../(partials)/settings-group";
|
||||
import ThemePicker from "../(partials)/theme-picker";
|
||||
|
||||
export default function AppearanceSection() {
|
||||
return (
|
||||
<SettingsGroup
|
||||
title="Theme"
|
||||
description="Saved on this device only, so each computer can have its own."
|
||||
>
|
||||
<ThemePicker />
|
||||
</SettingsGroup>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { PagePropsType } from "@/src/types";
|
||||
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
|
||||
import grabSettingsServerProps from "../(functions)/grab-settings-server-props";
|
||||
|
||||
export const server: BunextPageServerFn<PagePropsType> = async (params) => {
|
||||
return await grabSettingsServerProps(params);
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
import AppearanceSection from "./(sections)/appearance-section";
|
||||
import SettingsShell from "../(partials)/settings-shell";
|
||||
import settingsPageMeta from "../(functions)/settings-page-meta";
|
||||
|
||||
export default function AdminSettingsGeneralPage() {
|
||||
return (
|
||||
<SettingsShell
|
||||
title="General"
|
||||
description="How the app looks and behaves."
|
||||
>
|
||||
<AppearanceSection />
|
||||
</SettingsShell>
|
||||
);
|
||||
}
|
||||
|
||||
export const meta = settingsPageMeta({ label: "General" });
|
||||
Reference in New Issue
Block a user