diff --git a/src/components/CreateRoleModal.svelte b/src/components/CreateRoleModal.svelte new file mode 100644 index 0000000..32d28d8 --- /dev/null +++ b/src/components/CreateRoleModal.svelte @@ -0,0 +1,953 @@ + + +{#if isOpen} + + +{/if} + + diff --git a/src/lib/index.ts b/src/lib/index.ts index caece70..0d66291 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -8,6 +8,8 @@ export const optima = { credential: (await import("./optima-api/modules/credentials")).credential, credentialType: (await import("./optima-api/modules/credentialTypes")) .credentialType, + role: (await import("./optima-api/modules/roles")).role, + permission: (await import("./optima-api/modules/permissions")).permission, user, }; /** diff --git a/src/lib/optima-api/modules/companies.ts b/src/lib/optima-api/modules/companies.ts index d070d01..60604aa 100644 --- a/src/lib/optima-api/modules/companies.ts +++ b/src/lib/optima-api/modules/companies.ts @@ -1,8 +1,16 @@ import api from "../axios"; export const company = { - async fetch(accessToken: string, id: string) { + async fetch( + accessToken: string, + id: string, + options?: { includeAddress?: boolean }, + ) { + const params: Record = {}; + if (options?.includeAddress) params.includeAddress = "true"; + const company = await api.get(`/v1/company/companies/${id}`, { + params, headers: { Authorization: `Bearer ${accessToken}`, }, @@ -27,6 +35,14 @@ export const company = { return companies.data; }, + async count(accessToken: string) { + const response = await api.get("/v1/company/count", { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }); + return response.data.data.count; + }, async fetchConfigurations(accessToken: string, id: string) { const configurations = await api.get( `/v1/company/companies/${id}/configurations`, diff --git a/src/lib/optima-api/modules/permissions.ts b/src/lib/optima-api/modules/permissions.ts new file mode 100644 index 0000000..8300321 --- /dev/null +++ b/src/lib/optima-api/modules/permissions.ts @@ -0,0 +1,47 @@ +import api from "../axios"; + +export interface PermissionNode { + node: string; + description: string; + usedIn: string[]; + dependencies?: string[]; +} + +export interface PermissionCategory { + name: string; + description: string; + permissions: PermissionNode[]; +} + +export interface PermissionsCategorized { + [category: string]: PermissionCategory; +} + +export const permission = { + async fetchCategorized(accessToken: string) { + const response = await api.get("/v1/permissions", { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }); + return response.data; + }, + + async fetchFlat(accessToken: string) { + const response = await api.get("/v1/permissions/nodes", { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }); + return response.data; + }, + + async fetchByCategory(accessToken: string, category: string) { + const response = await api.get(`/v1/permissions/${category}`, { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }); + return response.data; + }, +}; diff --git a/src/lib/optima-api/modules/roles.ts b/src/lib/optima-api/modules/roles.ts new file mode 100644 index 0000000..e145dd7 --- /dev/null +++ b/src/lib/optima-api/modules/roles.ts @@ -0,0 +1,104 @@ +import api from "../axios"; + +export interface Role { + id: string; + title: string; + moniker: string; + permissions: string[]; + createdAt: string; + updatedAt: string; +} + +export const role = { + async fetchMany(accessToken: string) { + const response = await api.get("/v1/role", { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }); + return response.data; + }, + + async fetch(accessToken: string, identifier: string) { + const response = await api.get(`/v1/role/${identifier}`, { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }); + return response.data; + }, + + async create( + accessToken: string, + data: Omit, + ) { + const response = await api.post("/v1/role", data, { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }); + return response.data; + }, + + async update( + accessToken: string, + identifier: string, + updates: Partial>, + ) { + const response = await api.patch(`/v1/role/${identifier}`, updates, { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }); + return response.data; + }, + + async delete(accessToken: string, identifier: string) { + const response = await api.delete(`/v1/role/${identifier}`, { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }); + return response.data; + }, + + async addPermissions( + accessToken: string, + identifier: string, + permissions: string[], + ) { + const response = await api.post( + `/v1/role/${identifier}/permissions`, + { permissions }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + return response.data; + }, + + async removePermissions( + accessToken: string, + identifier: string, + permissions: string[], + ) { + const response = await api.delete(`/v1/role/${identifier}/permissions`, { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + data: { permissions }, + }); + return response.data; + }, + + async fetchUsers(accessToken: string, identifier: string) { + const response = await api.get(`/v1/role/${identifier}/users`, { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }); + return response.data; + }, +}; diff --git a/src/lib/permissions.ts b/src/lib/permissions.ts new file mode 100644 index 0000000..499ff60 --- /dev/null +++ b/src/lib/permissions.ts @@ -0,0 +1,51 @@ +import { optima } from "$lib"; + +export type PermissionMap = Record; + +/** + * Check multiple permissions for the current user and return a map of + * permission → boolean. Designed to be called from any +page.server.ts + * or +layout.server.ts load function. + * + * @example + * ```ts + * const perms = await checkPermissions(accessToken, [ + * "company.fetch.address", + * "credential.create", + * ]); + * // perms => { "company.fetch.address": true, "credential.create": false } + * ``` + */ +export async function checkPermissions( + accessToken: string, + permissions: string[], +): Promise { + if (!permissions.length) return {}; + + try { + const result = await optima.user.checkPermissions(accessToken, permissions); + + const results: Array<{ permission: string; hasPermission: boolean }> = + result?.data?.results ?? []; + + return results.reduce((map, entry) => { + map[entry.permission] = entry.hasPermission === true; + return map; + }, {}); + } catch (err) { + console.error("Permission check failed:", err); + // Default every requested permission to false on failure + return permissions.reduce((map, p) => { + map[p] = false; + return map; + }, {}); + } +} + +/** + * Convenience helper — returns true when a specific permission is + * granted inside a PermissionMap. + */ +export function hasPermission(map: PermissionMap, permission: string): boolean { + return map[permission] === true; +} diff --git a/src/lib/theme.ts b/src/lib/theme.ts new file mode 100644 index 0000000..2b8c496 --- /dev/null +++ b/src/lib/theme.ts @@ -0,0 +1,39 @@ +import { writable } from "svelte/store"; +import { browser } from "$app/environment"; + +type Theme = "light" | "dark"; + +function createThemeStore() { + const initial: Theme = browser + ? ((localStorage.getItem("theme") as Theme) ?? "dark") + : "dark"; + + const { subscribe, set, update } = writable(initial); + + function applyTheme(theme: Theme) { + if (browser) { + document.documentElement.setAttribute("data-theme", theme); + localStorage.setItem("theme", theme); + } + } + + // Apply on init + if (browser) applyTheme(initial); + + return { + subscribe, + toggle() { + update((current) => { + const next = current === "dark" ? "light" : "dark"; + applyTheme(next); + return next; + }); + }, + set(theme: Theme) { + applyTheme(theme); + set(theme); + }, + }; +} + +export const theme = createThemeStore(); diff --git a/src/routes/(auth)/login/+page.svelte b/src/routes/(auth)/login/+page.svelte index febc971..d6a79bd 100644 --- a/src/routes/(auth)/login/+page.svelte +++ b/src/routes/(auth)/login/+page.svelte @@ -4,6 +4,7 @@ import { enhance } from "$app/forms"; import LoadingSpinner from "../../../components/LoadingSpinner.svelte"; import { writable } from "svelte/store"; + import { theme } from "$lib/theme"; const uriData = await optima.auth.fetchAuthRedirectUri(PUBLIC_API_URL); let loading = writable(false); @@ -34,6 +35,42 @@
+ +
diff --git a/src/routes/admin/+layout.server.ts b/src/routes/admin/+layout.server.ts new file mode 100644 index 0000000..3c96481 --- /dev/null +++ b/src/routes/admin/+layout.server.ts @@ -0,0 +1,40 @@ +import { optima } from "$lib"; +import { handleApiError } from "$lib/optima-api/errorHandler"; +import { checkPermissions, type PermissionMap } from "$lib/permissions"; +import { redirect } from "@sveltejs/kit"; +import type { LayoutServerLoad } from "./$types"; + +export const load: LayoutServerLoad = async ({ locals }) => { + const accessToken = locals.session?.accessToken; + if (!accessToken) { + throw redirect(303, "/login"); + } + + try { + // Check the top-level admin gate + all per-tab permissions in one call + const permissions = await checkPermissions(accessToken, [ + "ui.navigation.admin.view", + "admin.users.view", + "admin.roles.view", + "admin.credential-types.view", + ]); + + if (!permissions["ui.navigation.admin.view"]) { + throw redirect(303, "/"); + } + + // Fetch current user info for the dashboard greeting + const userInfo = await optima.user.fetchInfo(accessToken); + + return { + user: userInfo?.data ?? null, + permissions, + }; + } catch (err) { + // Re-throw redirects so SvelteKit handles them + if (err && typeof err === "object" && "status" in err) { + throw err; + } + handleApiError(err); + } +}; diff --git a/src/routes/admin/+layout.svelte b/src/routes/admin/+layout.svelte new file mode 100644 index 0000000..04b4067 --- /dev/null +++ b/src/routes/admin/+layout.svelte @@ -0,0 +1,105 @@ + + + + Admin — Project Optima + + +
+
+ +
+
+ + + + +

Administration

+ Welcome back, {userName} +
+
+ + +
+ {#each visibleTabs as tab} + + {tab.label} + + {/each} +
+ + +
+ +
+
+
diff --git a/src/routes/admin/+page.server.ts b/src/routes/admin/+page.server.ts new file mode 100644 index 0000000..bef50cd --- /dev/null +++ b/src/routes/admin/+page.server.ts @@ -0,0 +1,20 @@ +import { optima } from "$lib"; +import { handleApiError } from "$lib/optima-api/errorHandler"; +import type { PageServerLoad } from "./$types"; + +export const load: PageServerLoad = async ({ locals }) => { + const accessToken = locals.session?.accessToken; + if (!accessToken) { + return { companyCount: null }; + } + + try { + const companyCount = await optima.company.count(accessToken); + + return { + companyCount: companyCount ?? null, + }; + } catch (err) { + handleApiError(err); + } +}; diff --git a/src/routes/admin/+page.svelte b/src/routes/admin/+page.svelte new file mode 100644 index 0000000..aeea7f5 --- /dev/null +++ b/src/routes/admin/+page.svelte @@ -0,0 +1,136 @@ + + + +
+
+
+ + + + + +
+
+ {companyCount ?? "—"} + Companies +
+
+ +
+
+ + + + + + +
+
+ + Users +
+
+ +
+
+ + + + +
+
+ + Credentials +
+
+ +
+
+ + + +
+
+ + Activity Today +
+
+
+ + +

Quick Actions

+ + + +

Recent Activity

+
+
+ + + + + Activity feed coming soon +
+
diff --git a/src/routes/admin/credential-types/+page.svelte b/src/routes/admin/credential-types/+page.svelte new file mode 100644 index 0000000..8406c8d --- /dev/null +++ b/src/routes/admin/credential-types/+page.svelte @@ -0,0 +1,33 @@ + + +{#if !hasAccess} +
+ + + + +

Access Denied

+

+ You don't have permission to manage credential types. Contact your + administrator to request access. +

+
+{:else} +
+ + + + +

Credential Type Management

+

+ Credential type definitions and configuration will be wired up here. + Connect an API module to populate this view. +

+
+{/if} diff --git a/src/routes/admin/roles/+page.server.ts b/src/routes/admin/roles/+page.server.ts new file mode 100644 index 0000000..7865827 --- /dev/null +++ b/src/routes/admin/roles/+page.server.ts @@ -0,0 +1,148 @@ +import { optima } from "$lib"; +import { handleApiError } from "$lib/optima-api/errorHandler"; +import { checkPermissions } from "$lib/permissions"; +import { fail } from "@sveltejs/kit"; +import type { Actions, PageServerLoad } from "./$types"; +import { AxiosError } from "axios"; + +export const load: PageServerLoad = async ({ locals }) => { + const accessToken = locals.session?.accessToken; + if (!accessToken) { + return { roles: [], permissions: {}, permissionNodes: {} }; + } + + try { + const [rolesResult, permissions, permNodesResult] = await Promise.all([ + optima.role.fetchMany(accessToken), + checkPermissions(accessToken, [ + "admin.roles.view", + "admin.roles.create", + "admin.roles.edit", + "admin.roles.delete", + ]), + optima.permission + .fetchCategorized(accessToken) + .catch(() => ({ data: {} })), + ]); + + const roles = rolesResult?.data ?? []; + + // Fetch users for each role in parallel + const rolesWithUsers = await Promise.all( + roles.map(async (role: Record) => { + try { + const usersResult = await optima.role.fetchUsers( + accessToken, + role.id as string, + ); + return { ...role, users: usersResult?.data ?? [] }; + } catch { + return { ...role, users: [] }; + } + }), + ); + + return { + roles: rolesWithUsers, + permissions, + permissionNodes: permNodesResult?.data ?? {}, + }; + } catch (err) { + handleApiError(err); + } +}; + +export const actions: Actions = { + createRole: async ({ locals, request }) => { + const accessToken = locals.session?.accessToken; + if (!accessToken) { + return fail(401, { message: "Not authenticated." }); + } + + const formData = await request.formData(); + const title = (formData.get("title") as string)?.trim(); + const moniker = (formData.get("moniker") as string)?.trim(); + const permissions = formData.getAll("permissions") as string[]; + + if (!title || !moniker) { + return fail(400, { message: "Title and moniker are required." }); + } + + try { + await optima.role.create(accessToken, { title, moniker, permissions }); + return {}; + } catch (err: unknown) { + const data = (err as AxiosError)?.response?.data as + | Record + | undefined; + const message = + (data?.message as string) ?? + (err instanceof Error ? err.message : "Failed to create role."); + const status = (data?.status as number) ?? 500; + return fail(status, { message }); + } + }, + + updateRole: async ({ locals, request }) => { + const accessToken = locals.session?.accessToken; + if (!accessToken) { + return fail(401, { message: "Not authenticated." }); + } + + const formData = await request.formData(); + const id = (formData.get("id") as string)?.trim(); + const title = (formData.get("title") as string)?.trim(); + const moniker = (formData.get("moniker") as string)?.trim(); + const permissions = formData.getAll("permissions") as string[]; + + if (!id || !title || !moniker) { + return fail(400, { message: "Required fields are missing." }); + } + + try { + await optima.role.update(accessToken, id, { + title, + moniker, + permissions, + }); + return {}; + } catch (err: unknown) { + const data = (err as AxiosError)?.response?.data as + | Record + | undefined; + const message = + (data?.message as string) ?? + (err instanceof Error ? err.message : "Failed to update role."); + const status = (data?.status as number) ?? 500; + return fail(status, { message }); + } + }, + + deleteRole: async ({ locals, request }) => { + const accessToken = locals.session?.accessToken; + if (!accessToken) { + return fail(401, { message: "Not authenticated." }); + } + + const formData = await request.formData(); + const id = (formData.get("id") as string)?.trim(); + + if (!id) { + return fail(400, { message: "Role ID is required." }); + } + + try { + await optima.role.delete(accessToken, id); + return {}; + } catch (err: unknown) { + const data = (err as AxiosError)?.response?.data as + | Record + | undefined; + const message = + (data?.message as string) ?? + (err instanceof Error ? err.message : "Failed to delete role."); + const status = (data?.status as number) ?? 500; + return fail(status, { message }); + } + }, +}; diff --git a/src/routes/admin/roles/+page.svelte b/src/routes/admin/roles/+page.svelte new file mode 100644 index 0000000..73c3ab3 --- /dev/null +++ b/src/routes/admin/roles/+page.svelte @@ -0,0 +1,1027 @@ + + + (openMenuId = null)} /> + +{#if !hasAccess} +
+ + + + +

Access Denied

+

+ You don't have permission to manage roles. Contact your administrator to + request access. +

+
+{:else if roles.length === 0} + { + isCreateModalOpen = false; + roleToEdit = null; + }} + onSuccess={() => { + isCreateModalOpen = false; + roleToEdit = null; + }} + /> +
+ + + +

No Roles Found

+

+ There are no roles configured yet. Create your first role to get started. +

+ {#if canCreate} + + {/if} +
+{:else} + { + isCreateModalOpen = false; + roleToEdit = null; + }} + onSuccess={() => { + isCreateModalOpen = false; + roleToEdit = null; + }} + /> + + {#if roleToDelete} + +
e.key === "Escape" && cancelDelete()} + > +
+
+ + + + + + +
+

Delete Role

+

+ Are you sure you want to delete + {roleToDelete.title}? This action cannot be undone. +

+ {#if deleteError} +

{deleteError}

+ {/if} +
+ + + + + +
+
+
+ {/if} + +
+

+ Roles + {roles.length} role{roles.length === 1 ? "" : "s"} +

+ {#if canCreate} + + {/if} +
+ +
+ + + + + + + + + + + + + + {#each roles as role (role.id)} + toggleRole(role.id)} + > + + + + + + + + + + {#if expandedRoleId === role.id} + + + + {/if} + {/each} + +
TitleMonikerPermissionsUsersCreatedUpdated
+
+ + + + {role.title} +
+
+ {role.moniker} + + + {role.permissions.length} permission{role.permissions.length === + 1 + ? "" + : "s"} + + + + + + + + + + {role.users.length} user{role.users.length === 1 ? "" : "s"} + + {formatDate(role.createdAt)}{formatDate(role.updatedAt)} +
+ {#if role.moniker === "administrator"} + + + + + + System + + {:else if canEdit || canDelete} + + {/if} + + + +
+
+
+
+
+

+ + + + + Permissions + {role.permissions.length} +

+ {#if role.permissions.length === 0} +

No permissions assigned

+ {:else} +
+ {#each role.permissions as perm} + {perm} + {/each} +
+ {/if} +
+ +
+

+ + + + + + + Users + {role.users.length} +

+ {#if role.users.length === 0} +

+ No users assigned to this role +

+ {:else} +
+ {#each role.users as user (user.id)} +
+
+ {user.name + .split(" ") + .map((n) => n[0]) + .join("") + .slice(0, 2) + .toUpperCase()} +
+ +
+ {/each} +
+ {/if} +
+
+
+
+
+{/if} + + diff --git a/src/routes/admin/users/+page.svelte b/src/routes/admin/users/+page.svelte new file mode 100644 index 0000000..52bea7d --- /dev/null +++ b/src/routes/admin/users/+page.svelte @@ -0,0 +1,35 @@ + + +{#if !hasAccess} +
+ + + + +

Access Denied

+

+ You don't have permission to manage users. Contact your administrator to + request access. +

+
+{:else} +
+ + + + + + +

User Management

+

+ User listing and editing will be wired up here. Connect an API module to + populate this view. +

+
+{/if} diff --git a/src/routes/companies/+page.svelte b/src/routes/companies/+page.svelte index d288ad1..acfa5bf 100644 --- a/src/routes/companies/+page.svelte +++ b/src/routes/companies/+page.svelte @@ -25,14 +25,19 @@ let debounceTimer: ReturnType; let isSearching = false; let searchInputEl: HTMLInputElement; + let searchStartedAt = 0; // When navigation completes (results loaded), clear loading & refocus + // Ensure spinner stays visible for at least 500ms afterNavigate(() => { - isSearching = false; - if (searchInputEl && document.activeElement !== searchInputEl) { - // Use tick to ensure DOM is settled - requestAnimationFrame(() => searchInputEl?.focus()); - } + const elapsed = Date.now() - searchStartedAt; + const remaining = Math.max(0, 500 - elapsed); + setTimeout(() => { + isSearching = false; + if (searchInputEl && document.activeElement !== searchInputEl) { + requestAnimationFrame(() => searchInputEl?.focus()); + } + }, remaining); }); $: currentPage = data.currentPage; @@ -49,6 +54,7 @@ function handleSearch() { isSearching = true; + searchStartedAt = Date.now(); clearTimeout(debounceTimer); debounceTimer = setTimeout(() => { const params = new URLSearchParams(); @@ -61,6 +67,7 @@ function handleKeydown(e: KeyboardEvent) { if (e.key === "Enter") { isSearching = true; + searchStartedAt = Date.now(); clearTimeout(debounceTimer); const params = new URLSearchParams(); params.set("page", "1"); diff --git a/src/routes/companies/[id]/+page.server.ts b/src/routes/companies/[id]/+page.server.ts new file mode 100644 index 0000000..57c1e28 --- /dev/null +++ b/src/routes/companies/[id]/+page.server.ts @@ -0,0 +1,37 @@ +import { optima } from "$lib"; +import { handleApiError } from "$lib/optima-api/errorHandler"; +import { checkPermissions, type PermissionMap } from "$lib/permissions"; +import type { PageServerLoad } from "./$types"; + +export const load: PageServerLoad = async ({ locals, params }) => { + const accessToken = locals.session?.accessToken; + if (!accessToken) { + return { + company: null, + configurations: [], + permissions: {} as PermissionMap, + }; + } + + try { + // Run permission checks in parallel with other data fetches. + // Add any new permissions the company page needs to this array. + const [permissions, configsResult] = await Promise.all([ + checkPermissions(accessToken, ["company.fetch.address"]), + optima.company.fetchConfigurations(accessToken, params.id), + ]); + + // Fetch company with or without address based on permission + const companyResult = await optima.company.fetch(accessToken, params.id, { + includeAddress: permissions["company.fetch.address"] === true, + }); + + return { + company: companyResult?.data ?? null, + configurations: configsResult?.data ?? [], + permissions, + }; + } catch (err) { + handleApiError(err); + } +}; diff --git a/src/routes/companies/[id]/+page.svelte b/src/routes/companies/[id]/+page.svelte new file mode 100644 index 0000000..a750aa6 --- /dev/null +++ b/src/routes/companies/[id]/+page.svelte @@ -0,0 +1,712 @@ + + + + {company?.name ?? "Company"} — Project Optima + + +
+ +
+
+ + {#if company} + +
+
+ {companyInitials(company.name)} +
+

{company.name}

+ {#if company.status} + {company.status} + {/if} +
+ + +
+ {#if company.type} +
+ + + + +
+ Type + {company.type} +
+
+ {/if} + + {#if company.identifier || company.id} +
+ + + +
+ Identifier + {company.identifier || company.id} +
+
+ {/if} + + {#if company.contactEmail} +
+ + + + +
+ Email + {company.contactEmail} +
+
+ {/if} + + {#if company.contactPhone} +
+ + + +
+ Phone + {company.contactPhone} +
+
+ {/if} + + {#if permissions["company.fetch.address"] && formatAddress(company).length > 0} +
+ + + + +
+ Address + + {#each formatAddress(company) as line} + {line}
+ {/each} +
+
+
+ {/if} + + {#if formatDate(company.createdAt)} +
+ + + + + + +
+ Created + {formatDate(company.createdAt)} +
+
+ {/if} + + {#if formatDate(company.updatedAt)} +
+ + + + +
+ Updated + {formatDate(company.updatedAt)} +
+
+ {/if} +
+ {:else} +
+

Company not found.

+
+ {/if} +
+
+ + +
+
+ {#each tabs as tab} + + {/each} +
+
+ {#if activeTab === "Credentials"} +

Credentials content

+ {:else if activeTab === "Configurations"} + {#if configurations.length === 0} +
+ + + +

No configurations found

+
+ {:else} +
+ +
+ {#each configurations as config (config.id)} + + {/each} +
+ + + {#if selectedConfig} +
+ {#key configFadeKey} +
+
+
+

+ {selectedConfig.name} +

+
+ {#if selectedConfig.type?.name} + {selectedConfig.type.name} + {/if} + {#if selectedConfig.status?.name} + + {selectedConfig.status.name} + + {/if} +
+
+ +
+ + {#if selectedConfig.serialNumber} +
+ Serial # + {selectedConfig.serialNumber} +
+ {/if} + + + {#if selectedConfig.notes} +
+

+ + + + + + + + Notes +

+

{selectedConfig.notes}

+
+ {/if} + + + {#if selectedConfig.questions && selectedConfig.questions.length > 0} +
+

+ + + + + Configuration Details +

+
+ {#each selectedConfig.questions as q (q.id)} +
+ {q.question} +
+ {#if q.fieldType === "Password"} + + {#if revealedPasswords[q.id]} + {q.answer || "—"} + {:else} + {q.answer ? "••••••••" : "—"} + {/if} + + {#if q.answer} + + {/if} + {:else if q.fieldType === "TextArea"} + {q.answer || "—"} + {:else} + {q.answer || "—"} + {/if} +
+
+ {/each} +
+
+ {:else if !selectedConfig.notes} +
+ + + + + +

No configuration fields available

+
+ {/if} + + + {#if selectedConfig.info} + + {/if} +
+ {/key} +
+ {/if} +
+ {/if} + {:else if activeTab === "Users"} +

Users content

+ {:else if activeTab === "Activity"} +

Activity content

+ {/if} +
+
+
diff --git a/src/styles/admin.css b/src/styles/admin.css new file mode 100644 index 0000000..266cc9b --- /dev/null +++ b/src/styles/admin.css @@ -0,0 +1,480 @@ +/* ═══════════════════════════════════════════════════ + Admin — Pane + Tab Bar Layout + ═══════════════════════════════════════════════════ */ + +/* Page container */ +.admin-page { + display: flex; + flex-direction: column; + flex: 1; + min-height: 0; + overflow: hidden; + width: 100%; +} + +/* ── Pane container ── */ +.admin-pane { + display: flex; + flex-direction: column; + flex: 1; + min-height: 0; + background: var(--bg-surface); + border-radius: 12px; + box-shadow: var(--header-shadow); + overflow: hidden; +} + +/* ── Pane header ── */ +.admin-header { + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 12px; + padding: 20px 24px 16px; + border-bottom: none; + flex-shrink: 0; +} + +.admin-header-left { + display: flex; + align-items: center; + gap: 12px; +} + +.admin-header-icon { + width: 22px; + height: 22px; + color: var(--text-secondary); + flex-shrink: 0; +} + +.admin-title { + margin: 0; + font-size: 20px; + font-weight: 600; + color: var(--text-primary); +} + +.admin-subtitle { + font-size: 13px; + font-weight: 500; + color: var(--text-secondary); + margin-left: 4px; +} + +/* ── Tab bar (mirrors companydetail.css) ── */ +.admin-pane .tab-bar { + display: flex; + align-items: stretch; + gap: 0; + padding: 0 24px; + border-bottom: 1px solid var(--border-subtle); + flex-shrink: 0; + background: var(--bg-surface); +} + +.admin-pane .tab-btn { + position: relative; + display: inline-flex; + align-items: center; + padding: 14px 16px 12px; + border: none; + background: none; + font-size: 13px; + font-weight: 500; + color: var(--text-secondary); + cursor: pointer; + transition: color 0.15s; + white-space: nowrap; + text-decoration: none; +} + +.admin-pane .tab-btn::after { + content: ""; + position: absolute; + bottom: -1px; + left: 0; + right: 0; + height: 2px; + border-radius: 2px 2px 0 0; + background: transparent; + transition: background 0.15s; +} + +.admin-pane .tab-btn:hover { + color: var(--text-primary); +} + +.admin-pane .tab-btn.active { + color: var(--text-primary); + font-weight: 600; +} + +.admin-pane .tab-btn.active::after { + background: var(--input-focus-border); +} + +/* ── Pane body (tab content area) ── */ +.admin-body { + position: relative; + flex: 1; + overflow-y: auto; + overflow-x: hidden; + padding: 24px; +} + +/* ── Stats grid ── */ +.stats-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); + gap: 16px; + margin-bottom: 32px; +} + +.stat-card { + display: flex; + align-items: flex-start; + gap: 14px; + padding: 20px; + background: var(--card-bg); + border: 1px solid var(--card-border); + border-radius: 10px; + transition: + background 0.2s, + border-color 0.2s, + box-shadow 0.2s; +} + +.stat-card:hover { + background: var(--card-hover-bg); + border-color: var(--card-hover-border); + box-shadow: var(--card-hover-shadow); +} + +.stat-icon { + display: flex; + align-items: center; + justify-content: center; + width: 42px; + height: 42px; + border-radius: 10px; + background: linear-gradient( + 135deg, + var(--avatar-gradient-from), + var(--avatar-gradient-to) + ); + flex-shrink: 0; +} + +.stat-icon svg { + width: 20px; + height: 20px; + color: var(--text-inverse); + stroke: currentColor; +} + +.stat-info { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; +} + +.stat-value { + font-size: 24px; + font-weight: 700; + color: var(--text-primary); + line-height: 1.2; +} + +.stat-label { + font-size: 13px; + font-weight: 500; + color: var(--text-secondary); +} + +/* ── Section headings ── */ +.section-heading { + margin: 0 0 16px; + font-size: 16px; + font-weight: 600; + color: var(--text-primary); +} + +/* ── Quick-action cards ── */ +.actions-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); + gap: 16px; + margin-bottom: 32px; +} + +.action-card { + display: flex; + align-items: center; + gap: 16px; + padding: 18px 20px; + background: var(--card-bg); + border: 1px solid var(--card-border); + border-radius: 10px; + cursor: pointer; + text-decoration: none; + color: inherit; + transition: + background 0.2s, + border-color 0.2s, + box-shadow 0.2s; + position: relative; +} + +.action-card:hover { + background: var(--card-hover-bg); + border-color: var(--card-hover-border); + box-shadow: var(--card-hover-shadow); +} + +.action-card:active { + box-shadow: var(--card-active-shadow); +} + +.action-icon { + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + border-radius: 10px; + background: var(--nav-active-bg); + flex-shrink: 0; +} + +.action-icon svg { + width: 20px; + height: 20px; + color: var(--nav-active-color); + stroke: currentColor; +} + +.action-text { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; +} + +.action-name { + font-size: 14px; + font-weight: 600; + color: var(--text-primary); +} + +.action-desc { + font-size: 12px; + color: var(--text-secondary); +} + +.action-arrow { + position: absolute; + right: 16px; + width: 16px; + height: 16px; + color: var(--card-arrow-color); + opacity: 0; + transform: translateX(-4px); + transition: + opacity 0.2s, + transform 0.2s; +} + +.action-card:hover .action-arrow { + opacity: 1; + transform: translateX(0); +} + +/* ── Activity placeholder ── */ +.activity-section { + margin-bottom: 32px; +} + +.activity-empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 40px 20px; + background: var(--bg-inset); + border: 1px dashed var(--border-default); + border-radius: 10px; + color: var(--text-muted); + font-size: 14px; + gap: 8px; +} + +.activity-empty svg { + width: 32px; + height: 32px; + color: var(--text-faint); +} + +/* ── Scrollbar ── */ +.admin-body::-webkit-scrollbar { + width: 6px; +} + +.admin-body::-webkit-scrollbar-track { + background: transparent; +} + +.admin-body::-webkit-scrollbar-thumb { + background: var(--scrollbar-thumb); + border-radius: 3px; +} + +.admin-body::-webkit-scrollbar-thumb:hover { + background: var(--scrollbar-thumb-hover); +} + +/* ═══════════════════════════════════════════════════ + Admin — Tab Empty State + ═══════════════════════════════════════════════════ */ + +.admin-tab-empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 60px 20px; + color: var(--text-muted); + font-size: 14px; + gap: 12px; +} + +.admin-tab-empty svg { + width: 40px; + height: 40px; + color: var(--text-faint); +} + +.admin-tab-empty h3 { + margin: 0; + font-size: 16px; + font-weight: 600; + color: var(--text-primary); +} + +.admin-tab-empty p { + margin: 0; + color: var(--text-secondary); + font-size: 13px; + text-align: center; + max-width: 360px; +} + +/* ═══════════════════════════════════════════════════ + Admin — Permission Denied State + ═══════════════════════════════════════════════════ */ + +.admin-denied { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 60px 20px; + gap: 12px; +} + +.admin-denied svg { + width: 40px; + height: 40px; + color: var(--status-inactive-color); +} + +.admin-denied h3 { + margin: 0; + font-size: 16px; + font-weight: 600; + color: var(--text-primary); +} + +.admin-denied p { + margin: 0; + color: var(--text-secondary); + font-size: 13px; + text-align: center; + max-width: 360px; +} + +/* ═══════════════════════════════════════════════════ + Admin — Data Tables (Users, Roles, Cred Types) + ═══════════════════════════════════════════════════ */ + +.admin-table-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 16px; + flex-wrap: wrap; + gap: 12px; +} + +.admin-table-header h3 { + margin: 0; + font-size: 16px; + font-weight: 600; + color: var(--text-primary); +} + +.admin-table-header .result-count { + font-size: 13px; + font-weight: 500; + color: var(--text-secondary); + margin-left: 8px; +} + +.admin-table-wrap { + border: 1px solid var(--card-border); + border-radius: 10px; + overflow: hidden; +} + +.admin-table { + width: 100%; + border-collapse: collapse; + font-size: 13px; +} + +.admin-table thead { + background: var(--bg-inset); +} + +.admin-table th { + padding: 10px 16px; + text-align: left; + font-size: 12px; + font-weight: 600; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 0.04em; + border-bottom: 1px solid var(--card-border); + white-space: nowrap; +} + +.admin-table td { + padding: 12px 16px; + color: var(--text-primary); + border-bottom: 1px solid var(--border-subtle); + vertical-align: middle; +} + +.admin-table tbody tr { + transition: background 0.15s; +} + +.admin-table tbody tr:hover { + background: var(--card-hover-bg); +} + +.admin-table tbody tr:last-child td { + border-bottom: none; +} diff --git a/src/styles/app.css b/src/styles/app.css index 2eb463b..a54263c 100644 --- a/src/styles/app.css +++ b/src/styles/app.css @@ -2,6 +2,226 @@ @plugin '@tailwindcss/forms'; @plugin '@tailwindcss/typography'; +/* ═══════════════════════════════════════════════════ + Theme Variables + ═══════════════════════════════════════════════════ */ + +:root, +[data-theme="light"] { + /* Surfaces */ + --bg-base: #f0f0f0; + --bg-surface: #ffffff; + --bg-surface-alt: #f8f9fb; + --bg-elevated: #ffffff; + --bg-inset: #f9fafb; + --bg-gradient-from: #3498db; + --bg-gradient-to: #2980b9; + + /* Borders */ + --border-default: #e0e0e0; + --border-subtle: #eef0f3; + --border-strong: #d1d5db; + + /* Text */ + --text-primary: #2c3e50; + --text-secondary: #666666; + --text-muted: #8492a6; + --text-faint: #9ca3af; + --text-inverse: #ffffff; + + /* Nav */ + --nav-hover-bg: rgba(0, 0, 0, 0.04); + --nav-active-bg: rgba(52, 152, 219, 0.08); + --nav-active-color: #3498db; + --nav-active-border: #3498db; + + /* Header */ + --header-bg: #ffffff; + --header-text: #2c3e50; + --header-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + --accent-bar-from: #2980b9; + --accent-bar-to: #3498db; + --accent-bar-height: 0; + + /* Cards */ + --card-bg: #f8f9fb; + --card-hover-bg: #ffffff; + --card-border: #eef0f3; + --card-hover-border: #d0d9e8; + --card-hover-shadow: + 0 8px 24px rgba(0, 0, 0, 0.08), 0 2px 8px rgba(0, 0, 0, 0.04); + --card-active-shadow: 0 4px 12px rgba(0, 0, 0, 0.06); + --card-focus-ring: #3498db; + --card-arrow-color: #cbd5e1; + + /* Avatar */ + --avatar-gradient-from: #3498db; + --avatar-gradient-to: #2980b9; + + /* Inputs */ + --input-bg: #f9fafb; + --input-focus-bg: #ffffff; + --input-border: #d1d5db; + --input-focus-border: #3498db; + --input-focus-ring: rgba(52, 152, 219, 0.12); + --input-text: #374151; + --input-placeholder: #9ca3af; + + /* Pagination */ + --page-btn-bg: #ffffff; + --page-btn-border: #d1d5db; + --page-btn-hover-bg: #f3f4f6; + --page-btn-hover-border: #9ca3af; + --page-btn-active-bg: #2c3e50; + --page-btn-active-border: #2c3e50; + --page-btn-active-color: #ffffff; + --page-btn-color: #374151; + + /* Overlay */ + --overlay-bg: rgba(255, 255, 255, 0.8); + --spinner-track: #eef0f3; + --spinner-accent: #3498db; + + /* Scrollbar */ + --scrollbar-thumb: rgba(0, 0, 0, 0.15); + --scrollbar-thumb-hover: rgba(0, 0, 0, 0.25); + + /* Status labels */ + --status-active-bg: #dcfce7; + --status-active-color: #15803d; + --status-inactive-bg: #fee2e2; + --status-inactive-color: #b91c1c; + --status-pending-bg: #fef3c7; + --status-pending-color: #a16207; + --status-reserved-bg: #dbeafe; + --status-reserved-color: #1d4ed8; + --status-provisioning-bg: #fef3c7; + --status-provisioning-color: #a16207; + --status-neutral-bg: #f1f5f9; + --status-neutral-color: #64748b; + --status-neutral-dot: #d1d5db; + --status-neutral-dot-ring: rgba(209, 213, 219, 0.3); + + /* Toggle */ + --toggle-bg: rgba(0, 0, 0, 0.08); + --toggle-hover-bg: rgba(0, 0, 0, 0.12); + --toggle-color: #666666; + + /* Error illustration */ + --error-circle-outer: #fef2f2; + --error-circle-stroke: #fecaca; + --error-circle-inner: #fee2e2; + + color-scheme: light; +} + +[data-theme="dark"] { + /* Surfaces */ + --bg-base: #0e0e0e; + --bg-surface: #1a1a1a; + --bg-surface-alt: #141414; + --bg-elevated: #1c1c1c; + --bg-inset: #111111; + --bg-gradient-from: #12161e; + --bg-gradient-to: #181d28; + + /* Borders */ + --border-default: #2a2a2a; + --border-subtle: #262626; + --border-strong: #333333; + + /* Text */ + --text-primary: #e0e0e0; + --text-secondary: #737373; + --text-muted: #737373; + --text-faint: #525252; + --text-inverse: #0e0e0e; + + /* Nav */ + --nav-hover-bg: rgba(255, 255, 255, 0.04); + --nav-active-bg: rgba(255, 255, 255, 0.06); + --nav-active-color: #ffffff; + --nav-active-border: #ffffff; + + /* Header */ + --header-bg: #1a1a1a; + --header-text: #e0e0e0; + --header-shadow: none; + --accent-bar-from: #1a2a44; + --accent-bar-to: #0e1b33; + --accent-bar-height: 0; + + /* Cards */ + --card-bg: #141414; + --card-hover-bg: #1c1c1c; + --card-border: #262626; + --card-hover-border: #3d3d3d; + --card-hover-shadow: 0 8px 24px rgba(0, 0, 0, 0.4); + --card-active-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + --card-focus-ring: #a3a3a3; + --card-arrow-color: #a3a3a3; + + /* Avatar */ + --avatar-gradient-from: #333333; + --avatar-gradient-to: #262626; + + /* Inputs */ + --input-bg: #111111; + --input-focus-bg: #161616; + --input-border: #333333; + --input-focus-border: #525252; + --input-focus-ring: rgba(255, 255, 255, 0.04); + --input-text: #d4d4d4; + --input-placeholder: #525252; + + /* Pagination */ + --page-btn-bg: #1a1a1a; + --page-btn-border: #333333; + --page-btn-hover-bg: #262626; + --page-btn-hover-border: #404040; + --page-btn-active-bg: #ffffff; + --page-btn-active-border: #ffffff; + --page-btn-active-color: #0e0e0e; + --page-btn-color: #a3a3a3; + + /* Overlay */ + --overlay-bg: rgba(14, 14, 14, 0.85); + --spinner-track: #262626; + --spinner-accent: #a3a3a3; + + /* Scrollbar */ + --scrollbar-thumb: rgba(255, 255, 255, 0.15); + --scrollbar-thumb-hover: rgba(255, 255, 255, 0.3); + + /* Status labels */ + --status-active-bg: rgba(34, 197, 94, 0.12); + --status-active-color: #4ade80; + --status-inactive-bg: rgba(239, 68, 68, 0.12); + --status-inactive-color: #f87171; + --status-pending-bg: rgba(245, 158, 11, 0.12); + --status-pending-color: #fbbf24; + --status-reserved-bg: rgba(59, 130, 246, 0.12); + --status-reserved-color: #60a5fa; + --status-provisioning-bg: rgba(245, 158, 11, 0.12); + --status-provisioning-color: #fbbf24; + --status-neutral-bg: rgba(255, 255, 255, 0.06); + --status-neutral-color: #737373; + --status-neutral-dot: #525252; + --status-neutral-dot-ring: rgba(82, 82, 82, 0.3); + + /* Toggle */ + --toggle-bg: rgba(255, 255, 255, 0.08); + --toggle-hover-bg: rgba(255, 255, 255, 0.12); + --toggle-color: #a3a3a3; + + /* Error illustration */ + --error-circle-outer: rgba(239, 68, 68, 0.08); + --error-circle-stroke: rgba(239, 68, 68, 0.2); + --error-circle-inner: rgba(239, 68, 68, 0.12); + + color-scheme: dark; +} + body { margin: 0; padding: 0; @@ -9,3 +229,47 @@ body { -webkit-tap-highlight-color: transparent; overflow: hidden; } + +/* ═══════════════════════════════════════════════════ + Theme Toggle Button + ═══════════════════════════════════════════════════ */ + +.theme-toggle { + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; + border: none; + border-radius: 8px; + background: var(--toggle-bg); + color: var(--toggle-color); + cursor: pointer; + transition: + background 0.2s, + color 0.2s; + margin-left: auto; + flex-shrink: 0; +} + +.theme-toggle:hover { + background: var(--toggle-hover-bg); + color: var(--text-primary); +} + +.theme-icon { + width: 18px; + height: 18px; + position: absolute; + opacity: 0; + transform: scale(0.6) rotate(-90deg); + transition: + opacity 0.25s ease, + transform 0.25s ease; +} + +.theme-icon.visible { + opacity: 1; + transform: scale(1) rotate(0deg); +} diff --git a/src/styles/companies/companydetail.css b/src/styles/companies/companydetail.css new file mode 100644 index 0000000..3103270 --- /dev/null +++ b/src/styles/companies/companydetail.css @@ -0,0 +1,925 @@ +/* ═══════════════════════════════════════════════════ + Company Detail — Two-Pane Layout + ═══════════════════════════════════════════════════ */ + +/* Page container */ +.company-detail-page { + display: flex; + flex: 1; + min-height: 0; + overflow: hidden; + width: 100%; + gap: 16px; +} + +/* ── Left pane (1/4 width) ── */ +.company-detail-left { + display: flex; + flex-direction: column; + flex: 0 0 25%; + min-height: 0; + background: var(--bg-surface); + border-radius: 12px; + box-shadow: var(--header-shadow); + overflow: hidden; +} + +/* ── Right pane (3/4 width) ── */ +.company-detail-right { + display: flex; + flex-direction: column; + flex: 1; + min-height: 0; + background: var(--bg-surface); + border-radius: 12px; + box-shadow: var(--header-shadow); + overflow: hidden; +} + +/* ── Pane header (reusable for both panes) ── */ +.detail-pane-header { + display: flex; + align-items: center; + gap: 12px; + padding: 20px 24px 16px; + border-bottom: 1px solid var(--border-subtle); + flex-shrink: 0; +} + +.detail-pane-title { + margin: 0; + font-size: 18px; + font-weight: 600; + color: var(--text-primary); +} + +/* ── Back button ── */ +.back-btn { + display: inline-flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border: 1px solid var(--border-subtle); + border-radius: 8px; + background: var(--bg-surface); + color: var(--text-secondary); + cursor: pointer; + transition: all 0.15s; + flex-shrink: 0; +} + +.back-btn:hover { + background: var(--nav-hover-bg); + color: var(--text-primary); + border-color: var(--input-focus-border); +} + +/* ═══════════════════════════════════════════════════ + Right Pane — Tab Bar + ═══════════════════════════════════════════════════ */ + +.tab-bar { + display: flex; + align-items: stretch; + gap: 0; + padding: 0 24px; + border-bottom: 1px solid var(--border-subtle); + flex-shrink: 0; + background: var(--bg-surface); +} + +.tab-btn { + position: relative; + padding: 14px 16px 12px; + border: none; + background: none; + font-size: 13px; + font-weight: 500; + color: var(--text-secondary); + cursor: pointer; + transition: color 0.15s; + white-space: nowrap; +} + +.tab-btn::after { + content: ""; + position: absolute; + bottom: -1px; + left: 0; + right: 0; + height: 2px; + border-radius: 2px 2px 0 0; + background: transparent; + transition: background 0.15s; +} + +.tab-btn:hover { + color: var(--text-primary); +} + +.tab-btn.active { + color: var(--text-primary); + font-weight: 600; +} + +.tab-btn.active::after { + background: var(--input-focus-border); +} + +.tab-count-badge { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 18px; + height: 18px; + padding: 0 5px; + margin-left: 6px; + border-radius: 10px; + background: var(--status-neutral-bg); + color: var(--status-neutral-color); + font-size: 11px; + font-weight: 600; + line-height: 1; +} + +.tab-btn.active .tab-count-badge { + background: var(--input-focus-border); + color: #fff; +} + +.tab-placeholder { + color: var(--text-secondary); + font-size: 14px; +} + +/* ── Pane body (reusable for both panes) ── */ +.detail-pane-body { + position: relative; + flex: 1; + overflow-y: auto; + overflow-x: hidden; + padding: 20px 24px; +} + +/* ═══════════════════════════════════════════════════ + Left Pane — Company Profile + ═══════════════════════════════════════════════════ */ + +/* Profile header (avatar + name + status) */ +.profile-header { + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border-subtle); + margin-bottom: 20px; + margin-top: 16px; +} + +.profile-avatar { + width: 64px; + height: 64px; + border-radius: 16px; + background: linear-gradient( + 135deg, + var(--avatar-gradient-from), + var(--avatar-gradient-to) + ); + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.profile-initials { + font-size: 22px; + font-weight: 700; + color: #fff; + letter-spacing: 0.5px; + line-height: 1; +} + +.profile-name { + margin: 0; + font-size: 18px; + font-weight: 600; + color: var(--text-primary); + text-align: center; + word-break: break-word; +} + +.profile-status { + font-size: 11px; + font-weight: 600; + padding: 3px 12px; + border-radius: 20px; + text-transform: capitalize; + letter-spacing: 0.02em; +} + +.profile-status.active { + background: var(--status-active-bg); + color: var(--status-active-color); +} + +.profile-status.inactive { + background: var(--status-inactive-bg); + color: var(--status-inactive-color); +} + +.profile-status.pending { + background: var(--status-pending-bg); + color: var(--status-pending-color); +} + +.profile-status.neutral { + background: var(--status-neutral-bg); + color: var(--status-neutral-color); +} + +/* Info rows */ +.profile-info { + display: flex; + flex-direction: column; + gap: 4px; +} + +.info-row { + display: flex; + align-items: flex-start; + gap: 12px; + padding: 10px 8px; + border-radius: 8px; + transition: background 0.15s; +} + +.info-row:hover { + background: var(--nav-hover-bg); +} + +.info-icon { + width: 16px; + height: 16px; + flex-shrink: 0; + color: var(--text-faint); + margin-top: 2px; +} + +.info-content { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; +} + +.info-label { + font-size: 11px; + font-weight: 500; + color: var(--text-faint); + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.info-value { + font-size: 13px; + font-weight: 500; + color: var(--text-primary); + word-break: break-word; +} + +.info-value.mono { + font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace; + font-size: 12px; + letter-spacing: -0.02em; +} + +.info-value.address-multiline { + line-height: 1.6; +} + +/* Empty state */ +.profile-empty { + display: flex; + justify-content: center; + align-items: center; + flex: 1; + padding: 48px 16px; + color: var(--text-secondary); + font-size: 14px; +} + +/* ═══════════════════════════════════════════════════ + Tab Content — Shared + ═══════════════════════════════════════════════════ */ + +.tab-empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 12px; + padding: 64px 16px; + color: var(--text-secondary); + font-size: 14px; +} + +.tab-empty p { + margin: 0; +} + +.tab-empty-icon { + width: 40px; + height: 40px; + color: var(--text-faint); +} + +/* ═══════════════════════════════════════════════════ + Tab Content — Configurations + ═══════════════════════════════════════════════════ */ + +/* Split container */ +.config-split { + display: flex; + gap: 0; + height: 100%; + min-height: 0; +} + +.config-split.expanded { + gap: 16px; +} + +.config-list { + display: flex; + flex-direction: column; + gap: 10px; + transition: flex 0.35s cubic-bezier(0.4, 0, 0.2, 1); + flex: 1 1 100%; + min-width: 0; + overflow-y: auto; +} + +.config-list.collapsed { + flex: 0 0 220px; + max-width: 220px; + gap: 6px; +} + +.config-item { + display: flex; + flex-direction: column; + gap: 8px; + padding: 14px 16px; + border: 1px solid var(--card-border); + border-radius: 10px; + background: var(--card-bg); + cursor: pointer; + text-align: left; + width: 100%; + font-family: inherit; + font-size: inherit; + transition: + border-color 0.15s, + background 0.15s, + padding 0.3s cubic-bezier(0.4, 0, 0.2, 1), + gap 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.config-list.collapsed .config-item { + padding: 10px 12px; + gap: 0; +} + +.config-item:hover { + border-color: var(--card-hover-border); + background: var(--card-hover-bg); +} + +.config-item.selected { + border-color: var(--input-focus-border); + background: var(--card-hover-bg); + border-radius: 14px; +} + +.config-item-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.config-name-group { + display: flex; + align-items: center; + gap: 8px; + min-width: 0; +} + +.config-status-dot { + width: 8px; + height: 8px; + border-radius: 50%; + flex-shrink: 0; +} + +.config-status-dot.dot-active { + background: var(--status-active-color); + box-shadow: 0 0 0 2px var(--status-active-bg); +} + +.config-status-dot.dot-inactive { + background: var(--status-inactive-color); + box-shadow: 0 0 0 2px var(--status-inactive-bg); +} + +.config-status-dot.dot-reserved { + background: var(--status-reserved-color); + box-shadow: 0 0 0 2px var(--status-reserved-bg); +} + +.config-status-dot.dot-pending { + background: var(--status-pending-color); + box-shadow: 0 0 0 2px var(--status-pending-bg); +} + +.config-status-dot.dot-neutral { + background: var(--status-neutral-dot); + box-shadow: 0 0 0 2px var(--status-neutral-dot-ring); +} + +/* Config header badges wrapper */ +.config-header-badges { + display: flex; + align-items: center; + gap: 6px; + flex-shrink: 0; +} + +/* Config status badge (in list items) */ +.config-status-badge { + font-size: 10px; + font-weight: 600; + padding: 2px 8px; + border-radius: 20px; + white-space: nowrap; + letter-spacing: 0.02em; +} + +.config-status-badge.status-active { + background: var(--status-active-bg); + color: var(--status-active-color); +} + +.config-status-badge.status-inactive { + background: var(--status-inactive-bg); + color: var(--status-inactive-color); +} + +.config-status-badge.status-reserved { + background: var(--status-reserved-bg); + color: var(--status-reserved-color); +} + +.config-status-badge.status-pending { + background: var(--status-pending-bg); + color: var(--status-pending-color); +} + +.config-status-badge.status-neutral { + background: var(--status-neutral-bg); + color: var(--status-neutral-color); +} + +.config-item.config-inactive { + opacity: 0.55; +} + +.config-item.config-inactive:hover { + opacity: 0.8; +} + +.config-item.config-inactive.selected { + opacity: 0.85; +} + +.config-name { + font-size: 14px; + font-weight: 600; + color: var(--text-primary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.config-list.collapsed .config-name { + font-size: 13px; +} + +.config-type-badge { + font-size: 11px; + font-weight: 500; + padding: 2px 10px; + border-radius: 20px; + background: var(--status-neutral-bg); + color: var(--status-neutral-color); + white-space: nowrap; + flex-shrink: 0; +} + +.config-description { + margin: 0; + font-size: 13px; + color: var(--text-secondary); + line-height: 1.4; +} + +.config-kv { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 10px; + border-radius: 6px; + background: var(--nav-hover-bg); + font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace; + font-size: 12px; + overflow-x: auto; +} + +.config-key { + color: var(--text-secondary); + font-weight: 500; + flex-shrink: 0; +} + +.config-key::after { + content: ":"; + margin-left: 2px; +} + +.config-value { + color: var(--text-primary); + word-break: break-all; +} + +.config-date { + font-size: 11px; + color: var(--text-faint); +} + +/* ═══════════════════════════════════════════════════ + Config Detail Panel (right side) + ═══════════════════════════════════════════════════ */ + +.config-detail-panel { + flex: 1 1 0%; + min-width: 0; + overflow: hidden; + border: 1px solid var(--border-subtle); + border-radius: 10px; + background: var(--card-bg); + display: flex; + flex-direction: column; +} + +/* Fade-in animation via {#key} */ +.config-detail-content { + display: flex; + flex-direction: column; + flex: 1; + padding: 20px 24px; + overflow-y: auto; + animation: configFadeIn 0.3s ease-out; +} + +@keyframes configFadeIn { + from { + opacity: 0; + transform: translateY(6px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.config-detail-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-bottom: 20px; + padding-bottom: 16px; + border-bottom: 1px solid var(--border-subtle); +} + +.config-detail-title { + margin: 0; + font-size: 16px; + font-weight: 600; + color: var(--text-primary); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.config-detail-close { + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border: 1px solid var(--border-subtle); + border-radius: 6px; + background: var(--bg-surface); + color: var(--text-secondary); + cursor: pointer; + flex-shrink: 0; + transition: all 0.15s; +} + +.config-detail-close:hover { + background: var(--nav-hover-bg); + color: var(--text-primary); + border-color: var(--input-focus-border); +} + +.config-detail-json { + margin: 0; + padding: 16px; + border-radius: 8px; + background: var(--nav-hover-bg); + color: var(--text-primary); + font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace; + font-size: 12px; + line-height: 1.6; + white-space: pre-wrap; + word-break: break-all; + overflow-y: auto; + flex: 1; +} + +/* ═══════════════════════════════════════════════════ + Config Detail — Structured Questions View + ═══════════════════════════════════════════════════ */ + +/* Header with badges */ +.config-detail-header-left { + display: flex; + flex-direction: column; + gap: 8px; + min-width: 0; + flex: 1; +} + +.config-detail-meta-badges { + display: flex; + align-items: center; + gap: 6px; + flex-wrap: wrap; +} + +.config-badge { + font-size: 11px; + font-weight: 600; + padding: 2px 10px; + border-radius: 20px; + white-space: nowrap; +} + +.config-badge.type { + background: var(--status-neutral-bg); + color: var(--status-neutral-color); +} + +.config-badge.active-badge, +.config-badge.status-active { + background: var(--status-active-bg); + color: var(--status-active-color); +} + +.config-badge.inactive-badge, +.config-badge.status-inactive { + background: var(--status-inactive-bg); + color: var(--status-inactive-color); +} + +.config-badge.status-reserved { + background: var(--status-reserved-bg); + color: var(--status-reserved-color); +} + +.config-badge.status-pending { + background: var(--status-pending-bg); + color: var(--status-pending-color); +} + +.config-badge.status-neutral { + background: var(--status-neutral-bg); + color: var(--status-neutral-color); +} + +/* Serial number */ +.config-serial { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 12px; + margin-bottom: 16px; + border-radius: 8px; + background: var(--nav-hover-bg); + font-size: 13px; +} + +.config-serial-label { + font-weight: 600; + color: var(--text-faint); + text-transform: uppercase; + font-size: 11px; + letter-spacing: 0.04em; + flex-shrink: 0; +} + +.config-serial-value { + font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace; + color: var(--text-primary); + font-size: 12px; + word-break: break-all; +} + +/* Notes */ +.config-notes { + margin-bottom: 20px; +} + +.config-notes-text { + margin: 0; + padding: 10px 14px; + border-radius: 8px; + background: var(--nav-hover-bg); + color: var(--text-primary); + font-size: 13px; + line-height: 1.6; + white-space: pre-wrap; + word-break: break-word; +} + +/* Section title */ +.config-section-title { + display: flex; + align-items: center; + gap: 8px; + margin: 0 0 14px; + font-size: 13px; + font-weight: 600; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.config-section-title svg { + color: var(--text-faint); + flex-shrink: 0; +} + +/* Questions grid */ +.config-questions { + flex: 1; + min-height: 0; + overflow-y: auto; +} + +.questions-grid { + display: flex; + flex-direction: column; + gap: 2px; +} + +.question-row { + display: flex; + flex-direction: column; + gap: 4px; + padding: 10px 12px; + border-radius: 8px; + transition: background 0.15s; +} + +.question-row:hover { + background: var(--nav-hover-bg); +} + +.question-label { + font-size: 11px; + font-weight: 600; + color: var(--text-faint); + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.question-value-wrap { + display: flex; + align-items: flex-start; + gap: 8px; +} + +.question-value { + font-size: 13px; + font-weight: 500; + color: var(--text-primary); + word-break: break-word; +} + +.question-row:not(.has-answer) .question-value { + color: var(--text-faint); + font-style: italic; + font-weight: 400; +} + +.question-value.textarea-value { + white-space: pre-wrap; + line-height: 1.6; + font-size: 13px; + padding: 6px 10px; + border-radius: 6px; + background: var(--nav-hover-bg); + width: 100%; +} + +.question-row:hover .question-value.textarea-value { + background: var(--card-bg); +} + +.question-value.password-value { + font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace; + font-size: 13px; + letter-spacing: 0.05em; +} + +/* Password toggle */ +.password-toggle { + display: inline-flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + border: 1px solid var(--border-subtle); + border-radius: 5px; + background: var(--bg-surface); + color: var(--text-secondary); + cursor: pointer; + flex-shrink: 0; + transition: all 0.15s; +} + +.password-toggle:hover { + background: var(--nav-hover-bg); + color: var(--text-primary); + border-color: var(--input-focus-border); +} + +/* No questions placeholder */ +.config-no-questions { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 10px; + padding: 40px 16px; + color: var(--text-faint); + font-size: 13px; + flex: 1; +} + +.config-no-questions p { + margin: 0; +} + +/* Footer metadata */ +.config-info-footer { + display: flex; + flex-direction: column; + gap: 6px; + margin-top: auto; + padding-top: 16px; + border-top: 1px solid var(--border-subtle); +} + +.config-info-item { + display: flex; + align-items: center; + gap: 6px; + font-size: 11px; + color: var(--text-faint); + line-height: 1.4; +} + +.config-info-item svg { + flex-shrink: 0; + color: var(--text-faint); +} + +.config-info-item strong { + font-weight: 600; + color: var(--text-secondary); +} diff --git a/src/styles/companies/companylist.css b/src/styles/companies/companylist.css index 06f1897..f42183f 100644 --- a/src/styles/companies/companylist.css +++ b/src/styles/companies/companylist.css @@ -18,11 +18,9 @@ flex-direction: column; flex: 1; min-height: 0; - background: #ffffff; + background: var(--bg-surface); border-radius: 12px; - box-shadow: - 0 4px 24px rgba(0, 0, 0, 0.08), - 0 1px 4px rgba(0, 0, 0, 0.04); + box-shadow: var(--header-shadow); overflow: hidden; } @@ -34,7 +32,7 @@ flex-wrap: wrap; gap: 12px; padding: 20px 24px 16px; - border-bottom: 1px solid #eef0f3; + border-bottom: 1px solid var(--border-subtle); flex-shrink: 0; } @@ -48,13 +46,13 @@ margin: 0; font-size: 20px; font-weight: 600; - color: #2c3e50; + color: var(--text-primary); } .result-count { font-size: 13px; font-weight: 500; - color: #8492a6; + color: var(--text-secondary); } .search-bar { @@ -65,25 +63,25 @@ .search-bar input { width: 100%; padding: 9px 34px 9px 38px; - border: 1px solid #d1d5db; + border: 1px solid var(--input-border); border-radius: 8px; font-size: 14px; outline: none; - background: #f9fafb; - color: #374151; + background: var(--input-bg); + color: var(--input-text); transition: border-color 0.2s, box-shadow 0.2s; } .search-bar input::placeholder { - color: #9ca3af; + color: var(--input-placeholder); } .search-bar input:focus { - border-color: #3498db; - box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.12); - background: #fff; + border-color: var(--input-focus-border); + box-shadow: 0 0 0 3px var(--input-focus-ring); + background: var(--input-focus-bg); } .search-icon { @@ -93,7 +91,7 @@ transform: translateY(-50%); width: 16px; height: 16px; - color: #9ca3af; + color: var(--text-faint); pointer-events: none; } @@ -104,7 +102,7 @@ transform: translateY(-50%); background: none; border: none; - color: #9ca3af; + color: var(--text-faint); cursor: pointer; padding: 4px; display: flex; @@ -117,8 +115,8 @@ } .search-clear:hover { - color: #374151; - background: #f3f4f6; + color: var(--input-text); + background: var(--nav-hover-bg); } /* ── Pane body ── */ @@ -134,7 +132,7 @@ .search-loading-overlay { position: absolute; inset: 0; - background: rgba(255, 255, 255, 0.8); + background: var(--overlay-bg); display: flex; align-items: center; justify-content: center; @@ -146,8 +144,8 @@ width: 36px; height: 36px; border-radius: 50%; - border: 4px solid #eef0f3; - border-top-color: #3498db; + border: 4px solid var(--spinner-track); + border-top-color: var(--spinner-accent); animation: search-spin 0.7s linear infinite; } @@ -184,9 +182,9 @@ flex-direction: column; gap: 14px; padding: 18px; - background: #f8f9fb; + background: var(--card-bg); border-radius: 12px; - border: 1px solid #eef0f3; + border: 1px solid var(--card-border); box-shadow: none; cursor: pointer; transition: @@ -196,26 +194,25 @@ background 0.18s; text-align: left; font: inherit; - color: inherit; + color: var(--input-text); overflow: hidden; width: 100%; } .company-card:hover { transform: translateY(-3px); - box-shadow: - 0 8px 24px rgba(0, 0, 0, 0.08), - 0 2px 8px rgba(0, 0, 0, 0.04); - border-color: #d0d9e8; + box-shadow: var(--card-hover-shadow); + border-color: var(--card-hover-border); + background: var(--card-hover-bg); } .company-card:active { transform: translateY(-1px); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06); + box-shadow: var(--card-active-shadow); } .company-card:focus-visible { - outline: 2px solid #3498db; + outline: 2px solid var(--card-focus-ring); outline-offset: 2px; } @@ -230,7 +227,11 @@ width: 44px; height: 44px; border-radius: 12px; - background: linear-gradient(135deg, #3498db, #2980b9); + background: linear-gradient( + 135deg, + var(--avatar-gradient-from), + var(--avatar-gradient-to) + ); display: flex; align-items: center; justify-content: center; @@ -270,8 +271,8 @@ } .status-dot.neutral { - background: #d1d5db; - box-shadow: 0 0 0 3px rgba(209, 213, 219, 0.3); + background: var(--status-neutral-dot); + box-shadow: 0 0 0 3px var(--status-neutral-dot-ring); } /* Card body */ @@ -286,7 +287,7 @@ margin: 0; font-size: 16px; font-weight: 600; - color: #1e293b; + color: var(--text-primary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; @@ -295,7 +296,7 @@ .card-email { font-size: 13px; - color: #94a3b8; + color: var(--text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; @@ -313,14 +314,14 @@ align-items: center; gap: 5px; font-size: 12px; - color: #64748b; + color: var(--text-secondary); } .meta-icon { width: 14px; height: 14px; flex-shrink: 0; - color: #94a3b8; + color: var(--text-faint); } .meta-item .mono { @@ -336,7 +337,7 @@ justify-content: space-between; gap: 8px; padding-top: 10px; - border-top: 1px solid #f1f5f9; + border-top: 1px solid var(--border-subtle); margin-top: auto; } @@ -350,28 +351,28 @@ } .status-label.active { - background: #dcfce7; - color: #15803d; + background: var(--status-active-bg); + color: var(--status-active-color); } .status-label.inactive { - background: #fee2e2; - color: #b91c1c; + background: var(--status-inactive-bg); + color: var(--status-inactive-color); } .status-label.pending { - background: #fef3c7; - color: #a16207; + background: var(--status-pending-bg); + color: var(--status-pending-color); } .status-label.neutral { - background: #f1f5f9; - color: #64748b; + background: var(--status-neutral-bg); + color: var(--status-neutral-color); } .card-date { font-size: 12px; - color: #94a3b8; + color: var(--text-faint); margin-left: auto; } @@ -382,7 +383,7 @@ right: 16px; width: 18px; height: 18px; - color: #cbd5e1; + color: var(--card-arrow-color); opacity: 0; transform: translateX(-4px); transition: @@ -405,14 +406,14 @@ justify-content: space-between; gap: 16px; padding: 12px 24px; - border-top: 1px solid #eef0f3; - background: #f8f9fb; + border-top: 1px solid var(--border-subtle); + background: var(--bg-surface-alt); flex-shrink: 0; } .page-info { font-size: 13px; - color: #8492a6; + color: var(--text-secondary); } .pagination { @@ -428,10 +429,10 @@ min-width: 32px; height: 32px; padding: 0 6px; - border: 1px solid #d1d5db; + border: 1px solid var(--page-btn-border); border-radius: 6px; - background: #fff; - color: #374151; + background: var(--page-btn-bg); + color: var(--page-btn-color); font-size: 13px; font-weight: 500; cursor: pointer; @@ -439,19 +440,19 @@ } .page-btn:hover:not(:disabled):not(.active) { - background: #f3f4f6; - border-color: #9ca3af; + background: var(--page-btn-hover-bg); + border-color: var(--page-btn-hover-border); } .page-btn:disabled { - opacity: 0.4; + opacity: 0.3; cursor: not-allowed; } .page-btn.active { - background: #3498db; - border-color: #3498db; - color: #fff; + background: var(--page-btn-active-bg); + border-color: var(--page-btn-active-border); + color: var(--page-btn-active-color); font-weight: 600; } @@ -461,7 +462,7 @@ justify-content: center; width: 32px; height: 32px; - color: #9ca3af; + color: var(--text-faint); font-size: 13px; user-select: none; } diff --git a/src/styles/errorpage.css b/src/styles/errorpage.css index e30ac5b..8de740f 100644 --- a/src/styles/errorpage.css +++ b/src/styles/errorpage.css @@ -18,7 +18,7 @@ flex-direction: column; flex: 1; min-height: 0; - background: #ffffff; + background: var(--bg-surface); border-radius: 12px; box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08), @@ -32,7 +32,7 @@ align-items: center; gap: 12px; padding: 20px 24px 16px; - border-bottom: 1px solid #eef0f3; + border-bottom: 1px solid var(--border-subtle); flex-shrink: 0; } @@ -40,7 +40,7 @@ margin: 0; font-size: 20px; font-weight: 600; - color: #2c3e50; + color: var(--text-primary); } .error-status-badge { @@ -48,8 +48,8 @@ font-weight: 600; padding: 3px 10px; border-radius: 20px; - background: #fee2e2; - color: #b91c1c; + background: var(--status-inactive-bg); + color: var(--status-inactive-color); letter-spacing: 0.02em; } @@ -76,7 +76,7 @@ margin: 0; font-size: 22px; font-weight: 700; - color: #1e293b; + color: var(--text-primary); line-height: 1.3; } @@ -84,7 +84,7 @@ .error-message { margin: 0; font-size: 15px; - color: #64748b; + color: var(--text-secondary); line-height: 1.6; max-width: 420px; } @@ -92,7 +92,7 @@ .error-hint { margin: 0; font-size: 13px; - color: #94a3b8; + color: var(--text-muted); line-height: 1.5; max-width: 420px; } @@ -139,14 +139,14 @@ } .btn-secondary { - background: #f1f5f9; - color: #475569; - border: 1px solid #e2e8f0; + background: var(--nav-hover-bg); + color: var(--text-secondary); + border: 1px solid var(--border-default); } .btn-secondary:hover { - background: #e2e8f0; - border-color: #cbd5e1; + background: var(--nav-active-bg); + border-color: var(--border-strong); transform: translateY(-1px); } diff --git a/src/styles/layout.css b/src/styles/layout.css index 1c71e95..06d5130 100644 --- a/src/styles/layout.css +++ b/src/styles/layout.css @@ -9,12 +9,12 @@ /* Header */ .header { height: 60px; - background-color: #ffffff; - color: #2c3e50; + background: var(--header-bg); + color: var(--header-text); display: flex; align-items: center; padding: 0 20px; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + box-shadow: var(--header-shadow); flex-shrink: 0; z-index: 100; } @@ -30,6 +30,7 @@ font-size: 24px; font-weight: 600; letter-spacing: 1px; + color: var(--header-text); } /* Layout Wrapper */ @@ -42,9 +43,9 @@ /* Sidebar */ .sidebar { width: 72px; - background-color: #ffffff; - border-right: 1px solid #e0e0e0; - box-shadow: 2px 0 4px rgba(0, 0, 0, 0.1); + background-color: var(--bg-surface-alt); + border-right: 1px solid var(--border-default); + box-shadow: none; flex-shrink: 0; overflow-y: auto; overflow-x: hidden; @@ -71,7 +72,7 @@ justify-content: center; width: 100%; padding: 14px 0; - color: #666666; + color: var(--text-secondary); text-decoration: none; transition: all 0.2s ease; position: relative; @@ -82,28 +83,28 @@ } .nav-item:hover { - background-color: #f0f4ff; - color: #2c3e50; + background-color: var(--nav-hover-bg); + color: var(--text-primary); } .nav-item:active { - background-color: #e0eaff; - color: #3498db; + background-color: var(--nav-active-bg); + color: var(--nav-active-color); } /* Active page indicator */ .nav-item.active { - color: #3498db; - background-color: #eef4ff; - border-left: 3px solid #3498db; + color: var(--nav-active-color); + background-color: var(--nav-active-bg); + border-left: 3px solid var(--nav-active-border); } .nav-item.active .nav-icon { - stroke: #3498db; + stroke: var(--nav-active-color); } .nav-item.active .nav-label { - color: #3498db; + color: var(--nav-active-color); font-weight: 600; } @@ -134,13 +135,18 @@ overflow: hidden; position: relative; background: - linear-gradient(135deg, #3498db, #2980b9) top / 100% 220px no-repeat, - #f0f0f0; + linear-gradient(180deg, var(--bg-gradient-from), var(--bg-gradient-to)) + top / 100% 220px no-repeat, + var(--bg-base); } .accent-bar { - height: 0; - background: linear-gradient(135deg, #3498db, #2980b9); + height: var(--accent-bar-height); + background: linear-gradient( + 90deg, + var(--accent-bar-from), + var(--accent-bar-to) + ); flex-shrink: 0; } @@ -170,13 +176,13 @@ .sidebar::-webkit-scrollbar-thumb, .main-content::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.2); + background: var(--scrollbar-thumb); border-radius: 3px; } .sidebar::-webkit-scrollbar-thumb:hover, .main-content::-webkit-scrollbar-thumb:hover { - background: rgba(255, 255, 255, 0.4); + background: var(--scrollbar-thumb-hover); } /* Footer */ @@ -185,10 +191,10 @@ align-items: center; justify-content: center; padding: 4px 20px; - background-color: #ffffff; - border-top: 1px solid #e0e0e0; + background-color: var(--bg-surface); + border-top: 1px solid var(--border-default); font-size: 12px; - color: #999; + color: var(--text-faint); flex-shrink: 0; } @@ -196,7 +202,7 @@ .nav-divider { width: calc(100% - 1.5rem); border: none; - border-top: 1px solid #d4dae3; + border-top: 1px solid var(--border-default); margin: 0.5rem auto; padding: 0; box-sizing: border-box; @@ -215,7 +221,6 @@ .layout-wrapper { flex-direction: column; - /* Account for fixed bottom nav */ padding-bottom: 56px; } @@ -227,8 +232,8 @@ width: 100%; height: 56px; border-right: none; - border-top: 1px solid #e0e0e0; - box-shadow: 0 -2px 4px rgba(0, 0, 0, 0.08); + border-top: 1px solid var(--border-default); + box-shadow: var(--header-shadow); padding: 0; z-index: 100; } @@ -248,7 +253,7 @@ .nav-item.active { border-left: none; - border-bottom: 3px solid #3498db; + border-bottom: 3px solid var(--nav-active-border); } .nav-icon { @@ -267,12 +272,12 @@ .content-area { background: - linear-gradient(135deg, #3498db, #2980b9) top / 100% 190px no-repeat, - #f0f0f0; + linear-gradient(180deg, var(--bg-gradient-from), var(--bg-gradient-to)) + top / 100% 190px no-repeat, + var(--bg-base); } .footer { - /* Hide footer on mobile since bottom nav takes that space */ display: none; } @@ -280,7 +285,7 @@ width: 1px; height: calc(100% - 1rem); border-top: none; - border-left: 1px solid #d4dae3; + border-left: 1px solid var(--border-default); margin: auto 0; align-self: center; } @@ -304,8 +309,9 @@ .content-area { background: - linear-gradient(135deg, #3498db, #2980b9) top / 100% 160px no-repeat, - #f0f0f0; + linear-gradient(180deg, var(--bg-gradient-from), var(--bg-gradient-to)) + top / 100% 160px no-repeat, + var(--bg-base); } .nav-item {