mirror of
https://github.com/fosrl/pangolin.git
synced 2025-08-09 20:35:28 +02:00
lighten dark background, add more info to resources table
This commit is contained in:
parent
658a6ca7bb
commit
ce2bfcddd5
11 changed files with 191 additions and 47 deletions
|
@ -14,6 +14,7 @@ traefik:
|
||||||
cert_resolver: letsencrypt
|
cert_resolver: letsencrypt
|
||||||
http_entrypoint: web
|
http_entrypoint: web
|
||||||
https_entrypoint: websecure
|
https_entrypoint: websecure
|
||||||
|
prefer_wildcard_cert: true
|
||||||
|
|
||||||
badger:
|
badger:
|
||||||
session_query_parameter: __pang_sess
|
session_query_parameter: __pang_sess
|
||||||
|
|
|
@ -165,7 +165,7 @@ function notAllowed(res: Response, redirectUrl?: string) {
|
||||||
error: false,
|
error: false,
|
||||||
message: "Access denied",
|
message: "Access denied",
|
||||||
status: HttpCode.OK,
|
status: HttpCode.OK,
|
||||||
}
|
};
|
||||||
logger.debug(JSON.stringify(data));
|
logger.debug(JSON.stringify(data));
|
||||||
return response<VerifyUserResponse>(res, data);
|
return response<VerifyUserResponse>(res, data);
|
||||||
}
|
}
|
||||||
|
@ -177,7 +177,7 @@ function allowed(res: Response) {
|
||||||
error: false,
|
error: false,
|
||||||
message: "Access allowed",
|
message: "Access allowed",
|
||||||
status: HttpCode.OK,
|
status: HttpCode.OK,
|
||||||
}
|
};
|
||||||
logger.debug(JSON.stringify(data));
|
logger.debug(JSON.stringify(data));
|
||||||
return response<VerifyUserResponse>(res, data);
|
return response<VerifyUserResponse>(res, data);
|
||||||
}
|
}
|
||||||
|
|
|
@ -94,6 +94,7 @@ export async function createResource(
|
||||||
orgId,
|
orgId,
|
||||||
name,
|
name,
|
||||||
subdomain,
|
subdomain,
|
||||||
|
ssl: true,
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,8 @@ import {
|
||||||
sites,
|
sites,
|
||||||
userResources,
|
userResources,
|
||||||
roleResources,
|
roleResources,
|
||||||
|
resourcePassword,
|
||||||
|
resourcePincode,
|
||||||
} from "@server/db/schema";
|
} from "@server/db/schema";
|
||||||
import response from "@server/utils/response";
|
import response from "@server/utils/response";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
@ -46,39 +48,63 @@ const listResourcesSchema = z.object({
|
||||||
function queryResources(
|
function queryResources(
|
||||||
accessibleResourceIds: number[],
|
accessibleResourceIds: number[],
|
||||||
siteId?: number,
|
siteId?: number,
|
||||||
orgId?: string
|
orgId?: string,
|
||||||
) {
|
) {
|
||||||
if (siteId) {
|
if (siteId) {
|
||||||
return db
|
return db
|
||||||
.select({
|
.select({
|
||||||
resourceId: resources.resourceId,
|
resourceId: resources.resourceId,
|
||||||
name: resources.name,
|
name: resources.name,
|
||||||
subdomain: resources.subdomain,
|
fullDomain: resources.fullDomain,
|
||||||
|
ssl: resources.ssl,
|
||||||
siteName: sites.name,
|
siteName: sites.name,
|
||||||
|
passwordId: resourcePassword.passwordId,
|
||||||
|
pincodeId: resourcePincode.pincodeId,
|
||||||
|
sso: resources.sso,
|
||||||
})
|
})
|
||||||
.from(resources)
|
.from(resources)
|
||||||
.leftJoin(sites, eq(resources.siteId, sites.siteId))
|
.leftJoin(sites, eq(resources.siteId, sites.siteId))
|
||||||
|
.leftJoin(
|
||||||
|
resourcePassword,
|
||||||
|
eq(resourcePassword.resourceId, resources.resourceId),
|
||||||
|
)
|
||||||
|
.leftJoin(
|
||||||
|
resourcePincode,
|
||||||
|
eq(resourcePincode.resourceId, resources.resourceId),
|
||||||
|
)
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
inArray(resources.resourceId, accessibleResourceIds),
|
inArray(resources.resourceId, accessibleResourceIds),
|
||||||
eq(resources.siteId, siteId)
|
eq(resources.siteId, siteId),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
} else if (orgId) {
|
} else if (orgId) {
|
||||||
return db
|
return db
|
||||||
.select({
|
.select({
|
||||||
resourceId: resources.resourceId,
|
resourceId: resources.resourceId,
|
||||||
name: resources.name,
|
name: resources.name,
|
||||||
subdomain: resources.subdomain,
|
ssl: resources.ssl,
|
||||||
|
fullDomain: resources.fullDomain,
|
||||||
siteName: sites.name,
|
siteName: sites.name,
|
||||||
|
passwordId: resourcePassword.passwordId,
|
||||||
|
sso: resources.sso,
|
||||||
|
pincodeId: resourcePincode.pincodeId,
|
||||||
})
|
})
|
||||||
.from(resources)
|
.from(resources)
|
||||||
.leftJoin(sites, eq(resources.siteId, sites.siteId))
|
.leftJoin(sites, eq(resources.siteId, sites.siteId))
|
||||||
|
.leftJoin(
|
||||||
|
resourcePassword,
|
||||||
|
eq(resourcePassword.resourceId, resources.resourceId),
|
||||||
|
)
|
||||||
|
.leftJoin(
|
||||||
|
resourcePincode,
|
||||||
|
eq(resourcePincode.resourceId, resources.resourceId),
|
||||||
|
)
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
inArray(resources.resourceId, accessibleResourceIds),
|
inArray(resources.resourceId, accessibleResourceIds),
|
||||||
eq(resources.orgId, orgId)
|
eq(resources.orgId, orgId),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -91,7 +117,7 @@ export type ListResourcesResponse = {
|
||||||
export async function listResources(
|
export async function listResources(
|
||||||
req: Request,
|
req: Request,
|
||||||
res: Response,
|
res: Response,
|
||||||
next: NextFunction
|
next: NextFunction,
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
try {
|
try {
|
||||||
const parsedQuery = listResourcesSchema.safeParse(req.query);
|
const parsedQuery = listResourcesSchema.safeParse(req.query);
|
||||||
|
@ -99,8 +125,8 @@ export async function listResources(
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.BAD_REQUEST,
|
HttpCode.BAD_REQUEST,
|
||||||
parsedQuery.error.errors.map((e) => e.message).join(", ")
|
parsedQuery.error.errors.map((e) => e.message).join(", "),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const { limit, offset } = parsedQuery.data;
|
const { limit, offset } = parsedQuery.data;
|
||||||
|
@ -110,8 +136,8 @@ export async function listResources(
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.BAD_REQUEST,
|
HttpCode.BAD_REQUEST,
|
||||||
parsedParams.error.errors.map((e) => e.message).join(", ")
|
parsedParams.error.errors.map((e) => e.message).join(", "),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const { siteId, orgId } = parsedParams.data;
|
const { siteId, orgId } = parsedParams.data;
|
||||||
|
@ -120,8 +146,8 @@ export async function listResources(
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.FORBIDDEN,
|
HttpCode.FORBIDDEN,
|
||||||
"User does not have access to this organization"
|
"User does not have access to this organization",
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -132,17 +158,17 @@ export async function listResources(
|
||||||
.from(userResources)
|
.from(userResources)
|
||||||
.fullJoin(
|
.fullJoin(
|
||||||
roleResources,
|
roleResources,
|
||||||
eq(userResources.resourceId, roleResources.resourceId)
|
eq(userResources.resourceId, roleResources.resourceId),
|
||||||
)
|
)
|
||||||
.where(
|
.where(
|
||||||
or(
|
or(
|
||||||
eq(userResources.userId, req.user!.userId),
|
eq(userResources.userId, req.user!.userId),
|
||||||
eq(roleResources.roleId, req.userOrgRoleId!)
|
eq(roleResources.roleId, req.userOrgRoleId!),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const accessibleResourceIds = accessibleResources.map(
|
const accessibleResourceIds = accessibleResources.map(
|
||||||
(resource) => resource.resourceId
|
(resource) => resource.resourceId,
|
||||||
);
|
);
|
||||||
|
|
||||||
let countQuery: any = db
|
let countQuery: any = db
|
||||||
|
@ -173,7 +199,10 @@ export async function listResources(
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(error);
|
logger.error(error);
|
||||||
return next(
|
return next(
|
||||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
"An error occurred",
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -117,7 +117,7 @@ export async function traefikConfigProvider(
|
||||||
? config.traefik.https_entrypoint
|
? config.traefik.https_entrypoint
|
||||||
: config.traefik.http_entrypoint,
|
: config.traefik.http_entrypoint,
|
||||||
],
|
],
|
||||||
middlewares: resource.ssl ? [badgerMiddlewareName] : [],
|
middlewares: [badgerMiddlewareName],
|
||||||
service: serviceName,
|
service: serviceName,
|
||||||
rule: `Host(\`${fullDomain}\`)`,
|
rule: `Host(\`${fullDomain}\`)`,
|
||||||
...(resource.ssl ? { tls } : {}),
|
...(resource.ssl ? { tls } : {}),
|
||||||
|
|
|
@ -91,7 +91,7 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="w-full border-b bg-neutral-100 dark:bg-neutral-900 mb-6 select-none sm:px-0 px-3 pt-3">
|
<div className="w-full border-b bg-neutral-100 dark:bg-neutral-800 mb-6 select-none sm:px-0 px-3 pt-3">
|
||||||
<div className="container mx-auto flex flex-col content-between gap-4 ">
|
<div className="container mx-auto flex flex-col content-between gap-4 ">
|
||||||
<Header
|
<Header
|
||||||
email={user.email}
|
email={user.email}
|
||||||
|
|
|
@ -9,7 +9,16 @@ import {
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@app/components/ui/dropdown-menu";
|
} from "@app/components/ui/dropdown-menu";
|
||||||
import { Button } from "@app/components/ui/button";
|
import { Button } from "@app/components/ui/button";
|
||||||
import { ArrowRight, ArrowUpDown, MoreHorizontal } from "lucide-react";
|
import {
|
||||||
|
Copy,
|
||||||
|
ArrowRight,
|
||||||
|
ArrowUpDown,
|
||||||
|
MoreHorizontal,
|
||||||
|
Check,
|
||||||
|
ArrowUpRight,
|
||||||
|
ShieldOff,
|
||||||
|
ShieldCheck,
|
||||||
|
} from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import api from "@app/api";
|
import api from "@app/api";
|
||||||
|
@ -26,6 +35,7 @@ export type ResourceRow = {
|
||||||
orgId: string;
|
orgId: string;
|
||||||
domain: string;
|
domain: string;
|
||||||
site: string;
|
site: string;
|
||||||
|
hasAuth: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ResourcesTableProps = {
|
type ResourcesTableProps = {
|
||||||
|
@ -91,10 +101,99 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const resourceRow = row.original;
|
||||||
|
return (
|
||||||
|
<Button variant="outline">
|
||||||
|
<Link
|
||||||
|
href={`/${resourceRow.orgId}/settings/sites/${resourceRow.site}`}
|
||||||
|
>
|
||||||
|
{resourceRow.site}
|
||||||
|
</Link>
|
||||||
|
<ArrowUpRight className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "domain",
|
accessorKey: "domain",
|
||||||
header: "Domain",
|
header: "Domain",
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const resourceRow = row.original;
|
||||||
|
return (
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Link
|
||||||
|
href={`https://${resourceRow.domain}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="hover:underline mr-2"
|
||||||
|
>
|
||||||
|
{resourceRow.domain}
|
||||||
|
</Link>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="h-6 w-6 p-0"
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(
|
||||||
|
resourceRow.domain,
|
||||||
|
);
|
||||||
|
const originalIcon = document.querySelector(
|
||||||
|
`#icon-${resourceRow.id}`,
|
||||||
|
);
|
||||||
|
if (originalIcon) {
|
||||||
|
originalIcon.classList.add("hidden");
|
||||||
|
}
|
||||||
|
const checkIcon = document.querySelector(
|
||||||
|
`#check-icon-${resourceRow.id}`,
|
||||||
|
);
|
||||||
|
if (checkIcon) {
|
||||||
|
checkIcon.classList.remove("hidden");
|
||||||
|
setTimeout(() => {
|
||||||
|
checkIcon.classList.add("hidden");
|
||||||
|
if (originalIcon) {
|
||||||
|
originalIcon.classList.remove(
|
||||||
|
"hidden",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Copy
|
||||||
|
id={`icon-${resourceRow.id}`}
|
||||||
|
className="h-4 w-4"
|
||||||
|
/>
|
||||||
|
<Check
|
||||||
|
id={`check-icon-${resourceRow.id}`}
|
||||||
|
className="hidden text-green-500 h-4 w-4"
|
||||||
|
/>
|
||||||
|
<span className="sr-only">Copy domain</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "hasAuth",
|
||||||
|
header: "Authentication",
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const resourceRow = row.original;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{resourceRow.hasAuth ? (
|
||||||
|
<span className="text-green-500 flex items-center space-x-2">
|
||||||
|
<ShieldCheck className="w-4 h-4" />
|
||||||
|
<span>Protected</span>
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-yellow-500 flex items-center space-x-2">
|
||||||
|
<ShieldOff className="w-4 h-4" />
|
||||||
|
<span>Not Protected</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "actions",
|
id: "actions",
|
||||||
|
@ -130,11 +229,11 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedResource(
|
setSelectedResource(
|
||||||
resourceRow
|
resourceRow,
|
||||||
);
|
);
|
||||||
setIsDeleteModalOpen(true);
|
setIsDeleteModalOpen(true);
|
||||||
}}
|
}}
|
||||||
className="text-red-600 hover:text-red-800 hover:underline cursor-pointer"
|
className="text-red-500"
|
||||||
>
|
>
|
||||||
Delete
|
Delete
|
||||||
</button>
|
</button>
|
||||||
|
@ -146,7 +245,7 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
|
||||||
className="ml-2"
|
className="ml-2"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
router.push(
|
router.push(
|
||||||
`/${resourceRow.orgId}/settings/resources/${resourceRow.id}`
|
`/${resourceRow.orgId}/settings/resources/${resourceRow.id}`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
|
@ -19,7 +19,7 @@ export default async function ResourcesPage(props: ResourcesPageProps) {
|
||||||
try {
|
try {
|
||||||
const res = await internal.get<AxiosResponse<ListResourcesResponse>>(
|
const res = await internal.get<AxiosResponse<ListResourcesResponse>>(
|
||||||
`/org/${params.orgId}/resources`,
|
`/org/${params.orgId}/resources`,
|
||||||
await authCookieHeader()
|
await authCookieHeader(),
|
||||||
);
|
);
|
||||||
resources = res.data.data.resources;
|
resources = res.data.data.resources;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -31,8 +31,8 @@ export default async function ResourcesPage(props: ResourcesPageProps) {
|
||||||
const getOrg = cache(async () =>
|
const getOrg = cache(async () =>
|
||||||
internal.get<AxiosResponse<GetOrgResponse>>(
|
internal.get<AxiosResponse<GetOrgResponse>>(
|
||||||
`/org/${params.orgId}`,
|
`/org/${params.orgId}`,
|
||||||
await authCookieHeader()
|
await authCookieHeader(),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
const res = await getOrg();
|
const res = await getOrg();
|
||||||
org = res.data.data;
|
org = res.data.data;
|
||||||
|
@ -49,8 +49,12 @@ export default async function ResourcesPage(props: ResourcesPageProps) {
|
||||||
id: resource.resourceId,
|
id: resource.resourceId,
|
||||||
name: resource.name,
|
name: resource.name,
|
||||||
orgId: params.orgId,
|
orgId: params.orgId,
|
||||||
domain: resource.subdomain || "",
|
domain: `${resource.ssl ? "https://" : "http://"}${resource.fullDomain}`,
|
||||||
site: resource.siteName || "None",
|
site: resource.siteName || "None",
|
||||||
|
hasAuth:
|
||||||
|
resource.sso ||
|
||||||
|
resource.pincodeId !== null ||
|
||||||
|
resource.pincodeId !== null,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -24,8 +24,8 @@ export type SiteRow = {
|
||||||
id: number;
|
id: number;
|
||||||
nice: string;
|
nice: string;
|
||||||
name: string;
|
name: string;
|
||||||
mbIn: number;
|
mbIn: string;
|
||||||
mbOut: number;
|
mbOut: string;
|
||||||
orgId: string;
|
orgId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -15,20 +15,30 @@ export default async function SitesPage(props: SitesPageProps) {
|
||||||
try {
|
try {
|
||||||
const res = await internal.get<AxiosResponse<ListSitesResponse>>(
|
const res = await internal.get<AxiosResponse<ListSitesResponse>>(
|
||||||
`/org/${params.orgId}/sites`,
|
`/org/${params.orgId}/sites`,
|
||||||
await authCookieHeader()
|
await authCookieHeader(),
|
||||||
);
|
);
|
||||||
sites = res.data.data.sites;
|
sites = res.data.data.sites;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error fetching sites", e);
|
console.error("Error fetching sites", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatSize(mb: number): string {
|
||||||
|
if (mb >= 1024 * 1024) {
|
||||||
|
return `${(mb / (1024 * 1024)).toFixed(2)} TB`;
|
||||||
|
} else if (mb >= 1024) {
|
||||||
|
return `${(mb / 1024).toFixed(2)} GB`;
|
||||||
|
} else {
|
||||||
|
return `${mb.toFixed(2)} MB`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const siteRows: SiteRow[] = sites.map((site) => {
|
const siteRows: SiteRow[] = sites.map((site) => {
|
||||||
return {
|
return {
|
||||||
name: site.name,
|
name: site.name,
|
||||||
id: site.siteId,
|
id: site.siteId,
|
||||||
nice: site.niceId.toString(),
|
nice: site.niceId.toString(),
|
||||||
mbIn: site.megabytesIn || 0,
|
mbIn: formatSize(site.megabytesIn || 0),
|
||||||
mbOut: site.megabytesOut || 0,
|
mbOut: formatSize(site.megabytesOut || 0),
|
||||||
orgId: params.orgId,
|
orgId: params.orgId,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,11 +6,11 @@
|
||||||
@layer base {
|
@layer base {
|
||||||
:root {
|
:root {
|
||||||
--background: 0 0% 100%;
|
--background: 0 0% 100%;
|
||||||
--foreground: 20 14.3% 4.1%;
|
--foreground: 20 5.0% 10.0%;
|
||||||
--card: 0 0% 100%;
|
--card: 0 0% 100%;
|
||||||
--card-foreground: 20 14.3% 4.1%;
|
--card-foreground: 20 5.0% 10.0%;
|
||||||
--popover: 0 0% 100%;
|
--popover: 0 0% 100%;
|
||||||
--popover-foreground: 20 14.3% 4.1%;
|
--popover-foreground: 20 5.0% 10.0%;
|
||||||
--primary: 24.6 95% 53.1%;
|
--primary: 24.6 95% 53.1%;
|
||||||
--primary-foreground: 60 9.1% 97.8%;
|
--primary-foreground: 60 9.1% 97.8%;
|
||||||
--secondary: 60 4.8% 95.9%;
|
--secondary: 60 4.8% 95.9%;
|
||||||
|
@ -33,24 +33,24 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
--background: 20 14.3% 4.1%;
|
--background: 20 5.0% 10.0%;
|
||||||
--foreground: 60 9.1% 97.8%;
|
--foreground: 60 9.1% 97.8%;
|
||||||
--card: 20 14.3% 4.1%;
|
--card: 20 5.0% 10.0%;
|
||||||
--card-foreground: 60 9.1% 97.8%;
|
--card-foreground: 60 9.1% 97.8%;
|
||||||
--popover: 20 14.3% 4.1%;
|
--popover: 20 5.0% 10.0%;
|
||||||
--popover-foreground: 60 9.1% 97.8%;
|
--popover-foreground: 60 9.1% 97.8%;
|
||||||
--primary: 20.5 90.2% 48.2%;
|
--primary: 20.5 90.2% 48.2%;
|
||||||
--primary-foreground: 60 9.1% 97.8%;
|
--primary-foreground: 60 9.1% 97.8%;
|
||||||
--secondary: 12 6.5% 15.1%;
|
--secondary: 12 6.5% 25.0%;
|
||||||
--secondary-foreground: 60 9.1% 97.8%;
|
--secondary-foreground: 60 9.1% 97.8%;
|
||||||
--muted: 12 6.5% 15.1%;
|
--muted: 12 6.5% 25.0%;
|
||||||
--muted-foreground: 24 5.4% 63.9%;
|
--muted-foreground: 24 5.4% 63.9%;
|
||||||
--accent: 12 6.5% 15.1%;
|
--accent: 12 6.5% 25.0%;
|
||||||
--accent-foreground: 60 9.1% 97.8%;
|
--accent-foreground: 60 9.1% 97.8%;
|
||||||
--destructive: 0 72.2% 50.6%;
|
--destructive: 0 72.2% 50.6%;
|
||||||
--destructive-foreground: 60 9.1% 97.8%;
|
--destructive-foreground: 60 9.1% 97.8%;
|
||||||
--border: 12 6.5% 15.1%;
|
--border: 12 6.5% 25.0%;
|
||||||
--input: 12 6.5% 15.1%;
|
--input: 12 6.5% 25.0%;
|
||||||
--ring: 20.5 90.2% 48.2%;
|
--ring: 20.5 90.2% 48.2%;
|
||||||
--chart-1: 220 70% 50%;
|
--chart-1: 220 70% 50%;
|
||||||
--chart-2: 160 60% 45%;
|
--chart-2: 160 60% 45%;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue