feat: plex sans font and tidy up
All checks were successful
Deploy eolas-app / deploy (push) Successful in 52s

This commit is contained in:
Thomas Bishop 2025-12-06 15:52:43 +00:00
parent 4c6a4d619f
commit 7143d0f389
8 changed files with 405 additions and 383 deletions

View file

@ -1,4 +1,5 @@
@import url("https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&display=swap"); @import url("https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&display=swap");
@import url("https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:ital,wght@0,100..700;1,100..700&display=swap");
code { code {
font-family: "JetBrains Mono"; font-family: "JetBrains Mono";

View file

@ -10,68 +10,68 @@ import { Link } from "react-router"
import { useState, useRef, useEffect } from "react" import { useState, useRef, useEffect } from "react"
export default function EntriesListSidebar() { export default function EntriesListSidebar() {
const scrollRef = useRef(null) const scrollRef = useRef(null)
const [isOpen, setIsOpen] = useState(() => { const [isOpen, setIsOpen] = useState(() => {
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
const saved = sessionStorage.getItem("entries_list_sidebar_open") const saved = sessionStorage.getItem("entries_list_sidebar_open")
return saved ? JSON.parse(saved) : false return saved ? JSON.parse(saved) : false
} }
return false return false
}) })
const { data: entries, isLoading } = useQuery({ const { data: entries, isLoading } = useQuery({
queryKey: ["entries_list"], queryKey: ["entries_list"],
queryFn: () => api.get("/entries").then((res) => res.data), queryFn: () => api.get("/entries").then((res) => res.data),
}) })
useEffect(() => { useEffect(() => {
sessionStorage.setItem("entries_list_sidebar_open", JSON.stringify(isOpen)) sessionStorage.setItem("entries_list_sidebar_open", JSON.stringify(isOpen))
}, [isOpen]) }, [isOpen])
useEffect(() => { useEffect(() => {
const savedScroll = sessionStorage.getItem("entries_list_sidebar_scroll_position") const savedScroll = sessionStorage.getItem("entries_list_sidebar_scroll_position")
if (savedScroll && scrollRef.current) { if (savedScroll && scrollRef.current) {
scrollRef.current.scrollTop = parseInt(savedScroll) scrollRef.current.scrollTop = parseInt(savedScroll)
} }
}, [entries]) }, [entries])
const handleScroll = () => { const handleScroll = () => {
if (scrollRef.current) { if (scrollRef.current) {
sessionStorage.setItem("entries_list_sidebar_scroll_position", scrollRef.current.scrollTop) sessionStorage.setItem("entries_list_sidebar_scroll_position", scrollRef.current.scrollTop)
} }
} }
return ( return (
<Collapsible open={isOpen} onOpenChange={setIsOpen} className="group/collapsible"> <Collapsible open={isOpen} onOpenChange={setIsOpen} className="group/collapsible">
<SidebarMenuItem key="entries"> <SidebarMenuItem key="entries">
<CollapsibleTrigger asChild className="rounded-none"> <CollapsibleTrigger asChild className="rounded-none">
<SidebarMenuButton asChild className="rounded-none"> <SidebarMenuButton asChild className="rounded-none">
<a href="#"> <a href="#">
<FileText /> <FileText />
<span>Entries</span> <span className="font-medium">Entries</span>
<Badge className="ml-0" variant="secondary"> <Badge className="ml-0" variant="secondary">
{entries?.count} {entries?.count}
</Badge> </Badge>
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" /> <ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
</a> </a>
</SidebarMenuButton> </SidebarMenuButton>
</CollapsibleTrigger> </CollapsibleTrigger>
<CollapsibleContent> <CollapsibleContent>
<div ref={scrollRef} onScroll={handleScroll} className="max-h-100 overflow-y-auto"> <div ref={scrollRef} onScroll={handleScroll} className="max-h-100 overflow-y-auto">
<SidebarMenuSub> <SidebarMenuSub>
{entries?.data.map((item, i) => ( {entries?.data.map((item, i) => (
<SidebarMenuItem key={i}> <SidebarMenuItem key={i}>
<Link to={`/entries/${item.title}`}> <Link to={`/entries/${item.title}`}>
<span className="text-xs text-foreground dark:hover:text-green-300 dark:active:text-green-400 hover:text-gray-600 active:text-gray-700 focus:text-gray-900 dark:focus:text-green-900"> <span className="text-xs text-foreground dark:hover:text-green-300 dark:active:text-green-400 hover:text-gray-600 active:text-gray-700 focus:text-gray-900 dark:focus:text-green-900">
{item.title.replace(/_/g, " ")} {item.title.replace(/_/g, " ")}
</span> </span>
</Link> </Link>
</SidebarMenuItem> </SidebarMenuItem>
))} ))}
</SidebarMenuSub> </SidebarMenuSub>
</div> </div>
</CollapsibleContent> </CollapsibleContent>
</SidebarMenuItem> </SidebarMenuItem>
</Collapsible> </Collapsible>
) )
} }

