mirror of
https://github.com/gabehf/Koito.git
synced 2026-04-22 12:01:52 -07:00
feat: v0.0.10 (#23)
* feat: single SOT for themes + basic custom support * fix: adjust colors for yuu theme * feat: Allow loading of environment variables from file (#20) * feat: allow loading of environment variables from file * Panic if a file for an environment variable cannot be read * Use log.Fatalf + os.Exit instead of panic * fix: remove supurfluous call to os.Exit() --------- Co-authored-by: adaexec <nixos-git.s1pht@simplelogin.com> Co-authored-by: Gabe Farrell <90876006+gabehf@users.noreply.github.com> * chore: add pr test workflow * chore: changelog * feat: make all activity grids configurable * fix: adjust activity grid style * fix: make background gradient consistent size * revert: remove year from activity grid opts * style: adjust top item list min size to 200px * feat: add support for custom themes * fix: stabilized the order of top items * chore: update changelog * feat: native import & export * fix: use correct request body for alias requests * fix: clear input when closing edit modal * chore: changelog * docs: make endpoint clearer for some apps * feat: add ui and handler for export * fix: fix pr test workflow --------- Co-authored-by: adaexec <78047743+adaexec@users.noreply.github.com> Co-authored-by: adaexec <nixos-git.s1pht@simplelogin.com>
This commit is contained in:
parent
486f5d0269
commit
c16b557c21
51 changed files with 1754 additions and 866 deletions
|
|
@ -139,6 +139,13 @@ input[type="text"]:focus {
|
|||
outline: none;
|
||||
border: 1px solid var(--color-fg-tertiary);
|
||||
}
|
||||
textarea {
|
||||
border: 1px solid var(--color-bg);
|
||||
}
|
||||
textarea:focus {
|
||||
outline: none;
|
||||
border: 1px solid var(--color-fg-tertiary);
|
||||
}
|
||||
input[type="password"] {
|
||||
border: 1px solid var(--color-bg);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,7 +45,6 @@ export default function ActivityGrid({
|
|||
albumId = 0,
|
||||
trackId = 0,
|
||||
configurable = false,
|
||||
autoAdjust = false,
|
||||
}: Props) {
|
||||
|
||||
const [color, setColor] = useState(getPrimaryColor())
|
||||
|
|
@ -111,24 +110,26 @@ export default function ActivityGrid({
|
|||
|
||||
const getDarkenAmount = (v: number, t: number): number => {
|
||||
|
||||
if (autoAdjust) {
|
||||
// automatically adjust the target value based on step
|
||||
// the smartest way to do this would be to have the api return the
|
||||
// highest value in the range. too bad im not smart
|
||||
switch (stepState) {
|
||||
case 'day':
|
||||
t = 10
|
||||
break;
|
||||
case 'week':
|
||||
t = 20
|
||||
break;
|
||||
case 'month':
|
||||
t = 50
|
||||
break;
|
||||
case 'year':
|
||||
t = 100
|
||||
break;
|
||||
}
|
||||
// really ugly way to just check if this is for all items and not a specific item.
|
||||
// is it jsut better to just pass the target in as a var? probably.
|
||||
const adjustment = artistId == albumId && albumId == trackId && trackId == 0 ? 10 : 1
|
||||
|
||||
// automatically adjust the target value based on step
|
||||
// the smartest way to do this would be to have the api return the
|
||||
// highest value in the range. too bad im not smart
|
||||
switch (stepState) {
|
||||
case 'day':
|
||||
t = 10 * adjustment
|
||||
break;
|
||||
case 'week':
|
||||
t = 20 * adjustment
|
||||
break;
|
||||
case 'month':
|
||||
t = 50 * adjustment
|
||||
break;
|
||||
case 'year':
|
||||
t = 100 * adjustment
|
||||
break;
|
||||
}
|
||||
|
||||
v = Math.min(v, t)
|
||||
|
|
@ -142,45 +143,58 @@ export default function ActivityGrid({
|
|||
}
|
||||
}
|
||||
|
||||
return (<div className="flex flex-col items-start">
|
||||
<h2>Activity</h2>
|
||||
{configurable ? (
|
||||
<ActivityOptsSelector
|
||||
rangeSetter={setRange}
|
||||
currentRange={rangeState}
|
||||
stepSetter={setStep}
|
||||
currentStep={stepState}
|
||||
/>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
<div className="w-auto grid grid-flow-col grid-rows-7 gap-[3px] md:gap-[5px]">
|
||||
{data.map((item) => (
|
||||
const CHUNK_SIZE = 26 * 7;
|
||||
const chunks = [];
|
||||
|
||||
for (let i = 0; i < data.length; i += CHUNK_SIZE) {
|
||||
chunks.push(data.slice(i, i + CHUNK_SIZE));
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-start">
|
||||
<h2>Activity</h2>
|
||||
{configurable ? (
|
||||
<ActivityOptsSelector
|
||||
rangeSetter={setRange}
|
||||
currentRange={rangeState}
|
||||
stepSetter={setStep}
|
||||
currentStep={stepState}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{chunks.map((chunk, index) => (
|
||||
<div
|
||||
key={new Date(item.start_time).toString()}
|
||||
className="w-[10px] sm:w-[12px] h-[10px] sm:h-[12px]"
|
||||
key={index}
|
||||
className="w-auto grid grid-flow-col grid-rows-7 gap-[3px] md:gap-[5px] mb-4"
|
||||
>
|
||||
<Popup
|
||||
position="top"
|
||||
space={12}
|
||||
extraClasses="left-2"
|
||||
inner={`${new Date(item.start_time).toLocaleDateString()} ${item.listens} plays`}
|
||||
>
|
||||
{chunk.map((item) => (
|
||||
<div
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
background:
|
||||
item.listens > 0
|
||||
? LightenDarkenColor(color, getDarkenAmount(item.listens, 100))
|
||||
: 'var(--color-bg-secondary)',
|
||||
}}
|
||||
className={`w-[10px] sm:w-[12px] h-[10px] sm:h-[12px] rounded-[2px] md:rounded-[3px] ${item.listens > 0 ? '' : 'border-[0.5px] border-(--color-bg-tertiary)'}`}
|
||||
></div>
|
||||
</Popup>
|
||||
key={new Date(item.start_time).toString()}
|
||||
className="w-[10px] sm:w-[12px] h-[10px] sm:h-[12px]"
|
||||
>
|
||||
<Popup
|
||||
position="top"
|
||||
space={12}
|
||||
extraClasses="left-2"
|
||||
inner={`${new Date(item.start_time).toLocaleDateString()} ${item.listens} plays`}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
background:
|
||||
item.listens > 0
|
||||
? LightenDarkenColor(color, getDarkenAmount(item.listens, 100))
|
||||
: 'var(--color-bg-secondary)',
|
||||
}}
|
||||
className={`w-[10px] sm:w-[12px] h-[10px] sm:h-[12px] rounded-[2px] md:rounded-[3px] ${
|
||||
item.listens > 0 ? '' : 'border-[0.5px] border-(--color-bg-tertiary)'
|
||||
}`}
|
||||
></div>
|
||||
</Popup>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useEffect } from "react";
|
||||
import { ChevronDown, ChevronUp } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
interface Props {
|
||||
stepSetter: (value: string) => void;
|
||||
|
|
@ -15,18 +16,15 @@ export default function ActivityOptsSelector({
|
|||
currentRange,
|
||||
disableCache = false,
|
||||
}: Props) {
|
||||
const stepPeriods = ['day', 'week', 'month', 'year'];
|
||||
const rangePeriods = [105, 182, 365];
|
||||
const stepPeriods = ['day', 'week', 'month'];
|
||||
const rangePeriods = [105, 182, 364];
|
||||
const [collapsed, setCollapsed] = useState(true);
|
||||
|
||||
const stepDisplay = (str: string): string => {
|
||||
return str.split('_').map(w =>
|
||||
w.split('').map((char, index) =>
|
||||
index === 0 ? char.toUpperCase() : char).join('')
|
||||
).join(' ');
|
||||
};
|
||||
|
||||
const rangeDisplay = (r: number): string => {
|
||||
return `${r}`
|
||||
const setMenuOpen = (val: boolean) => {
|
||||
setCollapsed(val)
|
||||
if (!disableCache) {
|
||||
localStorage.setItem('activity_configuring_' + window.location.pathname.split('/')[1], String(!val));
|
||||
}
|
||||
}
|
||||
|
||||
const setStep = (val: string) => {
|
||||
|
|
@ -42,56 +40,66 @@ export default function ActivityOptsSelector({
|
|||
localStorage.setItem('activity_range_' + window.location.pathname.split('/')[1], String(val));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (!disableCache) {
|
||||
const cachedRange = parseInt(localStorage.getItem('activity_range_' + window.location.pathname.split('/')[1]) ?? '35');
|
||||
if (cachedRange) {
|
||||
rangeSetter(cachedRange);
|
||||
}
|
||||
if (cachedRange) rangeSetter(cachedRange);
|
||||
const cachedStep = localStorage.getItem('activity_step_' + window.location.pathname.split('/')[1]);
|
||||
if (cachedStep) {
|
||||
stepSetter(cachedStep);
|
||||
}
|
||||
if (cachedStep) stepSetter(cachedStep);
|
||||
const cachedConfiguring = localStorage.getItem('activity_configuring_' + window.location.pathname.split('/')[1]);
|
||||
if (cachedStep) setMenuOpen(cachedConfiguring !== "true");
|
||||
}
|
||||
}, []);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="flex gap-2 items-center">
|
||||
<p>Step:</p>
|
||||
{stepPeriods.map((p, i) => (
|
||||
<div key={`step_selector_${p}`}>
|
||||
<button
|
||||
className={`period-selector ${p === currentStep ? 'color-fg' : 'color-fg-secondary'} ${i !== stepPeriods.length - 1 ? 'pr-2' : ''}`}
|
||||
onClick={() => setStep(p)}
|
||||
disabled={p === currentStep}
|
||||
>
|
||||
{stepDisplay(p)}
|
||||
</button>
|
||||
<span className="color-fg-secondary">
|
||||
{i !== stepPeriods.length - 1 ? '|' : ''}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="relative w-full">
|
||||
<button
|
||||
onClick={() => setMenuOpen(!collapsed)}
|
||||
className="absolute left-[75px] -top-9 text-muted hover:color-fg transition"
|
||||
title="Toggle options"
|
||||
>
|
||||
{collapsed ? <ChevronDown size={18} /> : <ChevronUp size={18} />}
|
||||
</button>
|
||||
|
||||
<div className="flex gap-2 items-center">
|
||||
<p>Range:</p>
|
||||
{rangePeriods.map((r, i) => (
|
||||
<div key={`range_selector_${r}`}>
|
||||
<button
|
||||
className={`period-selector ${r === currentRange ? 'color-fg' : 'color-fg-secondary'} ${i !== rangePeriods.length - 1 ? 'pr-2' : ''}`}
|
||||
onClick={() => setRange(r)}
|
||||
disabled={r === currentRange}
|
||||
>
|
||||
{rangeDisplay(r)}
|
||||
</button>
|
||||
<span className="color-fg-secondary">
|
||||
{i !== rangePeriods.length - 1 ? '|' : ''}
|
||||
</span>
|
||||
<div
|
||||
className={`overflow-hidden transition-[max-height,opacity] duration-250 ease ${
|
||||
collapsed ? 'max-h-0 opacity-0' : 'max-h-[100px] opacity-100'
|
||||
}`}
|
||||
>
|
||||
<div className="flex flex-wrap gap-4 mt-1 text-sm">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-muted">Step:</span>
|
||||
{stepPeriods.map((p) => (
|
||||
<button
|
||||
key={p}
|
||||
className={`px-1 rounded transition ${
|
||||
p === currentStep ? 'color-fg font-medium' : 'color-fg-secondary hover:color-fg'
|
||||
}`}
|
||||
onClick={() => setStep(p)}
|
||||
disabled={p === currentStep}
|
||||
>
|
||||
{p}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-muted">Range:</span>
|
||||
{rangePeriods.map((r) => (
|
||||
<button
|
||||
key={r}
|
||||
className={`px-1 rounded transition ${
|
||||
r === currentRange ? 'color-fg font-medium' : 'color-fg-secondary hover:color-fg'
|
||||
}`}
|
||||
onClick={() => setRange(r)}
|
||||
disabled={r === currentRange}
|
||||
>
|
||||
{r}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ interface Props {
|
|||
|
||||
export default function LastPlays(props: Props) {
|
||||
const { user } = useAppContext()
|
||||
console.log(user)
|
||||
const { isPending, isError, data, error } = useQuery({
|
||||
queryKey: ['last-listens', {
|
||||
limit: props.limit,
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ interface Props<T extends Item> {
|
|||
export default function TopItemList<T extends Item>({ data, separators, type, className }: Props<T>) {
|
||||
|
||||
return (
|
||||
<div className={`flex flex-col gap-1 ${className} min-w-[300px]`}>
|
||||
<div className={`flex flex-col gap-1 ${className} min-w-[200px]`}>
|
||||
{data.items.map((item, index) => {
|
||||
const key = `${type}-${item.id}`;
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -95,11 +95,15 @@ export default function EditModal({ open, setOpen, type, id }: Props) {
|
|||
}
|
||||
})
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
setOpen(false)
|
||||
setInput('')
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal maxW={1000} isOpen={open} onClose={() => setOpen(false)}>
|
||||
<Modal maxW={1000} isOpen={open} onClose={handleClose}>
|
||||
<div className="flex flex-col items-start gap-6 w-full">
|
||||
<div className="w-full">
|
||||
<h2>Alias Manager</h2>
|
||||
|
|
|
|||
45
client/app/components/modals/ExportModal.tsx
Normal file
45
client/app/components/modals/ExportModal.tsx
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import { useState } from "react";
|
||||
import { AsyncButton } from "../AsyncButton";
|
||||
import { getExport } from "api/api";
|
||||
|
||||
export default function ExportModal() {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
|
||||
const handleExport = () => {
|
||||
setLoading(true)
|
||||
fetch(`/apis/web/v1/export`, {
|
||||
method: "GET"
|
||||
})
|
||||
.then(res => {
|
||||
if (res.ok) {
|
||||
res.blob()
|
||||
.then(blob => {
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const a = document.createElement("a")
|
||||
a.href = url
|
||||
a.download = "koito_export.json"
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
a.remove()
|
||||
window.URL.revokeObjectURL(url)
|
||||
setLoading(false)
|
||||
})
|
||||
} else {
|
||||
res.json().then(r => setError(r.error))
|
||||
setLoading(false)
|
||||
}
|
||||
}).catch(err => {
|
||||
setError(err)
|
||||
setLoading(false)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>Export</h2>
|
||||
<AsyncButton loading={loading} onClick={handleExport}>Export Data</AsyncButton>
|
||||
{error && <p className="error">{error}</p>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -5,6 +5,8 @@ import { ThemeSwitcher } from "../themeSwitcher/ThemeSwitcher";
|
|||
import ThemeHelper from "../../routes/ThemeHelper";
|
||||
import { useAppContext } from "~/providers/AppProvider";
|
||||
import ApiKeysModal from "./ApiKeysModal";
|
||||
import { AsyncButton } from "../AsyncButton";
|
||||
import ExportModal from "./ExportModal";
|
||||
|
||||
interface Props {
|
||||
open: boolean
|
||||
|
|
@ -19,7 +21,7 @@ export default function SettingsModal({ open, setOpen } : Props) {
|
|||
const contentClasses = "w-full px-2 mt-8 sm:mt-0 sm:px-10 overflow-y-auto"
|
||||
|
||||
return (
|
||||
<Modal h={600} isOpen={open} onClose={() => setOpen(false)} maxW={900}>
|
||||
<Modal h={700} isOpen={open} onClose={() => setOpen(false)} maxW={900}>
|
||||
<Tabs
|
||||
defaultValue="Appearance"
|
||||
orientation="vertical" // still vertical, but layout is responsive via Tailwind
|
||||
|
|
@ -29,9 +31,12 @@ export default function SettingsModal({ open, setOpen } : Props) {
|
|||
<TabsTrigger className={triggerClasses} value="Appearance">Appearance</TabsTrigger>
|
||||
<TabsTrigger className={triggerClasses} value="Account">Account</TabsTrigger>
|
||||
{user && (
|
||||
<TabsTrigger className={triggerClasses} value="API Keys">
|
||||
API Keys
|
||||
</TabsTrigger>
|
||||
<>
|
||||
<TabsTrigger className={triggerClasses} value="API Keys">
|
||||
API Keys
|
||||
</TabsTrigger>
|
||||
<TabsTrigger className={triggerClasses} value="Export">Export</TabsTrigger>
|
||||
</>
|
||||
)}
|
||||
</TabsList>
|
||||
|
||||
|
|
@ -44,6 +49,9 @@ export default function SettingsModal({ open, setOpen } : Props) {
|
|||
<TabsContent value="API Keys" className={contentClasses}>
|
||||
<ApiKeysModal />
|
||||
</TabsContent>
|
||||
<TabsContent value="Export" className={contentClasses}>
|
||||
<ExportModal />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</Modal>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,36 +1,69 @@
|
|||
// ThemeSwitcher.tsx
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTheme } from '../../hooks/useTheme';
|
||||
import { themes } from '~/providers/ThemeProvider';
|
||||
import themes from '~/styles/themes.css';
|
||||
import ThemeOption from './ThemeOption';
|
||||
import { AsyncButton } from '../AsyncButton';
|
||||
|
||||
export function ThemeSwitcher() {
|
||||
const { theme, setTheme } = useTheme();
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const saved = localStorage.getItem('theme');
|
||||
if (saved && saved !== theme) {
|
||||
setTheme(saved);
|
||||
} else if (!saved) {
|
||||
localStorage.setItem('theme', theme)
|
||||
const initialTheme = {
|
||||
bg: "#1e1816",
|
||||
bgSecondary: "#2f2623",
|
||||
bgTertiary: "#453733",
|
||||
fg: "#f8f3ec",
|
||||
fgSecondary: "#d6ccc2",
|
||||
fgTertiary: "#b4a89c",
|
||||
primary: "#f5a97f",
|
||||
primaryDim: "#d88b65",
|
||||
accent: "#f9db6d",
|
||||
accentDim: "#d9bc55",
|
||||
error: "#e26c6a",
|
||||
warning: "#f5b851",
|
||||
success: "#8fc48f",
|
||||
info: "#87b8dd",
|
||||
}
|
||||
|
||||
const { setCustomTheme, getCustomTheme } = useTheme()
|
||||
const [custom, setCustom] = useState(JSON.stringify(getCustomTheme() ?? initialTheme, null, " "))
|
||||
|
||||
const handleCustomTheme = () => {
|
||||
console.log(custom)
|
||||
try {
|
||||
const theme = JSON.parse(custom)
|
||||
theme.name = "custom"
|
||||
setCustomTheme(theme)
|
||||
delete theme.name
|
||||
setCustom(JSON.stringify(theme, null, " "))
|
||||
console.log(theme)
|
||||
} catch(err) {
|
||||
console.log(err)
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (theme) {
|
||||
localStorage.setItem('theme', theme)
|
||||
setTheme(theme)
|
||||
}
|
||||
}, [theme]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2>Select Theme</h2>
|
||||
<div className="grid grid-cols-2 items-center gap-2">
|
||||
{themes.map((t) => (
|
||||
<ThemeOption setTheme={setTheme} key={t.name} theme={t} />
|
||||
))}
|
||||
<div className='flex flex-col gap-10'>
|
||||
<div>
|
||||
<h2>Select Theme</h2>
|
||||
<div className="grid grid-cols-2 items-center gap-2">
|
||||
{themes.map((t) => (
|
||||
<ThemeOption setTheme={setTheme} key={t.name} theme={t} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Use Custom Theme</h2>
|
||||
<div className="flex flex-col items-center gap-3 bg-secondary p-5 rounded-lg">
|
||||
<textarea name="custom-theme" onChange={(e) => setCustom(e.target.value)} id="custom-theme-input" className="bg-(--color-bg) h-[450px] w-[300px] p-5 rounded-md" value={custom} />
|
||||
<AsyncButton onClick={handleCustomTheme}>Submit</AsyncButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,259 +1,95 @@
|
|||
import { createContext, useEffect, useState, type ReactNode } from 'react';
|
||||
|
||||
// a fair number of colors aren't actually used, but i'm keeping
|
||||
// them so that I don't have to worry about colors when adding new ui elements
|
||||
export type Theme = {
|
||||
name: string,
|
||||
bg: string
|
||||
bgSecondary: string
|
||||
bgTertiary: string
|
||||
fg: string
|
||||
fgSecondary: string
|
||||
fgTertiary: string
|
||||
primary: string
|
||||
primaryDim: string
|
||||
accent: string
|
||||
accentDim: string
|
||||
error: string
|
||||
warning: string
|
||||
info: string
|
||||
success: string
|
||||
}
|
||||
|
||||
export const themes: Theme[] = [
|
||||
{
|
||||
name: "yuu",
|
||||
bg: "#161312",
|
||||
bgSecondary: "#272120",
|
||||
bgTertiary: "#382F2E",
|
||||
fg: "#faf5f4",
|
||||
fgSecondary: "#CCC7C6",
|
||||
fgTertiary: "#B0A3A1",
|
||||
primary: "#ff826d",
|
||||
primaryDim: "#CE6654",
|
||||
accent: "#464DAE",
|
||||
accentDim: "#393D74",
|
||||
error: "#FF6247",
|
||||
warning: "#FFC107",
|
||||
success: "#3ECE5F",
|
||||
info: "#41C4D8",
|
||||
},
|
||||
{
|
||||
name: "varia",
|
||||
bg: "rgb(25, 25, 29)",
|
||||
bgSecondary: "#222222",
|
||||
bgTertiary: "#333333",
|
||||
fg: "#eeeeee",
|
||||
fgSecondary: "#aaaaaa",
|
||||
fgTertiary: "#888888",
|
||||
primary: "rgb(203, 110, 240)",
|
||||
primaryDim: "#c28379",
|
||||
accent: "#f0ad0a",
|
||||
accentDim: "#d08d08",
|
||||
error: "#f44336",
|
||||
warning: "#ff9800",
|
||||
success: "#4caf50",
|
||||
info: "#2196f3",
|
||||
},
|
||||
{
|
||||
name: "midnight",
|
||||
bg: "rgb(8, 15, 24)",
|
||||
bgSecondary: "rgb(15, 27, 46)",
|
||||
bgTertiary: "rgb(15, 41, 70)",
|
||||
fg: "#dbdfe7",
|
||||
fgSecondary: "#9ea3a8",
|
||||
fgTertiary: "#74787c",
|
||||
primary: "#1a97eb",
|
||||
primaryDim: "#2680aa",
|
||||
accent: "#f0ad0a",
|
||||
accentDim: "#d08d08",
|
||||
error: "#f44336",
|
||||
warning: "#ff9800",
|
||||
success: "#4caf50",
|
||||
info: "#2196f3",
|
||||
},
|
||||
{
|
||||
name: "catppuccin",
|
||||
bg: "#1e1e2e",
|
||||
bgSecondary: "#181825",
|
||||
bgTertiary: "#11111b",
|
||||
fg: "#cdd6f4",
|
||||
fgSecondary: "#a6adc8",
|
||||
fgTertiary: "#9399b2",
|
||||
primary: "#89b4fa",
|
||||
primaryDim: "#739df0",
|
||||
accent: "#f38ba8",
|
||||
accentDim: "#d67b94",
|
||||
error: "#f38ba8",
|
||||
warning: "#f9e2af",
|
||||
success: "#a6e3a1",
|
||||
info: "#89dceb",
|
||||
},
|
||||
{
|
||||
name: "autumn",
|
||||
bg: "rgb(44, 25, 18)",
|
||||
bgSecondary: "rgb(70, 40, 18)",
|
||||
bgTertiary: "#4b2f1c",
|
||||
fg: "#fef9f3",
|
||||
fgSecondary: "#dbc6b0",
|
||||
fgTertiary: "#a3917a",
|
||||
primary: "#d97706",
|
||||
primaryDim: "#b45309",
|
||||
accent: "#8c4c28",
|
||||
accentDim: "#6b3b1f",
|
||||
error: "#d1433f",
|
||||
warning: "#e38b29",
|
||||
success: "#6b8e23",
|
||||
info: "#c084fc",
|
||||
},
|
||||
{
|
||||
name: "black",
|
||||
bg: "#000000",
|
||||
bgSecondary: "#1a1a1a",
|
||||
bgTertiary: "#2a2a2a",
|
||||
fg: "#dddddd",
|
||||
fgSecondary: "#aaaaaa",
|
||||
fgTertiary: "#888888",
|
||||
primary: "#08c08c",
|
||||
primaryDim: "#08c08c",
|
||||
accent: "#f0ad0a",
|
||||
accentDim: "#d08d08",
|
||||
error: "#f44336",
|
||||
warning: "#ff9800",
|
||||
success: "#4caf50",
|
||||
info: "#2196f3",
|
||||
},
|
||||
{
|
||||
name: "wine",
|
||||
bg: "#23181E",
|
||||
bgSecondary: "#2C1C25",
|
||||
bgTertiary: "#422A37",
|
||||
fg: "#FCE0B3",
|
||||
fgSecondary: "#C7AC81",
|
||||
fgTertiary: "#A78E64",
|
||||
primary: "#EA8A64",
|
||||
primaryDim: "#BD7255",
|
||||
accent: "#FAE99B",
|
||||
accentDim: "#C6B464",
|
||||
error: "#fca5a5",
|
||||
warning: "#fde68a",
|
||||
success: "#bbf7d0",
|
||||
info: "#bae6fd",
|
||||
},
|
||||
{
|
||||
name: "pearl",
|
||||
bg: "#FFFFFF",
|
||||
bgSecondary: "#EEEEEE",
|
||||
bgTertiary: "#E0E0E0",
|
||||
fg: "#333333",
|
||||
fgSecondary: "#555555",
|
||||
fgTertiary: "#777777",
|
||||
primary: "#007BFF",
|
||||
primaryDim: "#0056B3",
|
||||
accent: "#28A745",
|
||||
accentDim: "#1E7E34",
|
||||
error: "#DC3545",
|
||||
warning: "#FFC107",
|
||||
success: "#28A745",
|
||||
info: "#17A2B8",
|
||||
},
|
||||
{
|
||||
name: "asuka",
|
||||
bg: "#3B1212",
|
||||
bgSecondary: "#471B1B",
|
||||
bgTertiary: "#020202",
|
||||
fg: "#F1E9E6",
|
||||
fgSecondary: "#CCB6AE",
|
||||
fgTertiary: "#9F8176",
|
||||
primary: "#F1E9E6",
|
||||
primaryDim: "#CCB6AE",
|
||||
accent: "#41CE41",
|
||||
accentDim: "#3BA03B",
|
||||
error: "#DC143C",
|
||||
warning: "#FFD700",
|
||||
success: "#32CD32",
|
||||
info: "#1E90FF",
|
||||
},
|
||||
{
|
||||
name: "urim",
|
||||
bg: "#101713",
|
||||
bgSecondary: "#1B2921",
|
||||
bgTertiary: "#273B30",
|
||||
fg: "#D2E79E",
|
||||
fgSecondary: "#B4DA55",
|
||||
fgTertiary: "#7E9F2A",
|
||||
primary: "#ead500",
|
||||
primaryDim: "#C1B210",
|
||||
accent: "#28A745",
|
||||
accentDim: "#1E7E34",
|
||||
error: "#EE5237",
|
||||
warning: "#FFC107",
|
||||
success: "#28A745",
|
||||
info: "#17A2B8",
|
||||
},
|
||||
{
|
||||
name: "match",
|
||||
bg: "#071014",
|
||||
bgSecondary: "#0A181E",
|
||||
bgTertiary: "#112A34",
|
||||
fg: "#ebeaeb",
|
||||
fgSecondary: "#BDBDBD",
|
||||
fgTertiary: "#A2A2A2",
|
||||
primary: "#fda827",
|
||||
primaryDim: "#C78420",
|
||||
accent: "#277CFD",
|
||||
accentDim: "#1F60C1",
|
||||
error: "#F14426",
|
||||
warning: "#FFC107",
|
||||
success: "#28A745",
|
||||
info: "#17A2B8",
|
||||
},
|
||||
{
|
||||
name: "lemon",
|
||||
bg: "#1a171a",
|
||||
bgSecondary: "#2E272E",
|
||||
bgTertiary: "#443844",
|
||||
fg: "#E6E2DC",
|
||||
fgSecondary: "#B2ACA1",
|
||||
fgTertiary: "#968F82",
|
||||
primary: "#f5c737",
|
||||
primaryDim: "#C29D2F",
|
||||
accent: "#277CFD",
|
||||
accentDim: "#1F60C1",
|
||||
error: "#F14426",
|
||||
warning: "#FFC107",
|
||||
success: "#28A745",
|
||||
info: "#17A2B8",
|
||||
},
|
||||
];
|
||||
import { createContext, useEffect, useState, useCallback, type ReactNode } from 'react';
|
||||
import { type Theme } from '~/styles/themes.css';
|
||||
import { themeVars } from '~/styles/vars.css';
|
||||
|
||||
interface ThemeContextValue {
|
||||
theme: string;
|
||||
setTheme: (theme: string) => void;
|
||||
theme: string;
|
||||
setTheme: (theme: string) => void;
|
||||
setCustomTheme: (theme: Theme) => void;
|
||||
getCustomTheme: () => Theme | undefined;
|
||||
}
|
||||
|
||||
const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);
|
||||
|
||||
export function ThemeProvider({
|
||||
theme: initialTheme,
|
||||
children,
|
||||
}: {
|
||||
theme: string;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const [theme, setTheme] = useState(initialTheme);
|
||||
|
||||
useEffect(() => {
|
||||
if (theme) {
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
}
|
||||
}, [theme]);
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ theme, setTheme }}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
function toKebabCase(str: string) {
|
||||
return str.replace(/[A-Z]/g, m => '-' + m.toLowerCase());
|
||||
}
|
||||
|
||||
export { ThemeContext }
|
||||
function applyCustomThemeVars(theme: Theme) {
|
||||
const root = document.documentElement;
|
||||
for (const [key, value] of Object.entries(theme)) {
|
||||
if (key === 'name') continue;
|
||||
root.style.setProperty(`--color-${toKebabCase(key)}`, value);
|
||||
}
|
||||
}
|
||||
|
||||
function clearCustomThemeVars() {
|
||||
for (const cssVar of Object.values(themeVars)) {
|
||||
document.documentElement.style.removeProperty(cssVar);
|
||||
}
|
||||
}
|
||||
|
||||
export function ThemeProvider({
|
||||
theme: initialTheme,
|
||||
children,
|
||||
}: {
|
||||
theme: string;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const [theme, setThemeName] = useState(initialTheme);
|
||||
|
||||
const setTheme = (theme: string) => {
|
||||
setThemeName(theme)
|
||||
}
|
||||
|
||||
const setCustomTheme = useCallback((customTheme: Theme) => {
|
||||
localStorage.setItem('custom-theme', JSON.stringify(customTheme));
|
||||
applyCustomThemeVars(customTheme);
|
||||
setTheme('custom');
|
||||
}, []);
|
||||
|
||||
const getCustomTheme = (): Theme | undefined => {
|
||||
const themeStr = localStorage.getItem('custom-theme');
|
||||
if (!themeStr) {
|
||||
return undefined
|
||||
}
|
||||
try {
|
||||
let theme = JSON.parse(themeStr) as Theme
|
||||
return theme
|
||||
} catch (err) {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const root = document.documentElement;
|
||||
|
||||
root.setAttribute('data-theme', theme);
|
||||
localStorage.setItem('theme', theme)
|
||||
console.log(theme)
|
||||
|
||||
if (theme === 'custom') {
|
||||
const saved = localStorage.getItem('custom-theme');
|
||||
if (saved) {
|
||||
try {
|
||||
const parsed = JSON.parse(saved) as Theme;
|
||||
applyCustomThemeVars(parsed);
|
||||
} catch (err) {
|
||||
console.error('Invalid custom theme in localStorage', err);
|
||||
}
|
||||
} else {
|
||||
setTheme('yuu')
|
||||
}
|
||||
} else {
|
||||
clearCustomThemeVars()
|
||||
}
|
||||
}, [theme]);
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ theme, setTheme, setCustomTheme, getCustomTheme }}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export { ThemeContext };
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ export default function Home() {
|
|||
<div className="flex-1 flex flex-col items-center gap-16 min-h-0 mt-20">
|
||||
<div className="flex flex-col md:flex-row gap-10 md:gap-20">
|
||||
<AllTimeStats />
|
||||
<ActivityGrid />
|
||||
<ActivityGrid configurable />
|
||||
</div>
|
||||
<PeriodSelector setter={setPeriod} current={period} />
|
||||
<div className="flex flex-wrap gap-10 2xl:gap-20 xl:gap-10 justify-between mx-5 md:gap-5">
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ export default function Album() {
|
|||
<div className="flex flex-wrap gap-20 mt-10">
|
||||
<LastPlays limit={30} albumId={album.id} />
|
||||
<TopTracks limit={12} period={period} albumId={album.id} />
|
||||
<ActivityGrid autoAdjust configurable albumId={album.id} />
|
||||
<ActivityGrid configurable albumId={album.id} />
|
||||
</div>
|
||||
</MediaLayout>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ export default function Artist() {
|
|||
<div className="flex gap-15 mt-10 flex-wrap">
|
||||
<LastPlays limit={20} artistId={artist.id} />
|
||||
<TopTracks limit={8} period={period} artistId={artist.id} />
|
||||
<ActivityGrid configurable autoAdjust artistId={artist.id} />
|
||||
<ActivityGrid configurable artistId={artist.id} />
|
||||
</div>
|
||||
<ArtistAlbums period={period} artistId={artist.id} name={artist.name} />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ export default function MediaLayout(props: Props) {
|
|||
<main
|
||||
className="w-full flex flex-col flex-grow"
|
||||
style={{
|
||||
background: `linear-gradient(to bottom, ${bgColor}, var(--color-bg) 50%)`,
|
||||
background: `linear-gradient(to bottom, ${bgColor}, var(--color-bg) 700px)`,
|
||||
transition: '1000',
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ export default function Track() {
|
|||
</div>
|
||||
<div className="flex flex-wrap gap-20 mt-10">
|
||||
<LastPlays limit={20} trackId={track.id}/>
|
||||
<ActivityGrid trackId={track.id} configurable autoAdjust />
|
||||
<ActivityGrid trackId={track.id} configurable />
|
||||
</div>
|
||||
</MediaLayout>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -7,8 +7,44 @@ import LastPlays from "~/components/LastPlays"
|
|||
import TopAlbums from "~/components/TopAlbums"
|
||||
import TopArtists from "~/components/TopArtists"
|
||||
import TopTracks from "~/components/TopTracks"
|
||||
import { useTheme } from "~/hooks/useTheme"
|
||||
import { themes, type Theme } from "~/styles/themes.css"
|
||||
|
||||
export default function ThemeHelper() {
|
||||
const initialTheme = {
|
||||
name: "custom",
|
||||
bg: "#1e1816",
|
||||
bgSecondary: "#2f2623",
|
||||
bgTertiary: "#453733",
|
||||
fg: "#f8f3ec",
|
||||
fgSecondary: "#d6ccc2",
|
||||
fgTertiary: "#b4a89c",
|
||||
primary: "#f5a97f",
|
||||
primaryDim: "#d88b65",
|
||||
accent: "#f9db6d",
|
||||
accentDim: "#d9bc55",
|
||||
error: "#e26c6a",
|
||||
warning: "#f5b851",
|
||||
success: "#8fc48f",
|
||||
info: "#87b8dd",
|
||||
}
|
||||
|
||||
const [custom, setCustom] = useState(JSON.stringify(initialTheme, null, " "))
|
||||
const { setCustomTheme } = useTheme()
|
||||
|
||||
const handleCustomTheme = () => {
|
||||
console.log(custom)
|
||||
try {
|
||||
const theme = JSON.parse(custom) as Theme
|
||||
if (theme.name !== "custom") {
|
||||
throw new Error("theme name must be 'custom'")
|
||||
}
|
||||
console.log(theme)
|
||||
setCustomTheme(theme)
|
||||
} catch(err) {
|
||||
console.log(err)
|
||||
}
|
||||
}
|
||||
|
||||
const homeItems = 3
|
||||
|
||||
|
|
@ -24,43 +60,49 @@ export default function ThemeHelper() {
|
|||
<TopTracks period="all_time" limit={homeItems} />
|
||||
<LastPlays limit={Math.floor(homeItems * 2.5)} />
|
||||
</div>
|
||||
<div className="flex flex-col gap-6 bg-secondary p-10 rounded-lg">
|
||||
<div className="flex flex-col gap-4 items-center">
|
||||
<p>You're logged in as <strong>Example User</strong></p>
|
||||
<AsyncButton loading={false} onClick={() => {}}>Logout</AsyncButton>
|
||||
<div className="flex gap-10">
|
||||
<div className="flex flex-col items-center gap-3 bg-secondary p-5 rounded-lg">
|
||||
<textarea name="custom-theme" onChange={(e) => setCustom(e.target.value)} id="custom-theme-input" className="bg-(--color-bg) w-[300px] p-5 h-full rounded-md" value={custom} />
|
||||
<AsyncButton onClick={handleCustomTheme}>Submit</AsyncButton>
|
||||
</div>
|
||||
<div className="flex flex gap-4">
|
||||
<input
|
||||
name="koito-update-username"
|
||||
type="text"
|
||||
placeholder="Update username"
|
||||
className="w-full mx-auto fg bg rounded p-2"
|
||||
/>
|
||||
<AsyncButton loading={false} onClick={() => {}}>Submit</AsyncButton>
|
||||
<div className="flex flex-col gap-6 bg-secondary p-10 rounded-lg">
|
||||
<div className="flex flex-col gap-4 items-center">
|
||||
<p>You"re logged in as <strong>Example User</strong></p>
|
||||
<AsyncButton loading={false} onClick={() => {}}>Logout</AsyncButton>
|
||||
</div>
|
||||
<div className="flex flex gap-4">
|
||||
<input
|
||||
name="koito-update-username"
|
||||
type="text"
|
||||
placeholder="Update username"
|
||||
className="w-full mx-auto fg bg rounded p-2"
|
||||
/>
|
||||
<AsyncButton loading={false} onClick={() => {}}>Submit</AsyncButton>
|
||||
</div>
|
||||
<div className="flex flex gap-4">
|
||||
<input
|
||||
name="koito-update-password"
|
||||
type="password"
|
||||
placeholder="Update password"
|
||||
className="w-full mx-auto fg bg rounded p-2"
|
||||
/>
|
||||
<input
|
||||
name="koito-confirm-password"
|
||||
type="password"
|
||||
placeholder="Confirm password"
|
||||
className="w-full mx-auto fg bg rounded p-2"
|
||||
/>
|
||||
<AsyncButton loading={false} onClick={() => {}}>Submit</AsyncButton>
|
||||
</div>
|
||||
<div className="flex gap-2 mt-3">
|
||||
<input type="checkbox" name="reverse-merge-order" onChange={() => {}} />
|
||||
<label htmlFor="reverse-merge-order">Example checkbox</label>
|
||||
</div>
|
||||
<p className="success">successfully displayed example text</p>
|
||||
<p className="error">this is an example of error text</p>
|
||||
<p className="info">here is an informational example</p>
|
||||
<p className="warning">heed this warning, traveller</p>
|
||||
</div>
|
||||
<div className="flex flex gap-4">
|
||||
<input
|
||||
name="koito-update-password"
|
||||
type="password"
|
||||
placeholder="Update password"
|
||||
className="w-full mx-auto fg bg rounded p-2"
|
||||
/>
|
||||
<input
|
||||
name="koito-confirm-password"
|
||||
type="password"
|
||||
placeholder="Confirm password"
|
||||
className="w-full mx-auto fg bg rounded p-2"
|
||||
/>
|
||||
<AsyncButton loading={false} onClick={() => {}}>Submit</AsyncButton>
|
||||
</div>
|
||||
<div className="flex gap-2 mt-3">
|
||||
<input type="checkbox" name="reverse-merge-order" onChange={() => {}} />
|
||||
<label htmlFor="reverse-merge-order">Example checkbox</label>
|
||||
</div>
|
||||
<p className="success">successfully displayed example text</p>
|
||||
<p className="error">this is an example of error text</p>
|
||||
<p className="info">here is an informational example</p>
|
||||
<p className="warning">heed this warning, traveller</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
256
client/app/styles/themes.css.ts
Normal file
256
client/app/styles/themes.css.ts
Normal file
|
|
@ -0,0 +1,256 @@
|
|||
import { globalStyle } from "@vanilla-extract/css"
|
||||
import { themeVars } from "./vars.css"
|
||||
|
||||
export type Theme = {
|
||||
name: string,
|
||||
bg: string
|
||||
bgSecondary: string
|
||||
bgTertiary: string
|
||||
fg: string
|
||||
fgSecondary: string
|
||||
fgTertiary: string
|
||||
primary: string
|
||||
primaryDim: string
|
||||
accent: string
|
||||
accentDim: string
|
||||
error: string
|
||||
warning: string
|
||||
info: string
|
||||
success: string
|
||||
}
|
||||
|
||||
export const THEME_KEYS = [
|
||||
'--color'
|
||||
]
|
||||
|
||||
export const themes: Theme[] = [
|
||||
{
|
||||
name: "yuu",
|
||||
bg: "#1e1816",
|
||||
bgSecondary: "#2f2623",
|
||||
bgTertiary: "#453733",
|
||||
fg: "#f8f3ec",
|
||||
fgSecondary: "#d6ccc2",
|
||||
fgTertiary: "#b4a89c",
|
||||
primary: "#fc9174",
|
||||
primaryDim: "#d88b65",
|
||||
accent: "#f9db6d",
|
||||
accentDim: "#d9bc55",
|
||||
error: "#e26c6a",
|
||||
warning: "#f5b851",
|
||||
success: "#8fc48f",
|
||||
info: "#87b8dd",
|
||||
},
|
||||
{
|
||||
name: "varia",
|
||||
bg: "rgb(25, 25, 29)",
|
||||
bgSecondary: "#222222",
|
||||
bgTertiary: "#333333",
|
||||
fg: "#eeeeee",
|
||||
fgSecondary: "#aaaaaa",
|
||||
fgTertiary: "#888888",
|
||||
primary: "rgb(203, 110, 240)",
|
||||
primaryDim: "#c28379",
|
||||
accent: "#f0ad0a",
|
||||
accentDim: "#d08d08",
|
||||
error: "#f44336",
|
||||
warning: "#ff9800",
|
||||
success: "#4caf50",
|
||||
info: "#2196f3",
|
||||
},
|
||||
{
|
||||
name: "midnight",
|
||||
bg: "rgb(8, 15, 24)",
|
||||
bgSecondary: "rgb(15, 27, 46)",
|
||||
bgTertiary: "rgb(15, 41, 70)",
|
||||
fg: "#dbdfe7",
|
||||
fgSecondary: "#9ea3a8",
|
||||
fgTertiary: "#74787c",
|
||||
primary: "#1a97eb",
|
||||
primaryDim: "#2680aa",
|
||||
accent: "#f0ad0a",
|
||||
accentDim: "#d08d08",
|
||||
error: "#f44336",
|
||||
warning: "#ff9800",
|
||||
success: "#4caf50",
|
||||
info: "#2196f3",
|
||||
},
|
||||
{
|
||||
name: "catppuccin",
|
||||
bg: "#1e1e2e",
|
||||
bgSecondary: "#181825",
|
||||
bgTertiary: "#11111b",
|
||||
fg: "#cdd6f4",
|
||||
fgSecondary: "#a6adc8",
|
||||
fgTertiary: "#9399b2",
|
||||
primary: "#89b4fa",
|
||||
primaryDim: "#739df0",
|
||||
accent: "#f38ba8",
|
||||
accentDim: "#d67b94",
|
||||
error: "#f38ba8",
|
||||
warning: "#f9e2af",
|
||||
success: "#a6e3a1",
|
||||
info: "#89dceb",
|
||||
},
|
||||
{
|
||||
name: "autumn",
|
||||
bg: "rgb(44, 25, 18)",
|
||||
bgSecondary: "rgb(70, 40, 18)",
|
||||
bgTertiary: "#4b2f1c",
|
||||
fg: "#fef9f3",
|
||||
fgSecondary: "#dbc6b0",
|
||||
fgTertiary: "#a3917a",
|
||||
primary: "#d97706",
|
||||
primaryDim: "#b45309",
|
||||
accent: "#8c4c28",
|
||||
accentDim: "#6b3b1f",
|
||||
error: "#d1433f",
|
||||
warning: "#e38b29",
|
||||
success: "#6b8e23",
|
||||
info: "#c084fc",
|
||||
},
|
||||
{
|
||||
name: "black",
|
||||
bg: "#000000",
|
||||
bgSecondary: "#1a1a1a",
|
||||
bgTertiary: "#2a2a2a",
|
||||
fg: "#dddddd",
|
||||
fgSecondary: "#aaaaaa",
|
||||
fgTertiary: "#888888",
|
||||
primary: "#08c08c",
|
||||
primaryDim: "#08c08c",
|
||||
accent: "#f0ad0a",
|
||||
accentDim: "#d08d08",
|
||||
error: "#f44336",
|
||||
warning: "#ff9800",
|
||||
success: "#4caf50",
|
||||
info: "#2196f3",
|
||||
},
|
||||
{
|
||||
name: "wine",
|
||||
bg: "#23181E",
|
||||
bgSecondary: "#2C1C25",
|
||||
bgTertiary: "#422A37",
|
||||
fg: "#FCE0B3",
|
||||
fgSecondary: "#C7AC81",
|
||||
fgTertiary: "#A78E64",
|
||||
primary: "#EA8A64",
|
||||
primaryDim: "#BD7255",
|
||||
accent: "#FAE99B",
|
||||
accentDim: "#C6B464",
|
||||
error: "#fca5a5",
|
||||
warning: "#fde68a",
|
||||
success: "#bbf7d0",
|
||||
info: "#bae6fd",
|
||||
},
|
||||
{
|
||||
name: "pearl",
|
||||
bg: "#FFFFFF",
|
||||
bgSecondary: "#EEEEEE",
|
||||
bgTertiary: "#E0E0E0",
|
||||
fg: "#333333",
|
||||
fgSecondary: "#555555",
|
||||
fgTertiary: "#777777",
|
||||
primary: "#007BFF",
|
||||
primaryDim: "#0056B3",
|
||||
accent: "#28A745",
|
||||
accentDim: "#1E7E34",
|
||||
error: "#DC3545",
|
||||
warning: "#FFC107",
|
||||
success: "#28A745",
|
||||
info: "#17A2B8",
|
||||
},
|
||||
{
|
||||
name: "asuka",
|
||||
bg: "#3B1212",
|
||||
bgSecondary: "#471B1B",
|
||||
bgTertiary: "#020202",
|
||||
fg: "#F1E9E6",
|
||||
fgSecondary: "#CCB6AE",
|
||||
fgTertiary: "#9F8176",
|
||||
primary: "#F1E9E6",
|
||||
primaryDim: "#CCB6AE",
|
||||
accent: "#41CE41",
|
||||
accentDim: "#3BA03B",
|
||||
error: "#DC143C",
|
||||
warning: "#FFD700",
|
||||
success: "#32CD32",
|
||||
info: "#1E90FF",
|
||||
},
|
||||
{
|
||||
name: "urim",
|
||||
bg: "#101713",
|
||||
bgSecondary: "#1B2921",
|
||||
bgTertiary: "#273B30",
|
||||
fg: "#D2E79E",
|
||||
fgSecondary: "#B4DA55",
|
||||
fgTertiary: "#7E9F2A",
|
||||
primary: "#ead500",
|
||||
primaryDim: "#C1B210",
|
||||
accent: "#28A745",
|
||||
accentDim: "#1E7E34",
|
||||
error: "#EE5237",
|
||||
warning: "#FFC107",
|
||||
success: "#28A745",
|
||||
info: "#17A2B8",
|
||||
},
|
||||
{
|
||||
name: "match",
|
||||
bg: "#071014",
|
||||
bgSecondary: "#0A181E",
|
||||
bgTertiary: "#112A34",
|
||||
fg: "#ebeaeb",
|
||||
fgSecondary: "#BDBDBD",
|
||||
fgTertiary: "#A2A2A2",
|
||||
primary: "#fda827",
|
||||
primaryDim: "#C78420",
|
||||
accent: "#277CFD",
|
||||
accentDim: "#1F60C1",
|
||||
error: "#F14426",
|
||||
warning: "#FFC107",
|
||||
success: "#28A745",
|
||||
info: "#17A2B8",
|
||||
},
|
||||
{
|
||||
name: "lemon",
|
||||
bg: "#1a171a",
|
||||
bgSecondary: "#2E272E",
|
||||
bgTertiary: "#443844",
|
||||
fg: "#E6E2DC",
|
||||
fgSecondary: "#B2ACA1",
|
||||
fgTertiary: "#968F82",
|
||||
primary: "#f5c737",
|
||||
primaryDim: "#C29D2F",
|
||||
accent: "#277CFD",
|
||||
accentDim: "#1F60C1",
|
||||
error: "#F14426",
|
||||
warning: "#FFC107",
|
||||
success: "#28A745",
|
||||
info: "#17A2B8",
|
||||
}
|
||||
];
|
||||
|
||||
export default themes
|
||||
|
||||
themes.forEach((theme) => {
|
||||
const selector = `[data-theme="${theme.name}"]`
|
||||
|
||||
globalStyle(selector, {
|
||||
vars: {
|
||||
[themeVars.bg]: theme.bg,
|
||||
[themeVars.bgSecondary]: theme.bgSecondary,
|
||||
[themeVars.bgTertiary]: theme.bgTertiary,
|
||||
[themeVars.fg]: theme.fg,
|
||||
[themeVars.fgSecondary]: theme.fgSecondary,
|
||||
[themeVars.fgTertiary]: theme.fgTertiary,
|
||||
[themeVars.primary]: theme.primary,
|
||||
[themeVars.primaryDim]: theme.primaryDim,
|
||||
[themeVars.accent]: theme.accent,
|
||||
[themeVars.accentDim]: theme.accentDim,
|
||||
[themeVars.error]: theme.error,
|
||||
[themeVars.warning]: theme.warning,
|
||||
[themeVars.success]: theme.success,
|
||||
[themeVars.info]: theme.info,
|
||||
}
|
||||
})
|
||||
})
|
||||
16
client/app/styles/vars.css.ts
Normal file
16
client/app/styles/vars.css.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
export const themeVars = {
|
||||
bg: '--color-bg',
|
||||
bgSecondary: '--color-bg-secondary',
|
||||
bgTertiary: '--color-bg-tertiary',
|
||||
fg: '--color-fg',
|
||||
fgSecondary: '--color-fg-secondary',
|
||||
fgTertiary: '--color-fg-tertiary',
|
||||
primary: '--color-primary',
|
||||
primaryDim: '--color-primary-dim',
|
||||
accent: '--color-accent',
|
||||
accentDim: '--color-accent-dim',
|
||||
error: '--color-error',
|
||||
warning: '--color-warning',
|
||||
info: '--color-info',
|
||||
success: '--color-success',
|
||||
}
|
||||
|
|
@ -1,391 +1,5 @@
|
|||
/* Theme Definitions */
|
||||
|
||||
[data-theme="varia"]{
|
||||
/* Backgrounds */
|
||||
--color-bg:rgb(25, 25, 29);
|
||||
--color-bg-secondary: #222222;
|
||||
--color-bg-tertiary: #333333;
|
||||
|
||||
/* Foregrounds */
|
||||
--color-fg: #eeeeee;
|
||||
--color-fg-secondary: #aaaaaa;
|
||||
--color-fg-tertiary: #888888;
|
||||
|
||||
/* Accents */
|
||||
--color-primary:rgb(203, 110, 240);
|
||||
--color-primary-dim: #c28379;
|
||||
--color-accent: #f0ad0a;
|
||||
--color-accent-dim: #d08d08;
|
||||
|
||||
/* Status Colors */
|
||||
--color-error: #f44336;
|
||||
--color-warning: #ff9800;
|
||||
--color-success: #4caf50;
|
||||
--color-info: #2196f3;
|
||||
|
||||
/* Borders and Shadows */
|
||||
--color-border: var(--color-bg-tertiary);
|
||||
--color-shadow: rgba(0, 0, 0, 0.5);
|
||||
|
||||
/* Interactive Elements */
|
||||
--color-link: var(--color-primary);
|
||||
--color-link-hover: var(--color-primary-dim);
|
||||
}
|
||||
|
||||
[data-theme="wine"] {
|
||||
/* Backgrounds */
|
||||
--color-bg: #23181E;
|
||||
--color-bg-secondary: #2C1C25;
|
||||
--color-bg-tertiary: #422A37;
|
||||
|
||||
/* Foregrounds */
|
||||
--color-fg: #FCE0B3;
|
||||
--color-fg-secondary:#C7AC81;
|
||||
--color-fg-tertiary:#A78E64;
|
||||
|
||||
/* Accents */
|
||||
--color-primary: #EA8A64;
|
||||
--color-primary-dim: #BD7255;
|
||||
--color-accent: #FAE99B;
|
||||
--color-accent-dim: #C6B464;
|
||||
|
||||
/* Status Colors */
|
||||
--color-error: #fca5a5;
|
||||
--color-warning: #fde68a;
|
||||
--color-success: #bbf7d0;
|
||||
--color-info: #bae6fd;
|
||||
|
||||
/* Borders and Shadows */
|
||||
--color-border: var(--color-bg-tertiary);
|
||||
--color-shadow: rgba(0, 0, 0, 0.05);
|
||||
|
||||
/* Interactive Elements */
|
||||
--color-link: var(--color-primary);
|
||||
--color-link-hover: var(--color-primary-dim);
|
||||
}
|
||||
|
||||
[data-theme="asuka"] {
|
||||
/* Backgrounds */
|
||||
--color-bg: #3B1212;
|
||||
--color-bg-secondary: #471B1B;
|
||||
--color-bg-tertiary: #020202;
|
||||
|
||||
/* Foregrounds */
|
||||
--color-fg: #F1E9E6;
|
||||
--color-fg-secondary: #CCB6AE;
|
||||
--color-fg-tertiary: #9F8176;
|
||||
|
||||
/* Accents */
|
||||
--color-primary: #F1E9E6;
|
||||
--color-primary-dim: #CCB6AE;
|
||||
--color-accent: #41CE41;
|
||||
--color-accent-dim: #3BA03B;
|
||||
|
||||
/* Status Colors */
|
||||
--color-error: #EB97A8;
|
||||
--color-warning: #FFD700;
|
||||
--color-success: #32CD32;
|
||||
--color-info: #1E90FF;
|
||||
|
||||
/* Borders and Shadows (derived from existing colors for consistency) */
|
||||
--color-border: var(--color-bg-tertiary);
|
||||
--color-shadow: rgba(0, 0, 0, 0.1); /* Slightly more prominent shadow for contrast */
|
||||
|
||||
/* Interactive Elements */
|
||||
--color-link: var(--color-primary);
|
||||
--color-link-hover: var(--color-primary-dim);
|
||||
}
|
||||
|
||||
[data-theme="midnight"] {
|
||||
/* Backgrounds */
|
||||
--color-bg:rgb(8, 15, 24);
|
||||
--color-bg-secondary:rgb(15, 27, 46);
|
||||
--color-bg-tertiary:rgb(15, 41, 70);
|
||||
|
||||
/* Foregrounds */
|
||||
--color-fg: #dbdfe7;
|
||||
--color-fg-secondary: #9ea3a8;
|
||||
--color-fg-tertiary: #74787c;
|
||||
|
||||
/* Accents */
|
||||
--color-primary: #1a97eb;
|
||||
--color-primary-dim: #2680aa;
|
||||
--color-accent: #f0ad0a;
|
||||
--color-accent-dim: #d08d08;
|
||||
|
||||
/* Status Colors */
|
||||
--color-error: #f44336;
|
||||
--color-warning: #ff9800;
|
||||
--color-success: #4caf50;
|
||||
--color-info: #2196f3;
|
||||
|
||||
/* Borders and Shadows */
|
||||
--color-border: var(--color-bg-tertiary);
|
||||
--color-shadow: rgba(0, 0, 0, 0.5);
|
||||
|
||||
/* Interactive Elements */
|
||||
--color-link: var(--color-primary);
|
||||
--color-link-hover: var(--color-primary-dim);
|
||||
}
|
||||
|
||||
/* TODO: Adjust */
|
||||
[data-theme="catppuccin"] {
|
||||
/* Backgrounds */
|
||||
--color-bg: #1e1e2e;
|
||||
--color-bg-secondary: #181825;
|
||||
--color-bg-tertiary: #11111b;
|
||||
|
||||
/* Foregrounds */
|
||||
--color-fg: #cdd6f4;
|
||||
--color-fg-secondary: #a6adc8;
|
||||
--color-fg-tertiary: #9399b2;
|
||||
|
||||
/* Accents */
|
||||
--color-primary: #cba6f7;
|
||||
--color-primary-dim: #739df0;
|
||||
--color-accent: #f38ba8;
|
||||
--color-accent-dim: #d67b94;
|
||||
|
||||
/* Status Colors */
|
||||
--color-error: #f38ba8;
|
||||
--color-warning: #f9e2af;
|
||||
--color-success: #a6e3a1;
|
||||
--color-info: #89dceb;
|
||||
|
||||
/* Borders and Shadows */
|
||||
--color-border: var(--color-bg-tertiary);
|
||||
--color-shadow: rgba(0, 0, 0, 0.5);
|
||||
|
||||
/* Interactive Elements */
|
||||
--color-link: var(--color-primary);
|
||||
--color-link-hover: var(--color-primary-dim);
|
||||
}
|
||||
|
||||
[data-theme="pearl"] {
|
||||
/* Backgrounds */
|
||||
--color-bg: #FFFFFF;
|
||||
--color-bg-secondary: #EEEEEE;
|
||||
--color-bg-tertiary: #E0E0E0;
|
||||
|
||||
/* Foregrounds */
|
||||
--color-fg: #333333;
|
||||
--color-fg-secondary: #555555;
|
||||
--color-fg-tertiary: #777777;
|
||||
|
||||
/* Accents */
|
||||
--color-primary: #007BFF;
|
||||
--color-primary-dim: #0056B3;
|
||||
--color-accent: #28A745;
|
||||
--color-accent-dim: #1E7E34;
|
||||
|
||||
/* Status Colors */
|
||||
--color-error: #DC3545;
|
||||
--color-warning: #CE9B00;
|
||||
--color-success: #099B2B;
|
||||
--color-info: #02B3CE;
|
||||
|
||||
/* Borders and Shadows */
|
||||
--color-border: var(--color-bg-tertiary);
|
||||
--color-shadow: rgba(0, 0, 0, 0.1);
|
||||
|
||||
/* Interactive Elements */
|
||||
--color-link: var(--color-primary);
|
||||
--color-link-hover: var(--color-primary-dim);
|
||||
}
|
||||
|
||||
[data-theme="urim"] {
|
||||
/* Backgrounds */
|
||||
--color-bg: #101713;
|
||||
--color-bg-secondary: #1B2921;
|
||||
--color-bg-tertiary: #273B30;
|
||||
|
||||
/* Foregrounds */
|
||||
--color-fg: #D2E79E;
|
||||
--color-fg-secondary: #B4DA55;
|
||||
--color-fg-tertiary: #7E9F2A;
|
||||
|
||||
/* Accents */
|
||||
--color-primary: #ead500;
|
||||
--color-primary-dim: #C1B210;
|
||||
--color-accent: #28A745;
|
||||
--color-accent-dim: #1E7E34;
|
||||
|
||||
/* Status Colors */
|
||||
--color-error: #EE5237;
|
||||
--color-warning: #FFC107;
|
||||
--color-success: #28A745;
|
||||
--color-info: #17A2B8;
|
||||
|
||||
/* Borders and Shadows */
|
||||
--color-border: var(--color-bg-tertiary);
|
||||
--color-shadow: rgba(0, 0, 0, 0.1);
|
||||
|
||||
/* Interactive Elements */
|
||||
--color-link: var(--color-primary);
|
||||
--color-link-hover: var(--color-primary-dim);
|
||||
}
|
||||
|
||||
[data-theme="yuu"] {
|
||||
/* Backgrounds */
|
||||
--color-bg: #161312;
|
||||
--color-bg-secondary: #272120;
|
||||
--color-bg-tertiary: #382F2E;
|
||||
|
||||
/* Foregrounds */
|
||||
--color-fg: #faf5f4;
|
||||
--color-fg-secondary: #CCC7C6;
|
||||
--color-fg-tertiary: #B0A3A1;
|
||||
|
||||
/* Accents */
|
||||
--color-primary: #ff826d;
|
||||
--color-primary-dim: #CE6654;
|
||||
--color-accent: #464DAE;
|
||||
--color-accent-dim: #393D74;
|
||||
|
||||
/* Status Colors */
|
||||
--color-error: #FF6247;
|
||||
--color-warning: #FFC107;
|
||||
--color-success: #3ECE5F;
|
||||
--color-info: #41C4D8;
|
||||
|
||||
/* Borders and Shadows */
|
||||
--color-border: var(--color-bg-tertiary);
|
||||
--color-shadow: rgba(0, 0, 0, 0.1);
|
||||
|
||||
/* Interactive Elements */
|
||||
--color-link: var(--color-primary);
|
||||
--color-link-hover: var(--color-primary-dim);
|
||||
}
|
||||
|
||||
[data-theme="match"] {
|
||||
/* Backgrounds */
|
||||
--color-bg: #071014;
|
||||
--color-bg-secondary: #0A181E;
|
||||
--color-bg-tertiary: #112A34;
|
||||
|
||||
/* Foregrounds */
|
||||
--color-fg: #ebeaeb;
|
||||
--color-fg-secondary: #BDBDBD;
|
||||
--color-fg-tertiary: #A2A2A2;
|
||||
|
||||
/* Accents */
|
||||
--color-primary: #fda827;
|
||||
--color-primary-dim: #C78420;
|
||||
--color-accent: #277CFD;
|
||||
--color-accent-dim: #1F60C1;
|
||||
|
||||
/* Status Colors */
|
||||
--color-error: #F14426;
|
||||
--color-warning: #FFC107;
|
||||
--color-success: #28A745;
|
||||
--color-info: #17A2B8;
|
||||
|
||||
/* Borders and Shadows */
|
||||
--color-border: var(--color-bg-tertiary);
|
||||
--color-shadow: rgba(0, 0, 0, 0.1);
|
||||
|
||||
/* Interactive Elements */
|
||||
--color-link: var(--color-primary);
|
||||
--color-link-hover: var(--color-primary-dim);
|
||||
}
|
||||
|
||||
[data-theme="lemon"] {
|
||||
/* Backgrounds */
|
||||
--color-bg: #1a171a;
|
||||
--color-bg-secondary: #2E272E;
|
||||
--color-bg-tertiary: #443844;
|
||||
|
||||
/* Foregrounds */
|
||||
--color-fg: #E6E2DC;
|
||||
--color-fg-secondary: #B2ACA1;
|
||||
--color-fg-tertiary: #968F82;
|
||||
|
||||
/* Accents */
|
||||
--color-primary: #f5c737;
|
||||
--color-primary-dim: #C29D2F;
|
||||
--color-accent: #277CFD;
|
||||
--color-accent-dim: #1F60C1;
|
||||
|
||||
/* Status Colors */
|
||||
--color-error: #F14426;
|
||||
--color-warning: #FFC107;
|
||||
--color-success: #28A745;
|
||||
--color-info: #17A2B8;
|
||||
|
||||
/* Borders and Shadows */
|
||||
--color-border: var(--color-bg-tertiary);
|
||||
--color-shadow: rgba(0, 0, 0, 0.1);
|
||||
|
||||
/* Interactive Elements */
|
||||
--color-link: var(--color-primary);
|
||||
--color-link-hover: var(--color-primary-dim);
|
||||
}
|
||||
|
||||
[data-theme="autumn"] {
|
||||
/* Backgrounds */
|
||||
--color-bg:rgb(44, 25, 18);
|
||||
--color-bg-secondary:rgb(70, 40, 18);
|
||||
--color-bg-tertiary: #4b2f1c;
|
||||
|
||||
/* Foregrounds */
|
||||
--color-fg: #fef9f3;
|
||||
--color-fg-secondary: #dbc6b0;
|
||||
--color-fg-tertiary: #a3917a;
|
||||
|
||||
/* Accents */
|
||||
--color-primary: #d97706;
|
||||
--color-primary-dim: #b45309;
|
||||
--color-accent: #8c4c28;
|
||||
--color-accent-dim: #6b3b1f;
|
||||
|
||||
/* Status Colors */
|
||||
--color-error: #d1433f;
|
||||
--color-warning: #e38b29;
|
||||
--color-success: #6b8e23;
|
||||
--color-info: #c084fc;
|
||||
|
||||
/* Borders and Shadows */
|
||||
--color-border: var(--color-bg-tertiary);
|
||||
--color-shadow: rgba(0, 0, 0, 0.4);
|
||||
|
||||
/* Interactive Elements */
|
||||
--color-link: var(--color-primary);
|
||||
--color-link-hover: var(--color-primary-dim);
|
||||
}
|
||||
|
||||
[data-theme="black"] {
|
||||
/* Backgrounds */
|
||||
--color-bg: #000000;
|
||||
--color-bg-secondary: #1a1a1a;
|
||||
--color-bg-tertiary: #2a2a2a;
|
||||
|
||||
/* Foregrounds */
|
||||
--color-fg: #dddddd;
|
||||
--color-fg-secondary: #aaaaaa;
|
||||
--color-fg-tertiary: #888888;
|
||||
|
||||
/* Accents */
|
||||
--color-primary: #08c08c;
|
||||
--color-primary-dim: #08c08c;
|
||||
--color-accent: #f0ad0a;
|
||||
--color-accent-dim: #d08d08;
|
||||
|
||||
/* Status Colors */
|
||||
--color-error: #f44336;
|
||||
--color-warning: #ff9800;
|
||||
--color-success: #4caf50;
|
||||
--color-info: #2196f3;
|
||||
|
||||
/* Borders and Shadows */
|
||||
--color-border: var(--color-bg-tertiary);
|
||||
--color-shadow: rgba(0, 0, 0, 0.5);
|
||||
|
||||
/* Interactive Elements */
|
||||
--color-link: #0af0af;
|
||||
--color-link-hover: #08c08c;
|
||||
}
|
||||
|
||||
|
||||
/* Theme Helper Classes */
|
||||
|
||||
/* Foreground Text */
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue