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=IBM+Plex+Sans:ital,wght@0,100..700;1,100..700&display=swap");
code {
font-family: "JetBrains Mono";

View file

@ -10,68 +10,68 @@ import { Link } from "react-router"
import { useState, useRef, useEffect } from "react"
export default function EntriesListSidebar() {
const scrollRef = useRef(null)
const [isOpen, setIsOpen] = useState(() => {
if (typeof window !== "undefined") {
const saved = sessionStorage.getItem("entries_list_sidebar_open")
return saved ? JSON.parse(saved) : false
}
return false
})
const scrollRef = useRef(null)
const [isOpen, setIsOpen] = useState(() => {
if (typeof window !== "undefined") {
const saved = sessionStorage.getItem("entries_list_sidebar_open")
return saved ? JSON.parse(saved) : false
}
return false
})
const { data: entries, isLoading } = useQuery({
queryKey: ["entries_list"],
queryFn: () => api.get("/entries").then((res) => res.data),
})
const { data: entries, isLoading } = useQuery({
queryKey: ["entries_list"],
queryFn: () => api.get("/entries").then((res) => res.data),
})
useEffect(() => {
sessionStorage.setItem("entries_list_sidebar_open", JSON.stringify(isOpen))
}, [isOpen])
useEffect(() => {
sessionStorage.setItem("entries_list_sidebar_open", JSON.stringify(isOpen))
}, [isOpen])
useEffect(() => {
const savedScroll = sessionStorage.getItem("entries_list_sidebar_scroll_position")
if (savedScroll && scrollRef.current) {
scrollRef.current.scrollTop = parseInt(savedScroll)
}
}, [entries])
useEffect(() => {
const savedScroll = sessionStorage.getItem("entries_list_sidebar_scroll_position")
if (savedScroll && scrollRef.current) {
scrollRef.current.scrollTop = parseInt(savedScroll)
}
}, [entries])
const handleScroll = () => {
if (scrollRef.current) {
sessionStorage.setItem("entries_list_sidebar_scroll_position", scrollRef.current.scrollTop)
}
}
const handleScroll = () => {
if (scrollRef.current) {
sessionStorage.setItem("entries_list_sidebar_scroll_position", scrollRef.current.scrollTop)
}
}
return (
<Collapsible open={isOpen} onOpenChange={setIsOpen} className="group/collapsible">
<SidebarMenuItem key="entries">
<CollapsibleTrigger asChild className="rounded-none">
<SidebarMenuButton asChild className="rounded-none">
<a href="#">
<FileText />
<span>Entries</span>
<Badge className="ml-0" variant="secondary">
{entries?.count}
</Badge>
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
</a>
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
<div ref={scrollRef} onScroll={handleScroll} className="max-h-100 overflow-y-auto">
<SidebarMenuSub>
{entries?.data.map((item, i) => (
<SidebarMenuItem key={i}>
<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">
{item.title.replace(/_/g, " ")}
</span>
</Link>
</SidebarMenuItem>
))}
</SidebarMenuSub>
</div>
</CollapsibleContent>
</SidebarMenuItem>
</Collapsible>
)
return (
<Collapsible open={isOpen} onOpenChange={setIsOpen} className="group/collapsible">
<SidebarMenuItem key="entries">
<CollapsibleTrigger asChild className="rounded-none">
<SidebarMenuButton asChild className="rounded-none">
<a href="#">
<FileText />
<span className="font-medium">Entries</span>
<Badge className="ml-0" variant="secondary">
{entries?.count}
</Badge>
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
</a>
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
<div ref={scrollRef} onScroll={handleScroll} className="max-h-100 overflow-y-auto">
<SidebarMenuSub>
{entries?.data.map((item, i) => (
<SidebarMenuItem key={i}>
<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">
{item.title.replace(/_/g, " ")}
</span>
</Link>
</SidebarMenuItem>
))}
</SidebarMenuSub>
</div>
</CollapsibleContent>
</SidebarMenuItem>
</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 { Badge } from "./ui/badge"
export default function EntryReferences({ entryTitle }) {
const { data: tags, isLoading: tagsLoading } = useQuery({
queryKey: [`tags_for_${entryTitle}`],
queryFn: () => api.get(`/tags/${entryTitle}`).then((res) => res.data),
})
export default function EntryReferences({ tags, backlinks, outlinks }) {
return (
<div className="w-full md:flex flex-row justify-stretch gap-3">
<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({
queryKey: [`backlinks_for_${entryTitle}`],
queryFn: () => api.get(`/entries/backlinks/${entryTitle}`).then((res) => res.data),
})
<div className="mt-2 mb-3 pl-1">
{backlinks &&
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({
queryKey: [`outlinks_for_${entryTitle}`],
queryFn: () => api.get(`/entries/outlinks/${entryTitle}`).then((res) => res.data),
})
<div className="flex flex row justify-between bg-sidebar">
<h3 className="font-medium text-sm p-1 ml-1">Outgoing links</h3>
<Badge variant="secondary" className="rounded-none">
{outlinks?.count}
</Badge>
</div>
return (
<div className="w-full md:flex flex-row justify-stretch gap-3">
<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>
<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="mt-2 mb-3 pl-1">
{backlinks &&
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>
<div className="flex flex row justify-between bg-sidebar">
<h3 className="font-medium text-sm p-1 ml-1">Outgoing links</h3>
<Badge variant="secondary" className="rounded-none">
{outlinks?.count}
</Badge>
</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>
)
<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"
export default function NetworkGraph({ data }) {
const nodeCount = data?.nodes.length
const nodeCount = data?.nodes.length
const sigmaStyle = {
height: "400px",
width: "100%",
}
const sigmaStyle = {
height: "400px",
width: "100%",
}
const settings = {
allowInvalidContainer: true,
defaultEdgeColor: "#a4a4a4",
labelColor: { color: "#0a0a0a" },
labelFont: "Inter",
labelSize: 14,
labelWeight: "400",
labelRenderedSizeThreshold: nodeCount > 15 ? 10 : 8,
renderLabels: true,
}
const settings = {
allowInvalidContainer: true,
defaultEdgeColor: "#a4a4a4",
labelColor: { color: "#0a0a0a" },
labelFont: "IBM Plex Sans",
labelSize: 14,
labelWeight: "400",
labelRenderedSizeThreshold: nodeCount > 15 ? 10 : 8,
renderLabels: true,
}
return (
<div
style={{
width: "100%",
height: "400px",
backgroundColor: "#fff",
overflow: "hidden",
contain: "strict",
}}
>
<SigmaContainer style={sigmaStyle} settings={settings}>
<LoadGraph data={data} />
<GraphEvents />
</SigmaContainer>
</div>
)
return (
<div
style={{
width: "100%",
height: "400px",
backgroundColor: "#fff",
overflow: "hidden",
contain: "strict",
}}
>
<SigmaContainer style={sigmaStyle} settings={settings}>
<LoadGraph data={data} />
<GraphEvents />
</SigmaContainer>
</div>
)
}

View file

@ -9,69 +9,69 @@ import { Link } from "react-router"
import { useState, useRef, useEffect } from "react"
export default function TagListSidebar() {
const scrollRef = useRef(null)
const scrollRef = useRef(null)
const [isOpen, setIsOpen] = useState(() => {
if (typeof window !== "undefined") {
const saved = sessionStorage.getItem("tags_list_sidebar_open")
return saved ? JSON.parse(saved) : false
}
return false
})
const [isOpen, setIsOpen] = useState(() => {
if (typeof window !== "undefined") {
const saved = sessionStorage.getItem("tags_list_sidebar_open")
return saved ? JSON.parse(saved) : false
}
return false
})
const { data: tags } = useQuery({
queryKey: ["tag_list"],
queryFn: () => api.get("/tags").then((res) => res.data),
})
const { data: tags } = useQuery({
queryKey: ["tag_list"],
queryFn: () => api.get("/tags").then((res) => res.data),
})
useEffect(() => {
sessionStorage.setItem("tags_list_sidebar_open", JSON.stringify(isOpen))
}, [isOpen])
useEffect(() => {
sessionStorage.setItem("tags_list_sidebar_open", JSON.stringify(isOpen))
}, [isOpen])
useEffect(() => {
const savedScroll = sessionStorage.getItem("tags_list_sidebar_open")
if (savedScroll && scrollRef.current) {
scrollRef.current.scrollTop = parseInt(savedScroll)
}
}, [tags])
useEffect(() => {
const savedScroll = sessionStorage.getItem("tags_list_sidebar_open")
if (savedScroll && scrollRef.current) {
scrollRef.current.scrollTop = parseInt(savedScroll)
}
}, [tags])
const handleScroll = () => {
if (scrollRef.current) {
sessionStorage.setItem("tags_list_sidebar_open", scrollRef.current.scrollTop)
}
}
const handleScroll = () => {
if (scrollRef.current) {
sessionStorage.setItem("tags_list_sidebar_open", scrollRef.current.scrollTop)
}
}
return (
<Collapsible open={isOpen} onOpenChange={setIsOpen} className="group/collapsible">
<SidebarMenuItem key="tags">
<CollapsibleTrigger asChild>
<SidebarMenuButton asChild className="rounded-none">
<a href="#">
<Tags />
<span>Tags</span>
<Badge className="ml-0" variant="secondary">
{tags?.count}
</Badge>
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
</a>
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
<div ref={scrollRef} onScroll={handleScroll} className="max-h-100 overflow-y-auto">
<SidebarMenuSub>
{tags?.data.map((item, i) => (
<SidebarMenuItem key={i}>
<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">
{item}
</span>
</Link>
</SidebarMenuItem>
))}
</SidebarMenuSub>
</div>
</CollapsibleContent>
</SidebarMenuItem>
</Collapsible>
)
return (
<Collapsible open={isOpen} onOpenChange={setIsOpen} className="group/collapsible">
<SidebarMenuItem key="tags">
<CollapsibleTrigger asChild>
<SidebarMenuButton asChild className="rounded-none">
<a href="#">
<Tags />
<span className="font-medium">Tags</span>
<Badge className="ml-0" variant="secondary">
{tags?.count}
</Badge>
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
</a>
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
<div ref={scrollRef} onScroll={handleScroll} className="max-h-100 overflow-y-auto">
<SidebarMenuSub>
{tags?.data.map((item, i) => (
<SidebarMenuItem key={i}>
<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">
{item}
</span>
</Link>
</SidebarMenuItem>
))}
</SidebarMenuSub>
</div>
</CollapsibleContent>
</SidebarMenuItem>
</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 { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Bookmark } from "lucide-react"
import { History } 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 }) {
return (
<Tabs defaultValue="references" className="w-full h-full flex flex-col">
<TabsList className="rounded-none shadow-none drop-shadow-none w-full bg-sidebar sticky top-0 z-10 flex-shrink-0">
<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>
const { data: tags } = useQuery({
queryKey: [`tags_for_${entryTitle}`],
queryFn: () => api.get(`/tags/${entryTitle}`).then((res) => res.data),
})
<TabsTrigger
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"
value="history"
>
<History />
History
</TabsTrigger>
const { data: backlinks } = useQuery({
queryKey: [`backlinks_for_${entryTitle}`],
queryFn: () => api.get(`/entries/backlinks/${entryTitle}`).then((res) => res.data),
})
<TabsTrigger
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"
value="metadata"
>
<Braces />
Metadata
</TabsTrigger>
</TabsList>
const { data: outlinks } = useQuery({
queryKey: [`outlinks_for_${entryTitle}`],
queryFn: () => api.get(`/entries/outlinks/${entryTitle}`).then((res) => res.data),
})
<div className="flex-1 overflow-auto">
<TabsContent value="references" className="px-2 lg:px-2 m-0">
<EntryReferences entryTitle={entryTitle} />
</TabsContent>
<TabsContent className="pl-4 lg:pl-6 m-0" value="history">
<div className="w-full">
<p>Last modified: {history.lastModified}</p>
</div>
</TabsContent>
<TabsContent className="pl-4 lg:pl-6 m-0" value="metadata">
<div className="w-full">
<p>Size on disk: {metadata.fileSize} B</p>
</div>
</TabsContent>
</div>
</Tabs>
)
const sizeInKb = parseFloat((metadata?.fileSize / 1000).toFixed(1))
return (
<Tabs defaultValue="references" className="w-full h-full flex flex-col">
<TabsList className="rounded-none shadow-none drop-shadow-none w-full bg-sidebar sticky top-0 z-10 flex-shrink-0">
<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
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"
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";
html {
overflow-y: hidden;
overflow-y: hidden;
}
@custom-variant dark (&:is(.dark *));
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
color-scheme: light;
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
color-scheme: light;
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
color-scheme: dark;
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
color-scheme: dark;
}
@layer base {
* {
@apply border-border outline-ring/50;
}
* {
@apply border-border outline-ring/50;
}
html {
font-family: "Inter", sans-serif;
}
html {
font-family: "IBM Plex Sans", sans-serif;
}
body {
@apply bg-background text-foreground;
}
body {
@apply bg-background text-foreground;
}
}

View file

@ -2,5 +2,28 @@ import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
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}`
}