View file

@ -1,87 +1,70 @@
import { useQuery } from "@tanstack/react-query"
import api from "../api/eolas-api"
import { Link } from "react-router" import { Link } from "react-router"
import { Badge } from "./ui/badge" import { Badge } from "./ui/badge"
export default function EntryReferences({ entryTitle }) { export default function EntryReferences({ tags, backlinks, outlinks }) {
const { data: tags, isLoading: tagsLoading } = useQuery({ return (
queryKey: [`tags_for_${entryTitle}`], <div className="w-full md:flex flex-row justify-stretch gap-3">
queryFn: () => api.get(`/tags/${entryTitle}`).then((res) => res.data), <div className="w-full">
}) <div className="flex flex row justify-between bg-sidebar">
<h3 className="font-medium text-sm p-1 ml-1">Incoming links</h3>
<Badge variant="secondary" className="rounded-none">
{backlinks?.count}
</Badge>
</div>
const { data: backlinks, isLoading: backlinksLoading } = useQuery({ <div className="mt-2 mb-3 pl-1">
queryKey: [`backlinks_for_${entryTitle}`], {backlinks &&
queryFn: () => api.get(`/entries/backlinks/${entryTitle}`).then((res) => res.data), backlinks?.data.map((item, i) => (
}) <Link
key={i}
to={`/entries/${item}`}
className="text-foreground underline-offset-3 text-sm underline hover:text-gray-700 dark:hover:text-green-300 pr-2 block mb-2"
>
{item.replace(/_/g, " ")}
</Link>
))}
</div>
const { data: outlinks, isLoading: outlinksLoading } = useQuery({ <div className="flex flex row justify-between bg-sidebar">
queryKey: [`outlinks_for_${entryTitle}`], <h3 className="font-medium text-sm p-1 ml-1">Outgoing links</h3>
queryFn: () => api.get(`/entries/outlinks/${entryTitle}`).then((res) => res.data), <Badge variant="secondary" className="rounded-none">
}) {outlinks?.count}
</Badge>
</div>
return ( <div className="mt-2 mb-3 pl-1">
<div className="w-full md:flex flex-row justify-stretch gap-3"> {outlinks &&
<div className="w-full"> outlinks?.data.map((item, i) => (
<div className="flex flex row justify-between bg-sidebar"> <Link
<h3 className="font-medium text-sm p-1 ml-1">Incoming links</h3> key={i}
<Badge variant="secondary" className="rounded-none"> to={`/entries/${item}`}
{backlinks?.count} className="text-foreground underline-offset-3 text-sm underline hover:text-gray-700 dark:hover:text-green-300 pr-2 block mb-2"
</Badge> >
</div> {item.replace(/_/g, " ")}
</Link>
))}
</div>
</div>
<div className="mt-2 mb-3 pl-1"> <div className="w-full">
{backlinks && <div className="flex flex row justify-between bg-sidebar">
backlinks?.data.map((item, i) => ( <h3 className="font-medium text-sm p-1 ml-1">Tags</h3>
<Link <Badge variant="secondary" className="rounded-none">
key={i} {tags?.count}
to={`/entries/${item}`} </Badge>
className="text-foreground underline-offset-3 text-sm underline hover:text-gray-700 dark:hover:text-green-300 pr-2 block mb-2" </div>
> <div className="mt-2 mb-3 pl-1">
{item.replace(/_/g, " ")} {tags?.data.map((item, i) => (
</Link> <Link
))} key={i}
</div> to={`/tags/${item}`}
className="text-foreground underline-offset-3 text-sm underline hover:text-gray-700 dark:hover:text-green-300 pr-2 block mb-2"
<div className="flex flex row justify-between bg-sidebar"> >
<h3 className="font-medium text-sm p-1 ml-1">Outgoing links</h3> {item}
<Badge variant="secondary" className="rounded-none"> </Link>
{outlinks?.count} ))}
</Badge> </div>
</div> </div>
</div>
<div className="mt-2 mb-3 pl-1"> )
{outlinks &&
outlinks?.data.map((item, i) => (
<Link
key={i}
to={`/entries/${item}`}
className="text-foreground underline-offset-3 text-sm underline hover:text-gray-700 dark:hover:text-green-300 pr-2 block mb-2"
>
{item.replace(/_/g, " ")}
</Link>
))}
</div>
</div>
<div className="w-full">
<div className="flex flex row justify-between bg-sidebar">
<h3 className="font-medium text-sm p-1 ml-1">Tags</h3>
<Badge variant="secondary" className="rounded-none">
{tags?.count}
</Badge>
</div>
<div className="mt-2 mb-3 pl-1">
{tags?.data.map((item, i) => (
<Link
key={i}
to={`/tags/${item}`}
className="text-foreground underline-offset-3 text-sm underline hover:text-gray-700 dark:hover:text-green-300 pr-2 block mb-2"
>
{item}
</Link>
))}
</div>
</div>
</div>
)
} }

