task-d.7.12

This commit is contained in:
phaichayon
2026-07-03 16:09:54 +07:00
parent 15c56efc8b
commit 944dbc031d
21 changed files with 1638 additions and 839 deletions

View File

@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from 'next/server';
import { commitSetupCsvImport } from '@/features/setup/server/csv-commit.service';
import { createCsvImportSeedManifest } from '@/features/setup/server/seed-manifest.service';
import {
assertBootstrapRequiredFiles,
assertBootstrapStepFiles,
assertBootstrapSetupAvailable,
assertBootstrapToken,
BOOTSTRAP_ACTOR_ID,
@@ -13,7 +13,13 @@ import {
import { saveSetupStep, startSetupRun } from '@/features/setup/server/setup-state.service';
function isFile(value: FormDataEntryValue): value is File {
return typeof value === 'object' && 'arrayBuffer' in value && 'name' in value;
return (
typeof value === 'object' &&
'arrayBuffer' in value &&
'name' in value &&
typeof value.name === 'string' &&
value.name.trim().length > 0
);
}
function parseBoolean(value: FormDataEntryValue | null): boolean {
@@ -28,6 +34,7 @@ export async function POST(request: NextRequest) {
const formData = await request.formData();
assertBootstrapToken(String(formData.get('bootstrapToken') ?? request.headers.get('x-bootstrap-setup-token') ?? ''));
const stepKey = String(formData.get('stepKey') ?? '');
const previewHash = String(formData.get('previewHash') ?? '');
const dryRun = parseBoolean(formData.get('dryRun'));
const uploadedFiles = Array.from(formData.values()).filter(isFile);
@@ -43,7 +50,7 @@ export async function POST(request: NextRequest) {
}))
);
assertBootstrapRequiredFiles(files);
const step = assertBootstrapStepFiles(stepKey, files);
await startSetupRun({ actorId: BOOTSTRAP_ACTOR_ID, mode: 'production', targetOrganizationId: null });
const report = await commitSetupCsvImport({
@@ -58,7 +65,7 @@ export async function POST(request: NextRequest) {
}
await saveSetupStep({
stepKey: 'csv_commit',
stepKey: step.stepKey,
status: setupStepStatusFromImportReport(report),
inputHash: report.committedHash,
output: toSerializableRecord(report),
@@ -72,6 +79,10 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ message: error.message, details: error.details }, { status: error.status });
}
if (error instanceof TypeError && /multipart\/form-data|application\/x-www-form-urlencoded/i.test(error.message)) {
return NextResponse.json({ message: 'Upload setup CSV files using multipart/form-data.' }, { status: 400 });
}
return NextResponse.json({ message: 'Unable commit bootstrap setup CSV import' }, { status: 500 });
}
}

View File

