28 changed files with 945 additions and 448 deletions
@ -0,0 +1,32 @@
@@ -0,0 +1,32 @@
|
||||
import { ofetch } from "ofetch"; |
||||
|
||||
import { getAuthHeaders } from "@/backend/accounts/auth"; |
||||
import { AccountWithToken } from "@/stores/auth"; |
||||
|
||||
export interface SessionResponse { |
||||
id: string; |
||||
userId: string; |
||||
createdAt: string; |
||||
accessedAt: string; |
||||
device: string; |
||||
userAgent: string; |
||||
} |
||||
|
||||
export async function getSessions(url: string, account: AccountWithToken) { |
||||
return ofetch<SessionResponse[]>(`/users/${account.userId}/sessions`, { |
||||
headers: getAuthHeaders(account.token), |
||||
baseURL: url, |
||||
}); |
||||
} |
||||
|
||||
export async function removeSession( |
||||
url: string, |
||||
token: string, |
||||
sessionId: string |
||||
) { |
||||
return ofetch<SessionResponse[]>(`/sessions/${sessionId}`, { |
||||
method: "DELETE", |
||||
headers: getAuthHeaders(token), |
||||
baseURL: url, |
||||
}); |
||||
} |
@ -0,0 +1,37 @@
@@ -0,0 +1,37 @@
|
||||
import classNames from "classnames"; |
||||
|
||||
export function SettingsCard(props: { |
||||
children: React.ReactNode; |
||||
className?: string; |
||||
paddingClass?: string; |
||||
}) { |
||||
return ( |
||||
<div |
||||
className={classNames( |
||||
"w-full rounded-lg bg-settings-card-background bg-opacity-[0.15] border border-settings-card-border", |
||||
props.paddingClass ?? "px-8 py-6", |
||||
props.className |
||||
)} |
||||
> |
||||
{props.children} |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
export function SolidSettingsCard(props: { |
||||
children: React.ReactNode; |
||||
className?: string; |
||||
paddingClass?: string; |
||||
}) { |
||||
return ( |
||||
<div |
||||
className={classNames( |
||||
"w-full rounded-lg bg-settings-card-altBackground bg-opacity-50", |
||||
props.paddingClass ?? "px-8 py-6", |
||||
props.className |
||||
)} |
||||
> |
||||
{props.children} |
||||
</div> |
||||
); |
||||
} |
@ -0,0 +1,45 @@
@@ -0,0 +1,45 @@
|
||||
import classNames from "classnames"; |
||||
|
||||
import { Icon, Icons } from "@/components/Icon"; |
||||
|
||||
export function SidebarSection(props: { |
||||
title: string; |
||||
children: React.ReactNode; |
||||
}) { |
||||
return ( |
||||
<section> |
||||
<p className="text-sm font-bold uppercase text-settings-sidebar-type-secondary mb-2"> |
||||
{props.title} |
||||
</p> |
||||
{props.children} |
||||
</section> |
||||
); |
||||
} |
||||
|
||||
export function SidebarLink(props: { |
||||
children: React.ReactNode; |
||||
icon: Icons; |
||||
active?: boolean; |
||||
onClick?: () => void; |
||||
}) { |
||||
return ( |
||||
<div |
||||
onClick={props.onClick} |
||||
className={classNames( |
||||
"w-full px-3 py-2 flex items-center space-x-3 cursor-pointer rounded my-2", |
||||
props.active |
||||
? "bg-settings-sidebar-activeLink text-settings-sidebar-type-activated" |
||||
: null |
||||
)} |
||||
> |
||||
<Icon |
||||
className={classNames( |
||||
"text-2xl text-settings-sidebar-type-icon", |
||||
props.active ? "text-settings-sidebar-type-iconActivated" : null |
||||
)} |
||||
icon={props.icon} |
||||
/> |
||||
<span>{props.children}</span> |
||||
</div> |
||||
); |
||||
} |
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
export function SecondaryLabel(props: { children: React.ReactNode }) { |
||||
return <p className="text-type-text">{props.children}</p>; |
||||
} |
@ -0,0 +1,49 @@
@@ -0,0 +1,49 @@
|
||||
import { useAsyncFn } from "react-use"; |
||||
|
||||
import { deleteUser } from "@/backend/accounts/user"; |
||||
import { Button } from "@/components/Button"; |
||||
import { SolidSettingsCard } from "@/components/layout/SettingsCard"; |
||||
import { Heading2, Heading3 } from "@/components/utils/Text"; |
||||
import { useAuthData } from "@/hooks/auth/useAuthData"; |
||||
import { useBackendUrl } from "@/hooks/auth/useBackendUrl"; |
||||
import { useAuthStore } from "@/stores/auth"; |
||||
|
||||
export function AccountActionsPart() { |
||||
const url = useBackendUrl(); |
||||
const account = useAuthStore((s) => s.account); |
||||
const { logout } = useAuthData(); |
||||
const [deleteResult, deleteExec] = useAsyncFn(async () => { |
||||
if (!account) return; |
||||
await deleteUser(url, account); |
||||
logout(); |
||||
}, [logout, account, url]); |
||||
|
||||
if (!account) return null; |
||||
|
||||
return ( |
||||
<div> |
||||
<Heading2 border>Actions</Heading2> |
||||
<SolidSettingsCard |
||||
paddingClass="px-6 py-12" |
||||
className="grid grid-cols-2 gap-12" |
||||
> |
||||
<div> |
||||
<Heading3>Delete account</Heading3> |
||||
<p className="text-type-text"> |
||||
This action is irreversible. All data will be deleted and nothing |
||||
can be recovered. |
||||
</p> |
||||
</div> |
||||
<div className="flex justify-end items-center"> |
||||
<Button |
||||
theme="danger" |
||||
onClick={deleteExec} |
||||
loading={deleteResult.loading} |
||||
> |
||||
Delete account |
||||
</Button> |
||||
</div> |
||||
</SolidSettingsCard> |
||||
</div> |
||||
); |
||||
} |
@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
|
||||
import { SettingsCard } from "@/components/layout/SettingsCard"; |
||||
|
||||
export function AccountEditPart() { |
||||
return ( |
||||
<SettingsCard className="!mt-8"> |
||||
<p>Account editing will go here</p> |
||||
</SettingsCard> |
||||
); |
||||
} |
@ -0,0 +1,86 @@
@@ -0,0 +1,86 @@
|
||||
import { useAsyncFn } from "react-use"; |
||||
|
||||
import { SessionResponse } from "@/backend/accounts/auth"; |
||||
import { base64ToBuffer, decryptData } from "@/backend/accounts/crypto"; |
||||
import { removeSession } from "@/backend/accounts/sessions"; |
||||
import { Button } from "@/components/Button"; |
||||
import { Loading } from "@/components/layout/Loading"; |
||||
import { SettingsCard } from "@/components/layout/SettingsCard"; |
||||
import { SecondaryLabel } from "@/components/text/SecondaryLabel"; |
||||
import { Heading2 } from "@/components/utils/Text"; |
||||
import { useBackendUrl } from "@/hooks/auth/useBackendUrl"; |
||||
import { useAuthStore } from "@/stores/auth"; |
||||
|
||||
export function Device(props: { |
||||
name: string; |
||||
id: string; |
||||
isCurrent?: boolean; |
||||
onRemove?: () => void; |
||||
}) { |
||||
const url = useBackendUrl(); |
||||
const token = useAuthStore((s) => s.account?.token); |
||||
const [result, exec] = useAsyncFn(async () => { |
||||
if (!token) throw new Error("No token present"); |
||||
await removeSession(url, token, props.id); |
||||
props.onRemove?.(); |
||||
}, [url, token, props.id]); |
||||
|
||||
return ( |
||||
<SettingsCard |
||||
className="flex justify-between items-center" |
||||
paddingClass="px-6 py-4" |
||||
> |
||||
<div className="font-medium"> |
||||
<SecondaryLabel>Device name</SecondaryLabel> |
||||
<p className="text-white">{props.name}</p> |
||||
</div> |
||||
{!props.isCurrent ? ( |
||||
<Button theme="danger" loading={result.loading} onClick={exec}> |
||||
Remove |
||||
</Button> |
||||
) : null} |
||||
</SettingsCard> |
||||
); |
||||
} |
||||
|
||||
export function DeviceListPart(props: { |
||||
loading?: boolean; |
||||
error?: boolean; |
||||
sessions: SessionResponse[]; |
||||
onChange?: () => void; |
||||
}) { |
||||
const seed = useAuthStore((s) => s.account?.seed); |
||||
const currentSessionId = useAuthStore((s) => s.account?.sessionId); |
||||
if (!seed) return null; |
||||
|
||||
return ( |
||||
<div> |
||||
<Heading2 border className="mt-0 mb-9"> |
||||
Devices |
||||
</Heading2> |
||||
{props.error ? ( |
||||
<p>Failed to load sessions</p> |
||||
) : props.loading ? ( |
||||
<Loading /> |
||||
) : ( |
||||
<div className="space-y-5"> |
||||
{props.sessions.map((session) => { |
||||
const decryptedName = decryptData( |
||||
session.device, |
||||
base64ToBuffer(seed) |
||||
); |
||||
return ( |
||||
<Device |
||||
name={decryptedName} |
||||
id={session.id} |
||||
key={session.id} |
||||
isCurrent={session.id === currentSessionId} |
||||
onRemove={props.onChange} |
||||
/> |
||||
); |
||||
})} |
||||
</div> |
||||
)} |
||||
</div> |
||||
); |
||||
} |
@ -0,0 +1,31 @@
@@ -0,0 +1,31 @@
|
||||
import { useHistory } from "react-router-dom"; |
||||
|
||||
import { Button } from "@/components/Button"; |
||||
import { SolidSettingsCard } from "@/components/layout/SettingsCard"; |
||||
import { Heading3 } from "@/components/utils/Text"; |
||||
|
||||
export function RegisterCalloutPart() { |
||||
const history = useHistory(); |
||||
|
||||
return ( |
||||
<div> |
||||
<SolidSettingsCard |
||||
paddingClass="px-6 py-12" |
||||
className="grid grid-cols-2 gap-12" |
||||
> |
||||
<div> |
||||
<Heading3>Sync to the cloud</Heading3> |
||||
<p className="text-type-text"> |
||||
Instantly share your watch progress between devices and keep them |
||||
synced. |
||||
</p> |
||||
</div> |
||||
<div className="flex justify-end items-center"> |
||||
<Button theme="purple" onClick={() => history.push("/register")}> |
||||
Get started |
||||
</Button> |
||||
</div> |
||||
</SolidSettingsCard> |
||||
</div> |
||||
); |
||||
} |
@ -0,0 +1,42 @@
@@ -0,0 +1,42 @@
|
||||
import Sticky from "react-stickynode"; |
||||
|
||||
import { Icons } from "@/components/Icon"; |
||||
import { SidebarLink, SidebarSection } from "@/components/layout/Sidebar"; |
||||
import { Divider } from "@/components/utils/Divider"; |
||||
import { conf } from "@/setup/config"; |
||||
|
||||
export function SidebarPart() { |
||||
// eslint-disable-next-line no-restricted-globals
|
||||
const hostname = location.hostname; |
||||
const rem = 16; |
||||
|
||||
return ( |
||||
<div> |
||||
<Sticky |
||||
enabled |
||||
top={10 * rem} // 10rem
|
||||
className="text-settings-sidebar-type-inactive" |
||||
> |
||||
<SidebarSection title="Settings"> |
||||
<SidebarLink icon={Icons.WAND}>A war in my name!</SidebarLink> |
||||
<SidebarLink active icon={Icons.COMPRESS}> |
||||
TANSTAAFL |
||||
</SidebarLink> |
||||
<SidebarLink icon={Icons.AIRPLAY}>We all float down here</SidebarLink> |
||||
<SidebarLink icon={Icons.BOOKMARK}>My skin is not my own</SidebarLink> |
||||
</SidebarSection> |
||||
<Divider /> |
||||
<SidebarSection title="App information"> |
||||
<div className="flex justify-between items-center space-x-3"> |
||||
<span>Version</span> |
||||
<span>{conf().APP_VERSION}</span> |
||||
</div> |
||||
<div className="flex justify-between items-center space-x-3"> |
||||
<span>Domain</span> |
||||
<span className="text-right">{hostname}</span> |
||||
</div> |
||||
</SidebarSection> |
||||
</Sticky> |
||||
</div> |
||||
); |
||||
} |
@ -0,0 +1,141 @@
@@ -0,0 +1,141 @@
|
||||
import classNames from "classnames"; |
||||
|
||||
import { Icon, Icons } from "@/components/Icon"; |
||||
import { Heading2 } from "@/components/utils/Text"; |
||||
|
||||
const availableThemes = [ |
||||
{ |
||||
id: "blue", |
||||
name: "Blue", |
||||
}, |
||||
{ |
||||
id: "teal", |
||||
name: "Teal", |
||||
}, |
||||
{ |
||||
id: "red", |
||||
name: "Red", |
||||
}, |
||||
{ |
||||
id: "gray", |
||||
name: "Gray", |
||||
}, |
||||
]; |
||||
|
||||
function ThemePreview(props: { |
||||
selector?: string; |
||||
active?: boolean; |
||||
name: string; |
||||
onClick?: () => void; |
||||
}) { |
||||
return ( |
||||
<div |
||||
className={classNames(props.selector, "cursor-pointer group")} |
||||
onClick={props.onClick} |
||||
> |
||||
{/* Little card thing */} |
||||
<div |
||||
className={classNames( |
||||
"w-full h-32 relative rounded-lg border bg-gradient-to-br from-themePreview-primary/20 to-themePreview-secondary/10 bg-clip-content transition-colors duration-150", |
||||
props.active |
||||
? "border-themePreview-primary" |
||||
: "border-transparent group-hover:border-white/20" |
||||
)} |
||||
> |
||||
{/* Dots */} |
||||
<div className="absolute top-2 left-2"> |
||||
<div className="h-5 w-5 bg-themePreview-primary rounded-full" /> |
||||
<div className="h-5 w-5 bg-themePreview-secondary rounded-full -mt-2" /> |
||||
</div> |
||||
{/* Active check */} |
||||
<Icon |
||||
icon={Icons.CHECKMARK} |
||||
className={classNames( |
||||
"absolute top-3 right-3 text-xs text-white transition-opacity duration-150", |
||||
props.active ? "opacity-100" : "opacity-0" |
||||
)} |
||||
/> |
||||
{/* Mini movie-web. So Kawaiiiii! */} |
||||
{/* ^ can we keep this comment in forever please? - Jip */} |
||||
<div className="absolute bottom-0 left-1/2 transform -translate-x-1/2 w-3/5 h-4/5 rounded-t-lg -mb-px bg-background-main overflow-hidden"> |
||||
<div className="relative w-full h-full"> |
||||
{/* Background color */} |
||||
<div className="bg-themePreview-primary/50 w-[130%] h-10 absolute left-1/2 -top-5 blur-xl transform -translate-x-1/2 rounded-[100%]" /> |
||||
{/* Navbar */} |
||||
<div className="p-2 flex justify-between items-center"> |
||||
<div className="flex space-x-1"> |
||||
<div className="bg-themePreview-ghost bg-opacity-10 w-4 h-2 rounded-full" /> |
||||
<div className="bg-themePreview-ghost bg-opacity-10 w-2 h-2 rounded-full" /> |
||||
<div className="bg-themePreview-ghost bg-opacity-10 w-2 h-2 rounded-full" /> |
||||
</div> |
||||
<div className="bg-themePreview-ghost bg-opacity-10 w-2 h-2 rounded-full" /> |
||||
</div> |
||||
{/* Hero */} |
||||
<div className="mt-1 flex items-center flex-col gap-1"> |
||||
{/* Title and subtitle */} |
||||
<div className="bg-themePreview-ghost bg-opacity-20 w-8 h-0.5 rounded-full" /> |
||||
<div className="bg-themePreview-ghost bg-opacity-20 w-6 h-0.5 rounded-full" /> |
||||
{/* Search bar */} |
||||
<div className="bg-themePreview-ghost bg-opacity-10 w-16 h-2 mt-1 rounded-full" /> |
||||
</div> |
||||
{/* Media grid */} |
||||
<div className="mt-5 px-3"> |
||||
{/* Title */} |
||||
<div className="flex gap-1 items-center"> |
||||
<div className="bg-themePreview-ghost bg-opacity-20 w-2 h-2 rounded-full" /> |
||||
<div className="bg-themePreview-ghost bg-opacity-20 w-8 h-0.5 rounded-full" /> |
||||
</div> |
||||
{/* Blocks */} |
||||
<div className="flex w-full gap-1 mt-1"> |
||||
<div className="bg-themePreview-ghost bg-opacity-10 w-full h-20 rounded" /> |
||||
<div className="bg-themePreview-ghost bg-opacity-10 w-full h-20 rounded" /> |
||||
<div className="bg-themePreview-ghost bg-opacity-10 w-full h-20 rounded" /> |
||||
<div className="bg-themePreview-ghost bg-opacity-10 w-full h-20 rounded" /> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
<div className="mt-2 flex justify-between items-center"> |
||||
<span className="font-medium text-white">{props.name}</span> |
||||
<span |
||||
className={classNames( |
||||
"inline-block px-3 text-sm transition-opacity duration-150 rounded-full bg-[#27182F] text-white", |
||||
props.active ? "opacity-100" : "opacity-0 pointer-events-none" |
||||
)} |
||||
> |
||||
Active |
||||
</span> |
||||
</div> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
export function ThemePart(props: { |
||||
active: string | null; |
||||
setTheme: (theme: string | null) => void; |
||||
}) { |
||||
return ( |
||||
<div> |
||||
<Heading2 border>Themes</Heading2> |
||||
<div className="grid grid-cols-[repeat(auto-fill,minmax(200px,1fr))] gap-6 max-w-[700px]"> |
||||
{/* default theme */} |
||||
<ThemePreview |
||||
name="Default" |
||||
selector="theme-default" |
||||
active={props.active === null} |
||||
onClick={() => props.setTheme(null)} |
||||
/> |
||||
{availableThemes.map((v) => ( |
||||
<ThemePreview |
||||
selector={`theme-${v.id}`} |
||||
active={props.active === v.id} |
||||
name={v.name} |
||||
key={v.id} |
||||
onClick={() => props.setTheme(v.id)} |
||||
/> |
||||
))} |
||||
</div> |
||||
</div> |
||||
); |
||||
} |
@ -0,0 +1,24 @@
@@ -0,0 +1,24 @@
|
||||
import { create } from "zustand"; |
||||
import { persist } from "zustand/middleware"; |
||||
import { immer } from "zustand/middleware/immer"; |
||||
|
||||
export interface ThemeStore { |
||||
theme: string | null; |
||||
setTheme(v: string | null): void; |
||||
} |
||||
|
||||
export const useThemeStore = create( |
||||
persist( |
||||
immer<ThemeStore>((set) => ({ |
||||
theme: null, |
||||
setTheme(v) { |
||||
set((s) => { |
||||
s.theme = v; |
||||
}); |
||||
}, |
||||
})), |
||||
{ |
||||
name: "__MW::theme", |
||||
} |
||||
) |
||||
); |
@ -1,236 +0,0 @@
@@ -1,236 +0,0 @@
|
||||
const themer = require("tailwindcss-themer"); |
||||
|
||||
/** @type {import('tailwindcss').Config} */ |
||||
module.exports = { |
||||
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], |
||||
theme: { |
||||
extend: { |
||||
/* colors */ |
||||
colors: { |
||||
"bink-100": "#432449", |
||||
"bink-200": "#412B57", |
||||
"bink-300": "#533670", |
||||
"bink-400": "#714C97", |
||||
"bink-500": "#8D66B5", |
||||
"bink-600": "#A87FD1", |
||||
"bink-700": "#CD97D6", |
||||
"denim-100": "#120F1D", |
||||
"denim-200": "#191526", |
||||
"denim-300": "#211D30", |
||||
"denim-400": "#2B263D", |
||||
"denim-500": "#38334A", |
||||
"denim-600": "#504B64", |
||||
"denim-700": "#7A758F", |
||||
"ash-600": "#817998", |
||||
"ash-500": "#9C93B5", |
||||
"ash-400": "#3D394D", |
||||
"ash-300": "#2C293A", |
||||
"ash-200": "#2B2836", |
||||
"ash-100": "#1E1C26" |
||||
}, |
||||
|
||||
/* fonts */ |
||||
fontFamily: { |
||||
"open-sans": "'Open Sans'" |
||||
}, |
||||
|
||||
/* animations */ |
||||
keyframes: { |
||||
"loading-pin": { |
||||
"0%, 40%, 100%": { height: "0.5em", "background-color": "#282336" }, |
||||
"20%": { height: "1em", "background-color": "white" } |
||||
} |
||||
}, |
||||
animation: { "loading-pin": "loading-pin 1.8s ease-in-out infinite" } |
||||
} |
||||
}, |
||||
plugins: [ |
||||
require("tailwind-scrollbar"), |
||||
themer({ |
||||
defaultTheme: { |
||||
extend: { |
||||
colors: { |
||||
// Branding
|
||||
pill: { |
||||
background: "#1C1C36" |
||||
}, |
||||
|
||||
// meta data for the theme itself
|
||||
global: { |
||||
accentA: "#505DBD", |
||||
accentB: "#3440A1" |
||||
}, |
||||
|
||||
// light bar
|
||||
lightBar: { |
||||
light: "#2A2A71" |
||||
}, |
||||
|
||||
// Buttons
|
||||
buttons: { |
||||
toggle: "#8D44D6", |
||||
toggleDisabled: "#202836", |
||||
danger: "#792131", |
||||
dangerHover: "#8a293b", |
||||
|
||||
secondary: "#161F25", |
||||
secondaryText: "#8EA3B0", |
||||
secondaryHover: "#1B262E", |
||||
primary: "#fff", |
||||
primaryText: "#000", |
||||
primaryHover: "#dedede", |
||||
purple: "#6b298a", |
||||
purpleHover: "#7f35a1", |
||||
cancel: "#252533", |
||||
cancelHover: "#3C3C4A" |
||||
}, |
||||
|
||||
// only used for body colors/textures
|
||||
background: { |
||||
main: "#0A0A10", |
||||
accentA: "#6E3B80", |
||||
accentB: "#1F1F50" |
||||
}, |
||||
|
||||
// typography
|
||||
type: { |
||||
emphasis: "#FFFFFF", |
||||
text: "#73739D", |
||||
dimmed: "#926CAD", |
||||
divider: "#262632", |
||||
secondary: "#64647B" |
||||
}, |
||||
|
||||
// search bar
|
||||
search: { |
||||
background: "#1E1E33", |
||||
focused: "#24243C", |
||||
placeholder: "#4A4A71", |
||||
icon: "#545476", |
||||
text: "#FFFFFF" |
||||
}, |
||||
|
||||
// media cards
|
||||
mediaCard: { |
||||
hoverBackground: "#161622", |
||||
hoverAccent: "#4D79A8", |
||||
hoverShadow: "#0A0A10", |
||||
shadow: "#161622", |
||||
barColor: "#4B4B63", |
||||
barFillColor: "#BA7FD6", |
||||
badge: "#151522", |
||||
badgeText: "#5F5F7A" |
||||
}, |
||||
|
||||
// Large card
|
||||
largeCard: { |
||||
background: "#171728", |
||||
icon: "#6741A5" |
||||
}, |
||||
|
||||
// Passphrase
|
||||
authentication: { |
||||
border: "#393954", |
||||
inputBg: "#171728", |
||||
wordBackground: "#171728", |
||||
copyText: "#58587A", |
||||
copyTextHover: "#8888AA", |
||||
errorText: "#DB3D62" |
||||
}, |
||||
|
||||
// Settings page
|
||||
settings: { |
||||
sidebar: { |
||||
activeLink: "#171728", |
||||
|
||||
type: { |
||||
secondary: "#4B395F", |
||||
inactive: "#8D68A9", |
||||
icon: "#926CAD", |
||||
iconActivated: "#6942A8", |
||||
activated: "#CBA1E8" |
||||
} |
||||
}, |
||||
|
||||
card: { |
||||
border: "#2A243E", |
||||
background: "#29243D", |
||||
altBackground: "#29243D" |
||||
} |
||||
}, |
||||
|
||||
utils: { |
||||
divider: "#353549" |
||||
}, |
||||
|
||||
// Error page
|
||||
errors: { |
||||
card: "#12121B", |
||||
border: "#252534", |
||||
|
||||
type: { |
||||
secondary: "#62627D" |
||||
} |
||||
}, |
||||
|
||||
// About page
|
||||
about: { |
||||
circle: "#262632", |
||||
circleText: "#9A9AC3" |
||||
}, |
||||
|
||||
progress: { |
||||
background: "#8787A8", |
||||
preloaded: "#8787A8", |
||||
filled: "#A75FC9" |
||||
}, |
||||
|
||||
// video player
|
||||
video: { |
||||
buttonBackground: "#444B5C", |
||||
|
||||
scraping: { |
||||
card: "#161620", |
||||
error: "#E44F4F", |
||||
success: "#40B44B", |
||||
loading: "#B759D8", |
||||
noresult: "#64647B" |
||||
}, |
||||
|
||||
audio: { |
||||
set: "#A75FC9" |
||||
}, |
||||
|
||||
context: { |
||||
background: "#0C1216", |
||||
light: "#4D79A8", |
||||
border: "#1d252b", |
||||
hoverColor: "#1E2A32", |
||||
buttonFocus: "#202836", |
||||
flagBg: "#202836", |
||||
inputBg: "#202836", |
||||
buttonOverInputHover: "#283040", |
||||
inputPlaceholder: "#374A56", |
||||
cardBorder: "#1B262E", |
||||
slider: "#8787A8", |
||||
sliderFilled: "#A75FC9", |
||||
error: "#E44F4F", |
||||
|
||||
buttons: { |
||||
list: "#161C26", |
||||
active: "#0D1317" |
||||
}, |
||||
|
||||
type: { |
||||
main: "#617A8A", |
||||
secondary: "#374A56", |
||||
accent: "#A570FA" |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
}) |
||||
] |
||||
}; |
@ -0,0 +1,66 @@
@@ -0,0 +1,66 @@
|
||||
import { allThemes, defaultTheme, safeThemeList } from "./themes"; |
||||
import type { Config } from "tailwindcss" |
||||
|
||||
const themer = require("tailwindcss-themer"); |
||||
|
||||
const config: Config = { |
||||
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], |
||||
safelist: safeThemeList, |
||||
theme: { |
||||
extend: { |
||||
// TODO remove old colors
|
||||
/* colors */ |
||||
colors: { |
||||
"bink-100": "#432449", |
||||
"bink-200": "#412B57", |
||||
"bink-300": "#533670", |
||||
"bink-400": "#714C97", |
||||
"bink-500": "#8D66B5", |
||||
"bink-600": "#A87FD1", |
||||
"bink-700": "#CD97D6", |
||||
"denim-100": "#120F1D", |
||||
"denim-200": "#191526", |
||||
"denim-300": "#211D30", |
||||
"denim-400": "#2B263D", |
||||
"denim-500": "#38334A", |
||||
"denim-600": "#504B64", |
||||
"denim-700": "#7A758F", |
||||
"ash-600": "#817998", |
||||
"ash-500": "#9C93B5", |
||||
"ash-400": "#3D394D", |
||||
"ash-300": "#2C293A", |
||||
"ash-200": "#2B2836", |
||||
"ash-100": "#1E1C26" |
||||
}, |
||||
|
||||
/* fonts */ |
||||
fontFamily: { |
||||
"open-sans": "'Open Sans'" |
||||
}, |
||||
|
||||
/* animations */ |
||||
keyframes: { |
||||
"loading-pin": { |
||||
"0%, 40%, 100%": { height: "0.5em", "background-color": "#282336" }, |
||||
"20%": { height: "1em", "background-color": "white" } |
||||
} |
||||
}, |
||||
animation: { "loading-pin": "loading-pin 1.8s ease-in-out infinite" } |
||||
} |
||||
}, |
||||
plugins: [ |
||||
require("tailwind-scrollbar"), |
||||
themer({ |
||||
defaultTheme: defaultTheme, |
||||
themes: [ |
||||
{ |
||||
name: "default", |
||||
selectors: [".theme-default"], |
||||
...defaultTheme, |
||||
}, |
||||
...allThemes] |
||||
}) |
||||
] |
||||
}; |
||||
|
||||
export default config; |
@ -0,0 +1,11 @@
@@ -0,0 +1,11 @@
|
||||
import teal from "./list/teal"; |
||||
import blue from "./list/blue"; |
||||
import red from "./list/red"; |
||||
import gray from "./list/gray"; |
||||
|
||||
export const allThemes = [ |
||||
teal, |
||||
blue, |
||||
gray, |
||||
red |
||||
] |
@ -0,0 +1,190 @@
@@ -0,0 +1,190 @@
|
||||
export const defaultTheme = { |
||||
extend: { |
||||
colors: { |
||||
themePreview: { |
||||
primary: "#505DBD", |
||||
secondary: "#73739D", |
||||
ghost: "white" |
||||
}, |
||||
|
||||
// Branding
|
||||
pill: { |
||||
background: "#1C1C36" |
||||
}, |
||||
|
||||
// meta data for the theme itself
|
||||
global: { |
||||
accentA: "#505DBD", |
||||
accentB: "#3440A1" |
||||
}, |
||||
|
||||
// light bar
|
||||
lightBar: { |
||||
light: "#2A2A71" |
||||
}, |
||||
|
||||
// Buttons
|
||||
buttons: { |
||||
toggle: "#8D44D6", |
||||
toggleDisabled: "#202836", |
||||
danger: "#792131", |
||||
dangerHover: "#8a293b", |
||||
|
||||
secondary: "#161F25", |
||||
secondaryText: "#8EA3B0", |
||||
secondaryHover: "#1B262E", |
||||
primary: "#fff", |
||||
primaryText: "#000", |
||||
primaryHover: "#dedede", |
||||
purple: "#6b298a", |
||||
purpleHover: "#7f35a1", |
||||
cancel: "#252533", |
||||
cancelHover: "#3C3C4A" |
||||
}, |
||||
|
||||
// only used for body colors/textures
|
||||
background: { |
||||
main: "#0A0A10", |
||||
accentA: "#6E3B80", |
||||
accentB: "#1F1F50" |
||||
}, |
||||
|
||||
// typography
|
||||
type: { |
||||
emphasis: "#FFFFFF", |
||||
text: "#73739D", |
||||
dimmed: "#926CAD", |
||||
divider: "#262632", |
||||
secondary: "#64647B" |
||||
}, |
||||
|
||||
// search bar
|
||||
search: { |
||||
background: "#1E1E33", |
||||
focused: "#24243C", |
||||
placeholder: "#4A4A71", |
||||
icon: "#545476", |
||||
text: "#FFFFFF" |
||||
}, |
||||
|
||||
// media cards
|
||||
mediaCard: { |
||||
hoverBackground: "#161622", |
||||
hoverAccent: "#4D79A8", |
||||
hoverShadow: "#0A0A10", |
||||
shadow: "#161622", |
||||
barColor: "#4B4B63", |
||||
barFillColor: "#BA7FD6", |
||||
badge: "#151522", |
||||
badgeText: "#5F5F7A" |
||||
}, |
||||
|
||||
// Large card
|
||||
largeCard: { |
||||
background: "#171728", |
||||
icon: "#6741A5" |
||||
}, |
||||
|
||||
// Passphrase
|
||||
authentication: { |
||||
border: "#393954", |
||||
inputBg: "#171728", |
||||
wordBackground: "#171728", |
||||
copyText: "#58587A", |
||||
copyTextHover: "#8888AA", |
||||
errorText: "#DB3D62" |
||||
}, |
||||
|
||||
// Settings page
|
||||
settings: { |
||||
sidebar: { |
||||
activeLink: "#171728", |
||||
|
||||
type: { |
||||
secondary: "#4B395F", |
||||
inactive: "#8D68A9", |
||||
icon: "#926CAD", |
||||
iconActivated: "#6942A8", |
||||
activated: "#CBA1E8" |
||||
} |
||||
}, |
||||
|
||||
card: { |
||||
border: "#2A243E", |
||||
background: "#29243D", |
||||
altBackground: "#29243D" |
||||
} |
||||
}, |
||||
|
||||
utils: { |
||||
divider: "#353549" |
||||
}, |
||||
|
||||
// Error page
|
||||
errors: { |
||||
card: "#12121B", |
||||
border: "#252534", |
||||
|
||||
type: { |
||||
secondary: "#62627D" |
||||
} |
||||
}, |
||||
|
||||
// About page
|
||||
about: { |
||||
circle: "#262632", |
||||
circleText: "#9A9AC3" |
||||
}, |
||||
|
||||
progress: { |
||||
background: "#8787A8", |
||||
preloaded: "#8787A8", |
||||
filled: "#A75FC9" |
||||
}, |
||||
|
||||
// video player
|
||||
video: { |
||||
buttonBackground: "#444B5C", |
||||
|
||||
scraping: { |
||||
card: "#161620", |
||||
error: "#E44F4F", |
||||
success: "#40B44B", |
||||
loading: "#B759D8", |
||||
noresult: "#64647B" |
||||
}, |
||||
|
||||
audio: { |
||||
set: "#A75FC9" |
||||
}, |
||||
|
||||
context: { |
||||
background: "#0C1216", |
||||
light: "#4D79A8", |
||||
border: "#1d252b", |
||||
hoverColor: "#1E2A32", |
||||
buttonFocus: "#202836", |
||||
flagBg: "#202836", |
||||
inputBg: "#202836", |
||||
buttonOverInputHover: "#283040", |
||||
inputPlaceholder: "#374A56", |
||||
cardBorder: "#1B262E", |
||||
slider: "#8787A8", |
||||
sliderFilled: "#A75FC9", |
||||
error: "#E44F4F", |
||||
|
||||
buttons: { |
||||
list: "#161C26", |
||||
active: "#0D1317" |
||||
}, |
||||
|
||||
type: { |
||||
main: "#617A8A", |
||||
secondary: "#374A56", |
||||
accent: "#A570FA" |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
|
||||
import { allThemes } from "./all"; |
||||
|
||||
export { defaultTheme } from "./default"; |
||||
export { allThemes } from "./all"; |
||||
|
||||
export const safeThemeList = allThemes |
||||
.flatMap(v=>v.selectors) |
||||
.filter(v=>v.startsWith(".")) |
||||
.map(v=>v.slice(1)); // remove dot from selector
|
@ -0,0 +1,19 @@
@@ -0,0 +1,19 @@
|
||||
import { createTheme } from "../types"; |
||||
|
||||
export default createTheme({ |
||||
name: "blue", |
||||
extend: { |
||||
colors: { |
||||
themePreview: { |
||||
primary: "#3A4FAA", |
||||
secondary: "#303487", |
||||
ghost: "white", |
||||
}, |
||||
|
||||
// light bar
|
||||
lightBar: { |
||||
light: "#3A4FAA", |
||||
}, |
||||
} |
||||
} |
||||
}) |
@ -0,0 +1,19 @@
@@ -0,0 +1,19 @@
|
||||
import { createTheme } from "../types"; |
||||
|
||||
export default createTheme({ |
||||
name: "gray", |
||||
extend: { |
||||
colors: { |
||||
themePreview: { |
||||
primary: "#343441", |
||||
secondary: "#0C0C16", |
||||
ghost: "white", |
||||
}, |
||||
|
||||
// light bar
|
||||
lightBar: { |
||||
light: "#343441" |
||||
}, |
||||
} |
||||
} |
||||
}) |
@ -0,0 +1,19 @@
@@ -0,0 +1,19 @@
|
||||
import { createTheme } from "../types"; |
||||
|
||||
export default createTheme({ |
||||
name: "red", |
||||
extend: { |
||||
colors: { |
||||
themePreview: { |
||||
primary: "#A8335E", |
||||
secondary: "#6A2441", |
||||
ghost: "white", |
||||
}, |
||||
|
||||
// light bar
|
||||
lightBar: { |
||||
light: "#A8335E" |
||||
}, |
||||
} |
||||
} |
||||
}) |
@ -0,0 +1,19 @@
@@ -0,0 +1,19 @@
|
||||
import { createTheme } from "../types"; |
||||
|
||||
export default createTheme({ |
||||
name: "teal", |
||||
extend: { |
||||
colors: { |
||||
themePreview: { |
||||
primary: "#469c51", |
||||
secondary: "#1a3d2b", |
||||
ghost: "white", |
||||
}, |
||||
|
||||
// light bar
|
||||
lightBar: { |
||||
light: "#469c51", |
||||
}, |
||||
} |
||||
} |
||||
}) |
@ -0,0 +1,15 @@
@@ -0,0 +1,15 @@
|
||||
import { DeepPartial } from "vite-plugin-checker/dist/esm/types"; |
||||
import { defaultTheme } from "./default"; |
||||
|
||||
export interface Theme { |
||||
name: string; |
||||
extend: DeepPartial<(typeof defaultTheme)["extend"]> |
||||
} |
||||
|
||||
export function createTheme(theme: Theme) { |
||||
return { |
||||
name: theme.name, |
||||
selectors: [`.theme-${theme.name}`], |
||||
extend: theme.extend |
||||
} |
||||
} |
Loading…
Reference in new issue