View file

@ -3,38 +3,38 @@ import LoadGraph from "./LoadGraph"
import GraphEvents from "./GraphEvents" import GraphEvents from "./GraphEvents"
export default function NetworkGraph({ data }) { export default function NetworkGraph({ data }) {
const nodeCount = data?.nodes.length const nodeCount = data?.nodes.length
const sigmaStyle = { const sigmaStyle = {
height: "400px", height: "400px",
width: "100%", width: "100%",
} }
const settings = { const settings = {
allowInvalidContainer: true, allowInvalidContainer: true,
defaultEdgeColor: "#a4a4a4", defaultEdgeColor: "#a4a4a4",
labelColor: { color: "#0a0a0a" }, labelColor: { color: "#0a0a0a" },
labelFont: "Inter", labelFont: "IBM Plex Sans",
labelSize: 14, labelSize: 14,
labelWeight: "400", labelWeight: "400",
labelRenderedSizeThreshold: nodeCount > 15 ? 10 : 8, labelRenderedSizeThreshold: nodeCount > 15 ? 10 : 8,
renderLabels: true, renderLabels: true,
} }
return ( return (
<div <div
style={{ style={{
width: "100%", width: "100%",
height: "400px", height: "400px",
backgroundColor: "#fff", backgroundColor: "#fff",
overflow: "hidden", overflow: "hidden",
contain: "strict", contain: "strict",
}} }}
> >
<SigmaContainer style={sigmaStyle} settings={settings}> <SigmaContainer style={sigmaStyle} settings={settings}>
<LoadGraph data={data} /> <LoadGraph data={data} />
<GraphEvents /> <GraphEvents />
</SigmaContainer> </SigmaContainer>
</div> </div>
) )
} }

View file