@@ -3,6 +3,7 @@ import {
assertBootstrapSetupAvailable,
assertBootstrapToken,
BOOTSTRAP_ACTOR_ID,
BOOTSTRAP_REQUIRED_STEPS,
BootstrapSetupError,
getBootstrapSetupStatus
} from '@/features/setup/server/setup-bootstrap.service';
@@ -16,15 +17,23 @@ export async function POST(request: NextRequest) {
assertBootstrapToken(body.bootstrapToken ?? request.headers.get('x-bootstrap-setup-token') ?? '');
const currentRun = await getCurrentSetupRun();
const commitStep = currentRun.steps.find((step) => step.stepKey === 'csv_commit');
const hasSuccessfulCommit =
currentRun.run?.status === 'csv_imported' ||
currentRun.run?.status === 'verified' ||
commitStep?.status === 'passed' ||
commitStep?.status === 'warning';
const completedStepKeys = new Set(
currentRun.steps
.filter((step) => step.status === 'passed' || step.status === 'warning')
.map((step) => step.stepKey)
);
const missingSteps = BOOTSTRAP_REQUIRED_STEPS.filter(
(step) => !completedStepKeys.has(step.stepKey)
).map((step) => step.stepKey);
if (!hasSuccessfulCommit) {
return NextResponse.json({ message: 'Commit bootstrap setup data before completing setup.' }, { status: 409 });
if (missingSteps.length > 0) {
return NextResponse.json(
{
message: 'Complete all required bootstrap CSV steps before completing setup.',
missingSteps
},
{ status: 409 }
);
}
const status = await getBootstrapSetupStatus();

View File

@@ -1,7 +1,7 @@
import { NextRequest, NextResponse } from 'next/server';
import { previewSetupCsvImport } from '@/features/setup/server/csv-preview.service';
import {
assertBootstrapRequiredFiles,
assertBootstrapStepFiles,
assertBootstrapSetupAvailable,
assertBootstrapToken,
BOOTSTRAP_ACTOR_ID,
@@ -12,7 +12,13 @@ import {
import { saveSetupStep, startSetupRun } from '@/features/setup/server/setup-state.service';
function isFile(value: FormDataEntryValue): value is File {
return typeof value === 'object' && 'arrayBuffer' in value && 'name' in value;
return (
typeof value === 'object' &&
'arrayBuffer' in value &&
'name' in value &&
typeof value.name === 'string' &&
value.name.trim().length > 0
);
}
export async function POST(request: NextRequest) {
@@ -22,6 +28,7 @@ export async function POST(request: NextRequest) {
const formData = await request.formData();
assertBootstrapToken(String(formData.get('bootstrapToken') ?? request.headers.get('x-bootstrap-setup-token') ?? ''));
const stepKey = String(formData.get('stepKey') ?? '');
const uploadedFiles = Array.from(formData.values()).filter(isFile);
if (uploadedFiles.length === 0) {
@@ -35,13 +42,13 @@ export async function POST(request: NextRequest) {
}))
);
assertBootstrapRequiredFiles(files);
const step = assertBootstrapStepFiles(stepKey, files);
await startSetupRun({ actorId: BOOTSTRAP_ACTOR_ID, mode: 'production', targetOrganizationId: null });
const preview = await previewSetupCsvImport(files);
await saveSetupStep({
stepKey: 'csv_preview',
stepKey: step.stepKey,
status: setupStepStatusFromPreview(preview),
inputHash: preview.previewHash,
output: toSerializableRecord(preview),
@@ -49,12 +56,24 @@ export async function POST(request: NextRequest) {
warnings: preview.files.flatMap((file) => file.warnings)
});
return NextResponse.json(preview, { status: preview.summary.error > 0 ? 400 : 200 });
return NextResponse.json(preview);
} catch (error) {
if (error instanceof BootstrapSetupError) {
return NextResponse.json({ message: error.message, details: error.details }, { status: error.status });
}
if (error instanceof TypeError && /multipart\/form-data|application\/x-www-form-urlencoded/i.test(error.message)) {
return NextResponse.json({ message: 'Upload setup CSV files using multipart/form-data.' }, { status: 400 });
}
console.error('Bootstrap preview failed');
if (error instanceof Error) {
for (const line of (error.stack ?? error.message).split('\n').slice(0, 12)) {
console.error(line);
}
} else {
console.error(error);
}
return NextResponse.json({ message: 'Unable preview bootstrap setup CSV import' }, { status: 500 });
}
}

View File

@@ -1,52 +1,20 @@
/**
* Default theme that loads when no user preference is set
* Change this value to set a different default theme
* Default theme loads when no user preference is set.
* Change value to set a different default theme.
*/
export const DEFAULT_THEME = 'mongodb';
export const DEFAULT_THEME = 'alla';
export const THEMES = [
{
name: 'MongoDB',
value: 'mongodb'
},
{
name: 'Claude',
value: 'claude'
},
{
name: 'Neobrutualism',
value: 'neobrutualism'
},
{
name: 'Supabase',
value: 'supabase'
},
{
name: 'Vercel',
value: 'vercel'
},
{
name: 'Mono',
value: 'mono'
},
{
name: 'Notebook',
value: 'notebook'
},
{
name: 'Light Green',
value: 'light-green'
},
{
name: 'Zen',
value: 'zen'
},
{
name: 'Astro Vista',
value: 'astro-vista'
},
{
name: 'WhatsApp',
value: 'whatsapp'
}
{ name: 'MongoDB', value: 'mongodb' },
{ name: 'Claude', value: 'claude' },
{ name: 'Neobrutualism', value: 'neobrutualism' },
{ name: 'Supabase', value: 'supabase' },
{ name: 'Vercel', value: 'vercel' },
{ name: 'Mono', value: 'mono' },
{ name: 'Notebook', value: 'notebook' },
{ name: 'Light Green', value: 'light-green' },
{ name: 'Zen', value: 'zen' },
{ name: 'Astro Vista', value: 'astro-vista' },
{ name: 'WhatsApp', value: 'whatsapp' },
{ name: 'ALLA', value: 'alla' }
];

View File

@@ -1,5 +1,6 @@
import { queryOptions } from '@tanstack/react-query';
import {
getBootstrapSetupStatus,
getCurrentSetupRun,
getSetupProfiles,
getSetupReadiness,
@@ -14,6 +15,7 @@ export const setupKeys = {
readiness: (profileId?: string) => [...setupKeys.all, 'readiness', profileId ?? 'crm_demo'] as const,
templates: (profileId?: string) => [...setupKeys.all, 'templates', profileId ?? 'crm_demo'] as const,
profiles: (profileId?: string) => [...setupKeys.all, 'profiles', profileId ?? 'crm_demo'] as const,
bootstrapStatus: () => [...setupKeys.all, 'bootstrap', 'status'] as const,
run: () => [...setupKeys.all, 'run'] as const,
report: () => [...setupKeys.all, 'run', 'report'] as const,
verification: (payload: SetupVerificationPayload = {}) =>
@@ -38,6 +40,12 @@ export const setupProfilesQueryOptions = (profileId?: string) =>
queryFn: () => getSetupProfiles(profileId)
});
export const bootstrapSetupStatusQueryOptions = () =>
queryOptions({
queryKey: setupKeys.bootstrapStatus(),
queryFn: () => getBootstrapSetupStatus()
});
export const setupRunQueryOptions = () =>
queryOptions({
queryKey: setupKeys.run(),

View File

@@ -1,6 +1,7 @@
import { ApiClientError, apiClient } from '@/lib/api-client';
import type {
CsvPreviewResult,
BootstrapSetupStatusResponse,
ImportReport,
SaveSetupStepPayload,
SetupProfilesResponse,
@@ -34,6 +35,10 @@ export async function getSetupProfiles(profileId?: string): Promise<SetupProfile
return apiClient<SetupProfilesResponse>(withProfile('/setup/profiles', profileId));
}
export async function getBootstrapSetupStatus(): Promise<BootstrapSetupStatusResponse> {
return apiClient<BootstrapSetupStatusResponse>('/setup/bootstrap/status');
}
export async function getCurrentSetupRun(): Promise<SetupRunResponse> {
return apiClient<SetupRunResponse>('/setup/run/current');
}
@@ -102,6 +107,23 @@ export async function previewSetupCsvFiles(files: File[]): Promise<CsvPreviewRes
);
}
export async function previewBootstrapCsvStep(input: {
stepKey: string;
file: File;
bootstrapToken?: string;
}): Promise<CsvPreviewResult> {
const formData = buildCsvFormData([input.file]);
formData.set('stepKey', input.stepKey);
if (input.bootstrapToken) formData.set('bootstrapToken', input.bootstrapToken);
return readUploadResponse<CsvPreviewResult>(
await fetch('/api/setup/bootstrap/preview', {
method: 'POST',
body: formData
})
);
}
export async function commitSetupCsvFiles(input: {
files: File[];
previewHash: string;
@@ -118,3 +140,31 @@ export async function commitSetupCsvFiles(input: {
})
);
}
export async function commitBootstrapCsvStep(input: {
stepKey: string;
file: File;
previewHash: string;
dryRun?: boolean;
bootstrapToken?: string;
}): Promise<ImportReport> {
const formData = buildCsvFormData([input.file]);
formData.set('stepKey', input.stepKey);
formData.set('previewHash', input.previewHash);
formData.set('dryRun', String(Boolean(input.dryRun)));
if (input.bootstrapToken) formData.set('bootstrapToken', input.bootstrapToken);
return readUploadResponse<ImportReport>(
await fetch('/api/setup/bootstrap/commit', {
method: 'POST',
body: formData
})
);
}
export async function completeBootstrapSetup(report: Record<string, unknown>): Promise<SetupRunResponse> {
return apiClient<SetupRunResponse>('/setup/bootstrap/complete', {
method: 'POST',
body: JSON.stringify({ report })
});
}

View File

@@ -99,6 +99,28 @@ export interface SetupTemplatesResponse {
templates: SetupTemplateMetadataItem[];
}
export interface BootstrapRequiredStepDto {
stepKey: string;
fileName: string;
title: string;
description: string;
dependsOn: string[];
}
export interface BootstrapSetupStatusResponse {
initialized: boolean;
tokenRequired: boolean;
checks: {
completedSetupRun: boolean;
superAdminUser: boolean;
organization: boolean;
adminMembership: boolean;
};
requiredFiles: string[];
optionalFiles: string[];
requiredSteps: BootstrapRequiredStepDto[];
}
export interface SetupProfilesResponse {
success: boolean;
time: string;

View File

@@ -1,7 +1,12 @@
'use client';
import { useMutation } from '@tanstack/react-query';
import { commitSetupCsvFiles, previewSetupCsvFiles } from './service';
import {
commitBootstrapCsvStep,
commitSetupCsvFiles,
previewBootstrapCsvStep,
previewSetupCsvFiles
} from './service';
export function useCsvPreview() {
return useMutation({
@@ -15,3 +20,22 @@ export function useCsvCommit() {
commitSetupCsvFiles(input)
});
}
export function useBootstrapCsvPreview() {
return useMutation({
mutationFn: (input: { stepKey: string; file: File; bootstrapToken?: string }) =>
previewBootstrapCsvStep(input)
});
}
export function useBootstrapCsvCommit() {
return useMutation({
mutationFn: (input: {
stepKey: string;
file: File;
previewHash: string;
dryRun?: boolean;
bootstrapToken?: string;
}) => commitBootstrapCsvStep(input)
});
}

View File

@@ -1,7 +1,7 @@
'use client';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { completeSetupRun, saveSetupStep, startSetupRun } from './service';
import { completeBootstrapSetup, completeSetupRun, saveSetupStep, startSetupRun } from './service';
import { setupKeys } from './queries';
import type { SaveSetupStepPayload } from './types';
@@ -40,3 +40,12 @@ export function useCompleteSetupRun() {
onSuccess: invalidate
});
}
export function useCompleteBootstrapSetup() {
const invalidate = useInvalidateSetupRun();
return useMutation({
mutationFn: (report: Record<string, unknown>) => completeBootstrapSetup(report),
onSuccess: invalidate
});
}

File diff suppressed because it is too large Load Diff

View File

@@ -883,11 +883,11 @@ export function buildCsvPreviewBatchIndex(files: CsvParsedFile[]): CsvPreviewBat
files.forEach((file) => {
file.rows.forEach((row) => {
const values = row.values;
const organizationCode = normalizeOrganizationCode(values.organization_code);
const organizationCode = values.organization_code ? normalizeOrganizationCode(values.organization_code) : '';
switch (file.template.file) {
case 'organizations.csv':
index.organizations.add(normalizeOrganizationCode(values.organization_code));
if (values.organization_code) index.organizations.add(normalizeOrganizationCode(values.organization_code));
break;
case 'users.csv':
index.users.add(normalizeEmail(values.email));

View File

@@ -27,7 +27,7 @@ function issue(
}
function organizationCode(values: Record<string, string>): string {
return normalizeOrganizationCode(values.organization_code);
return values.organization_code ? normalizeOrganizationCode(values.organization_code) : '';
}
function businessKey(file: CsvParsedFile, values: Record<string, string>): string {

View File

@@ -13,6 +13,7 @@ export const BOOTSTRAP_REQUIRED_TEMPLATE_FILES = [
'users.csv',
'organizations.csv',
'memberships.csv',
'crm-role-assignments.csv',
'master-options.csv',
'branches.csv',
'product-types.csv',
@@ -34,6 +35,96 @@ export const BOOTSTRAP_OPTIONAL_TEMPLATE_FILES = [
'quotation-topic-items.csv'
] as const;
export const BOOTSTRAP_REQUIRED_STEPS = [
{
stepKey: 'bootstrap_users',
fileName: 'users.csv',
title: 'Users',
description: 'Create the setup administrator and required app users.',
dependsOn: []
},
{
stepKey: 'bootstrap_organizations',
fileName: 'organizations.csv',
title: 'Organizations',
description: 'Create tenant organization records after users exist.',
dependsOn: ['bootstrap_users']
},
{
stepKey: 'bootstrap_memberships',
fileName: 'memberships.csv',
title: 'Memberships',
description: 'Connect users to organizations with app-level roles.',
dependsOn: ['bootstrap_users', 'bootstrap_organizations']
},
{
stepKey: 'bootstrap_crm_role_assignments',
fileName: 'crm-role-assignments.csv',
title: 'CRM Roles',
description: 'Assign CRM role profiles and branch/product scopes.',
dependsOn: ['bootstrap_users', 'bootstrap_organizations', 'bootstrap_memberships']
},
{
stepKey: 'bootstrap_master_options',
fileName: 'master-options.csv',
title: 'Master Options',
description: 'Seed governed CRM option categories and labels.',
dependsOn: ['bootstrap_users', 'bootstrap_organizations', 'bootstrap_memberships']
},
{
stepKey: 'bootstrap_branches',
fileName: 'branches.csv',
title: 'Branches',
description: 'Create branch options used for CRM scope and numbering.',
dependsOn: [
'bootstrap_users',
'bootstrap_organizations',
'bootstrap_memberships',
'bootstrap_master_options'
]
},
{
stepKey: 'bootstrap_product_types',
fileName: 'product-types.csv',
title: 'Product Types',
description: 'Create product type options used for CRM scope and numbering.',
dependsOn: [
'bootstrap_users',
'bootstrap_organizations',
'bootstrap_memberships',
'bootstrap_master_options'
]
},
{
stepKey: 'bootstrap_document_sequences',
fileName: 'document-sequences.csv',
title: 'Document Sequences',
description: 'Configure document numbering for CRM entities.',
dependsOn: ['bootstrap_organizations', 'bootstrap_branches', 'bootstrap_product_types']
},
{
stepKey: 'bootstrap_approval_workflows',
fileName: 'approval-workflows.csv',
title: 'Approval Workflows',
description: 'Create approval workflow headers.',
dependsOn: ['bootstrap_organizations']
},
{
stepKey: 'bootstrap_approval_steps',
fileName: 'approval-steps.csv',
title: 'Approval Steps',
description: 'Create ordered approval steps for each workflow.',
dependsOn: ['bootstrap_approval_workflows']
},
{
stepKey: 'bootstrap_approval_matrix',
fileName: 'approval-matrix.csv',
title: 'Approval Matrix',
description: 'Create approval routing rules by scope and amount.',
dependsOn: ['bootstrap_approval_workflows', 'bootstrap_branches', 'bootstrap_product_types']
}
] as const;
export class BootstrapSetupError extends Error {
status: number;
details?: Record<string, unknown>;
@@ -57,11 +148,16 @@ export interface BootstrapSetupStatus {
};
requiredFiles: string[];
optionalFiles: string[];
requiredSteps: typeof BOOTSTRAP_REQUIRED_STEPS;
}
async function hasRows<T>(query: Promise<Array<T & { value: number }>>) {
const [row] = await query;
return Number(row?.value ?? 0) > 0;
try {
const [row] = await query;
return Number(row?.value ?? 0) > 0;
} catch {
return false;
}
}
export async function getBootstrapSetupStatus(): Promise<BootstrapSetupStatus> {
@@ -99,7 +195,8 @@ export async function getBootstrapSetupStatus(): Promise<BootstrapSetupStatus> {
tokenRequired: Boolean(process.env.BOOTSTRAP_SETUP_TOKEN?.trim()),
checks,
requiredFiles: [...BOOTSTRAP_REQUIRED_TEMPLATE_FILES],
optionalFiles: [...BOOTSTRAP_OPTIONAL_TEMPLATE_FILES]
optionalFiles: [...BOOTSTRAP_OPTIONAL_TEMPLATE_FILES],
requiredSteps: [...BOOTSTRAP_REQUIRED_STEPS]
};
}
@@ -126,7 +223,12 @@ export function assertBootstrapToken(token: string | null | undefined) {
}
export function getMissingBootstrapRequiredFiles(files: CsvPreviewFileInput[]) {
const uploaded = new Set(files.map((file) => file.fileName.trim().toLowerCase()));
const uploaded = new Set(
files
.map((file) => file.fileName)
.filter((fileName): fileName is string => typeof fileName === 'string')
.map((fileName) => fileName.trim().toLowerCase())
);
return BOOTSTRAP_REQUIRED_TEMPLATE_FILES.filter((fileName) => !uploaded.has(fileName));
}
@@ -140,6 +242,37 @@ export function assertBootstrapRequiredFiles(files: CsvPreviewFileInput[]) {
}
}
export function assertBootstrapStepFiles(stepKey: string, files: CsvPreviewFileInput[]) {
const step = BOOTSTRAP_REQUIRED_STEPS.find((candidate) => candidate.stepKey === stepKey);
if (!step) {
throw new BootstrapSetupError('Unknown bootstrap setup step.', 400, {
stepKey,
allowedStepKeys: BOOTSTRAP_REQUIRED_STEPS.map((candidate) => candidate.stepKey)
});
}
if (files.length !== 1) {
throw new BootstrapSetupError('Upload exactly one CSV file for this setup step.', 400, {
stepKey,
expectedFileName: step.fileName,
receivedFiles: files.map((file) => file.fileName)
});
}
const [file] = files;
if (file.fileName.toLowerCase() !== step.fileName.toLowerCase()) {
throw new BootstrapSetupError('Uploaded CSV file does not match the current setup step.', 400, {
stepKey,
expectedFileName: step.fileName,
receivedFileName: file.fileName
});
}
return step;
}
export function setupStepStatusFromReportStatus(status: 'PASS' | 'WARNING' | 'FAIL'): SetupStepStatus {
if (status === 'PASS') return 'passed';
if (status === 'WARNING') return 'warning';

View File

@@ -77,7 +77,46 @@ const STALE_DEPENDENCIES: Record<string, string[]> = {
sequences: ['verification'],
approval: ['csv_preview', 'csv_commit', 'verification'],
csv_preview: ['csv_commit'],
csv_commit: ['verification']
csv_commit: ['verification'],
bootstrap_users: [
'bootstrap_organizations',
'bootstrap_memberships',
'bootstrap_crm_role_assignments',
'bootstrap_master_options',
'bootstrap_branches',
'bootstrap_product_types',
'bootstrap_document_sequences',
'bootstrap_approval_workflows',
'bootstrap_approval_steps',
'bootstrap_approval_matrix'
],
bootstrap_organizations: [
'bootstrap_memberships',
'bootstrap_crm_role_assignments',
'bootstrap_master_options',
'bootstrap_branches',
'bootstrap_product_types',
'bootstrap_document_sequences',
'bootstrap_approval_workflows',
'bootstrap_approval_steps',
'bootstrap_approval_matrix'
],
bootstrap_memberships: [
'bootstrap_crm_role_assignments',
'bootstrap_master_options',
'bootstrap_branches',
'bootstrap_product_types',
'bootstrap_document_sequences'
],
bootstrap_master_options: [
'bootstrap_branches',
'bootstrap_product_types',
'bootstrap_document_sequences',
'bootstrap_approval_matrix'
],
bootstrap_branches: ['bootstrap_document_sequences', 'bootstrap_approval_matrix'],
bootstrap_product_types: ['bootstrap_document_sequences', 'bootstrap_approval_matrix'],
bootstrap_approval_workflows: ['bootstrap_approval_steps', 'bootstrap_approval_matrix']
};
function now() {

View File

@@ -12,6 +12,7 @@ interface BootstrapStatusResponse {
function isBootstrapAllowedPath(pathname: string) {
return (
pathname === '/administration/setup' ||
pathname.startsWith('/api/auth/') ||
pathname.startsWith('/api/setup/bootstrap/') ||
pathname === '/api/setup/templates'
);

View File

@@ -10,6 +10,7 @@
@import './themes/zen.css';
@import './themes/astro-vista.css';
@import './themes/whatsapp.css';
@import './themes/alla.css';
body {
@apply overscroll-none;

117
src/styles/themes/alla.css Normal file
View File

@@ -0,0 +1,117 @@
[data-theme='alla'] {
--background: oklch(0.9911 0 0);
--foreground: oklch(0.2046 0 0);
--card: oklch(0.9911 0 0);
--card-foreground: oklch(0.2046 0 0);
--popover: oklch(0.9911 0 0);
--popover-foreground: oklch(0.4386 0 0);
--primary: oklch(0.4799 0.0804 155.8197);
--primary-foreground: oklch(1 0 0);
--secondary: oklch(0.994 0 0);
--secondary-foreground: oklch(0.2046 0 0);
--muted: oklch(0.9461 0 0);
--muted-foreground: oklch(0.2435 0 0);
--accent: oklch(0.9461 0 0);
--accent-foreground: oklch(0.2435 0 0);
--destructive: oklch(0.5523 0.1927 32.7272);
--destructive-foreground: oklch(0.9934 0.0032 17.2118);
--border: oklch(0.9037 0 0);
--input: oklch(0.9731 0 0);
--ring: oklch(0.5568 0.1355 155.812);
--chart-1: oklch(0.8348 0.1302 160.908);
--chart-2: oklch(0.6231 0.188 259.8145);
--chart-3: oklch(0.6056 0.2189 292.7172);
--chart-4: oklch(0.7686 0.1647 70.0804);
--chart-5: oklch(0.6959 0.1491 162.4796);
--sidebar: oklch(0.9911 0 0);
--sidebar-foreground: oklch(0.5452 0 0);
--sidebar-primary: oklch(0.5568 0.1355 155.812);
--sidebar-primary-foreground: oklch(1 0 0);
--sidebar-accent: oklch(0.9461 0 0);
--sidebar-accent-foreground: oklch(0.2435 0 0);
--sidebar-border: oklch(0.9037 0 0);
--sidebar-ring: oklch(0.5568 0.1355 155.812);
--font-sans: Anek Kannada, ui-sans-serif, sans-serif, system-ui;
--font-serif: ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif;
--font-mono: monospace;
--radius: 0.5rem;
}
[data-theme='alla'].dark {
--background: oklch(0.1822 0 0);
--foreground: oklch(0.9851 0 0);
--card: oklch(0.2046 0 0);
--card-foreground: oklch(0.9851 0 0);
--popover: oklch(0.2046 0 0);
--popover-foreground: oklch(0.9851 0 0);
--primary: oklch(0.5568 0.1355 155.812);
--primary-foreground: oklch(0.9213 0.0135 167.1556);
--secondary: oklch(0.2603 0 0);
--secondary-foreground: oklch(0.9851 0 0);
--muted: oklch(0.2603 0 0);
--muted-foreground: oklch(0.7122 0 0);
--accent: oklch(0.3132 0 0);
--accent-foreground: oklch(0.9851 0 0);
--destructive: oklch(0.3123 0.0852 29.7877);
--destructive-foreground: oklch(0.9368 0.0045 34.3092);
--border: oklch(0.2809 0 0);
--input: oklch(0.2603 0 0);
--ring: oklch(0.5568 0.1355 155.812);
--chart-1: oklch(0.8003 0.1821 151.711);
--chart-2: oklch(0.7137 0.1434 254.624);
--chart-3: oklch(0.709 0.1592 293.5412);
--chart-4: oklch(0.8369 0.1644 84.4286);
--chart-5: oklch(0.7845 0.1325 181.912);
--sidebar: oklch(0.1822 0 0);
--sidebar-foreground: oklch(0.6301 0 0);
--sidebar-primary: oklch(0.5568 0.1355 155.812);
--sidebar-primary-foreground: oklch(0.9213 0.0135 167.1556);
--sidebar-accent: oklch(0.3132 0 0);
--sidebar-accent-foreground: oklch(0.9851 0 0);
--sidebar-border: oklch(0.2809 0 0);
--sidebar-ring: oklch(0.5568 0.1355 155.812);
}
[data-theme='alla'] {
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
--font-sans: var(--font-sans);
--font-mono: var(--font-mono);
--font-serif: var(--font-serif);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
}
}