@ -9,69 +9,69 @@ import { Link } from "react-router"
import { useState, useRef, useEffect } from "react" import { useState, useRef, useEffect } from "react"
export default function TagListSidebar() { export default function TagListSidebar() {
const scrollRef = useRef(null) const scrollRef = useRef(null)
const [isOpen, setIsOpen] = useState(() => { const [isOpen, setIsOpen] = useState(() => {
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
const saved = sessionStorage.getItem("tags_list_sidebar_open") const saved = sessionStorage.getItem("tags_list_sidebar_open")
return saved ? JSON.parse(saved) : false return saved ? JSON.parse(saved) : false
} }
return false return false
}) })
const { data: tags } = useQuery({ const { data: tags } = useQuery({
queryKey: ["tag_list"], queryKey: ["tag_list"],
queryFn: () => api.get("/tags").then((res) => res.data), queryFn: () => api.get("/tags").then((res) => res.data),
}) })
useEffect(() => { useEffect(() => {
sessionStorage.setItem("tags_list_sidebar_open", JSON.stringify(isOpen)) sessionStorage.setItem("tags_list_sidebar_open", JSON.stringify(isOpen))
}, [isOpen]) }, [isOpen])
useEffect(() => { useEffect(() => {
const savedScroll = sessionStorage.getItem("tags_list_sidebar_open") const savedScroll = sessionStorage.getItem("tags_list_sidebar_open")
if (savedScroll && scrollRef.current) { if (savedScroll && scrollRef.current) {
scrollRef.current.scrollTop = parseInt(savedScroll) scrollRef.current.scrollTop = parseInt(savedScroll)
} }
}, [tags]) }, [tags])
const handleScroll = () => { const handleScroll = () => {
if (scrollRef.current) { if (scrollRef.current) {
sessionStorage.setItem("tags_list_sidebar_open", scrollRef.current.scrollTop) sessionStorage.setItem("tags_list_sidebar_open", scrollRef.current.scrollTop)
} }
} }
return ( return (
<Collapsible open={isOpen} onOpenChange={setIsOpen} className="group/collapsible"> <Collapsible open={isOpen} onOpenChange={setIsOpen} className="group/collapsible">
<SidebarMenuItem key="tags"> <SidebarMenuItem key="tags">
<CollapsibleTrigger asChild> <CollapsibleTrigger asChild>
<SidebarMenuButton asChild className="rounded-none"> <SidebarMenuButton asChild className="rounded-none">
<a href="#"> <a href="#">
<Tags /> <Tags />
<span>Tags</span> <span className="font-medium">Tags</span>
<Badge className="ml-0" variant="secondary"> <Badge className="ml-0" variant="secondary">
{tags?.count} {tags?.count}
</Badge> </Badge>
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" /> <ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
</a> </a>
</SidebarMenuButton> </SidebarMenuButton>
</CollapsibleTrigger> </CollapsibleTrigger>
<CollapsibleContent> <CollapsibleContent>
<div ref={scrollRef} onScroll={handleScroll} className="max-h-100 overflow-y-auto"> <div ref={scrollRef} onScroll={handleScroll} className="max-h-100 overflow-y-auto">
<SidebarMenuSub> <SidebarMenuSub>
{tags?.data.map((item, i) => ( {tags?.data.map((item, i) => (
<SidebarMenuItem key={i}> <SidebarMenuItem key={i}>
<Link to={`/tags/${item}`}> <Link to={`/tags/${item}`}>
<span className="text-xs text-foreground dark:hover:text-green-300 dark:active:text-green-400 hover:text-gray-600 active:text-gray-700 focus:text-gray-900 dark:focus:text-green-900"> <span className="text-xs text-foreground dark:hover:text-green-300 dark:active:text-green-400 hover:text-gray-600 active:text-gray-700 focus:text-gray-900 dark:focus:text-green-900">
{item} {item}
</span> </span>
</Link> </Link>
</SidebarMenuItem> </SidebarMenuItem>
))} ))}
</SidebarMenuSub> </SidebarMenuSub>
</div> </div>
</CollapsibleContent> </CollapsibleContent>
</SidebarMenuItem> </SidebarMenuItem>
</Collapsible> </Collapsible>
) )
} }

View file

@ -1,53 +1,68 @@
import { useQuery } from "@tanstack/react-query"
import api from "../api/eolas-api"
import EntryReferences from "@/components/EntryReferences" import EntryReferences from "@/components/EntryReferences"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Bookmark } from "lucide-react" import { Bookmark } from "lucide-react"
import { History } from "lucide-react"
import { Braces } from "lucide-react" import { Braces } from "lucide-react"
import { Badge } from "@/components/ui/badge"
import { convertDateFriendly } from "@/lib/utils"
export default function EntryMetadata({ entryTitle, history, metadata }) { export default function EntryMetadata({ entryTitle, history, metadata }) {
return ( const { data: tags } = useQuery({
<Tabs defaultValue="references" className="w-full h-full flex flex-col"> queryKey: [`tags_for_${entryTitle}`],
<TabsList className="rounded-none shadow-none drop-shadow-none w-full bg-sidebar sticky top-0 z-10 flex-shrink-0"> queryFn: () => api.get(`/tags/${entryTitle}`).then((res) => res.data),
<TabsTrigger })
value="references"
className="pl-2 justify-start text-sm font-semibold rounded-none shadow-none data-[state=active]:border-1 data-[state=active]:border-border py-4"
>
<Bookmark />
References
</TabsTrigger>
<TabsTrigger const { data: backlinks } = useQuery({
className="pl-2 justify-start text-sm font-semibold rounded-none shadow-none data-[state=active]:border-1 data-[state=active]:border-border py-4" queryKey: [`backlinks_for_${entryTitle}`],
value="history" queryFn: () => api.get(`/entries/backlinks/${entryTitle}`).then((res) => res.data),
> })
<History />
History
</TabsTrigger>
<TabsTrigger const { data: outlinks } = useQuery({
className="pl-2 justify-start text-sm font-semibold rounded-none shadow-none data-[state=active]:border-1 data-[state=active]:border-border py-4" queryKey: [`outlinks_for_${entryTitle}`],
value="metadata" queryFn: () => api.get(`/entries/outlinks/${entryTitle}`).then((res) => res.data),
> })
<Braces />
Metadata
</TabsTrigger>
</TabsList>
<div className="flex-1 overflow-auto"> const sizeInKb = parseFloat((metadata?.fileSize / 1000).toFixed(1))
<TabsContent value="references" className="px-2 lg:px-2 m-0">
<EntryReferences entryTitle={entryTitle} /> return (
</TabsContent> <Tabs defaultValue="references" className="w-full h-full flex flex-col">
<TabsContent className="pl-4 lg:pl-6 m-0" value="history"> <TabsList className="rounded-none shadow-none drop-shadow-none w-full bg-sidebar sticky top-0 z-10 flex-shrink-0">
<div className="w-full"> <TabsTrigger
<p>Last modified: {history.lastModified}</p> value="references"
</div> className="pl-2 justify-start text-sm font-semibold rounded-none shadow-none data-[state=active]:border-1 data-[state=active]:border-border py-4"
</TabsContent> >
<TabsContent className="pl-4 lg:pl-6 m-0" value="metadata"> <Bookmark />
<div className="w-full"> References
<p>Size on disk: {metadata.fileSize} B</p> </TabsTrigger>
</div>
</TabsContent> <TabsTrigger
</div> className="pl-2 justify-start text-sm font-semibold rounded-none shadow-none data-[state=active]:border-1 data-[state=active]:border-border py-4"
</Tabs> value="metadata"
) >
<Braces />
Metadata
</TabsTrigger>
</TabsList>
<div className="flex-1 overflow-auto">
<TabsContent value="references" className="px-2 lg:px-2 m-0">
<EntryReferences tags={tags} backlinks={backlinks} outlinks={outlinks} />
</TabsContent>
<TabsContent className="px-2 lg:px-2 m-0" value="metadata">
<div className="w-full">
<div className="flex mb-4 gap-2">
<Badge variant="secondary">Last modified</Badge>
<div className="text-sm">
{convertDateFriendly(new Date(history.lastModified))}
</div>
</div>
<div className="flex mb-2 gap-2">
<Badge variant="secondary">Size on disk</Badge>
<div className="text-sm">{sizeInKb} Kb</div>
</div>
</div>
</TabsContent>
</div>
</Tabs>
)
} }

View file

@ -2,130 +2,130 @@
@import "tw-animate-css"; @import "tw-animate-css";
html { html {
overflow-y: hidden; overflow-y: hidden;
} }
@custom-variant dark (&:is(.dark *)); @custom-variant dark (&:is(.dark *));
@theme inline { @theme inline {
--radius-sm: calc(var(--radius) - 4px); --radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px); --radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius); --radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px); --radius-xl: calc(var(--radius) + 4px);
--color-background: var(--background); --color-background: var(--background);
--color-foreground: var(--foreground); --color-foreground: var(--foreground);
--color-card: var(--card); --color-card: var(--card);
--color-card-foreground: var(--card-foreground); --color-card-foreground: var(--card-foreground);
--color-popover: var(--popover); --color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground); --color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary); --color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground); --color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary); --color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground); --color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted); --color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground); --color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent); --color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground); --color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive); --color-destructive: var(--destructive);
--color-border: var(--border); --color-border: var(--border);
--color-input: var(--input); --color-input: var(--input);
--color-ring: var(--ring); --color-ring: var(--ring);
--color-chart-1: var(--chart-1); --color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2); --color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3); --color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4); --color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5); --color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar); --color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground); --color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary); --color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground); --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent); --color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground); --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border); --color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring); --color-sidebar-ring: var(--sidebar-ring);
} }
:root { :root {
--radius: 0.625rem; --radius: 0.625rem;
--background: oklch(1 0 0); --background: oklch(1 0 0);
--foreground: oklch(0.145 0 0); --foreground: oklch(0.145 0 0);
--card: oklch(1 0 0); --card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0); --card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0); --popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0); --popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0); --primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0); --primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0); --secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0); --secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0); --muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0); --muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0); --accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0); --accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325); --destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0); --border: oklch(0.922 0 0);
--input: oklch(0.922 0 0); --input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0); --ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116); --chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704); --chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392); --chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429); --chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08); --chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0); --sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0); --sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0); --sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0); --sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0); --sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0); --sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0); --sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0); --sidebar-ring: oklch(0.708 0 0);
color-scheme: light; color-scheme: light;
} }
.dark { .dark {
--background: oklch(0.145 0 0); --background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0); --foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0); --card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0); --card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0); --popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0); --popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0); --primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0); --primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0); --secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0); --secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0); --muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0); --muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0); --accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0); --accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216); --destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%); --border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%); --input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0); --ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376); --chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48); --chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08); --chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9); --chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439); --chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0); --sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0); --sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376); --sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0); --sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0); --sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0); --sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%); --sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0); --sidebar-ring: oklch(0.556 0 0);
color-scheme: dark; color-scheme: dark;
} }
@layer base { @layer base {
* { * {
@apply border-border outline-ring/50; @apply border-border outline-ring/50;
} }
html { html {
font-family: "Inter", sans-serif; font-family: "IBM Plex Sans", sans-serif;
} }
body { body {
@apply bg-background text-foreground; @apply bg-background text-foreground;
} }
} }

View file

@ -2,5 +2,28 @@ import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge" import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)) return twMerge(clsx(inputs))
}
export const convertDateFriendly = (isoStamp) => {
const months = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
]
const unixSeconds = new Date(isoStamp)
const day = unixSeconds.getDate()
const month = months[unixSeconds.getMonth()]
const year = unixSeconds.getFullYear()
return `${day} ${month} ${year}`
} }