add calendar view and light mode support

This commit is contained in:
Gregor Vostrak
2025-11-12 18:27:21 +01:00
parent 9d7e3ac4ef
commit bd5e7d3131
21 changed files with 499 additions and 236 deletions
+26 -12
View File
@@ -3,6 +3,8 @@ import { resolve } from 'path'
import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
import vue from '@vitejs/plugin-vue'
const isProduction = process.env.NODE_ENV === 'production'
export default defineConfig({
main: {
build: {
@@ -10,20 +12,28 @@ export default defineConfig({
},
plugins: [
externalizeDepsPlugin(),
sentryVitePlugin({
org: 'solidtime',
project: 'desktop',
}),
...(isProduction
? [
sentryVitePlugin({
org: 'solidtime',
project: 'desktop',
}),
]
: []),
],
},
preload: {
plugins: [
externalizeDepsPlugin(),
sentryVitePlugin({
org: 'solidtime',
project: 'desktop',
}),
...(isProduction
? [
sentryVitePlugin({
org: 'solidtime',
project: 'desktop',
}),
]
: []),
],
build: {
sourcemap: true,
@@ -54,10 +64,14 @@ export default defineConfig({
},
plugins: [
vue(),
sentryVitePlugin({
org: 'solidtime',
project: 'desktop',
}),
...(isProduction
? [
sentryVitePlugin({
org: 'solidtime',
project: 'desktop',
}),
]
: []),
],
},
})
+1 -1
View File
@@ -5,7 +5,7 @@ export function initializeMainWindow(icon: string) {
const mainWindow = new BrowserWindow({
width: 800,
minWidth: 400,
trafficLightPosition: { x: 15, y: 15 },
trafficLightPosition: { x: 10, y: 10 },
minHeight: 400,
height: 800,
show: false,
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
+6
View File
@@ -1,11 +1,17 @@
<script setup lang="ts">
import { onMounted } from 'vue'
import { isLoggedIn } from './utils/oauth.ts'
import { showMainWindow } from './utils/window.ts'
import MiniControls from './components/MiniControls.vue'
import { useTheme } from './utils/theme.ts'
function focusMainWindow() {
showMainWindow()
}
onMounted(() => {
useTheme()
})
</script>
<template>
@@ -8,7 +8,7 @@ import {
TimeTrackerRunningInDifferentOrganizationOverlay,
TimeEntryMassActionRow,
TimeEntryCreateModal,
MoreOptionsDropdown,
TimeTrackerMoreOptionsDropdown,
} from '@solidtime/ui'
import {
emptyTimeEntry,
@@ -37,7 +37,7 @@ import { getAllTags, useTagCreateMutation } from '../utils/tags.ts'
import { LoadingSpinner } from '@solidtime/ui'
import { useLiveTimer } from '../utils/liveTimer.ts'
import { ClockIcon, PlusIcon } from '@heroicons/vue/20/solid'
import { ClockIcon } from '@heroicons/vue/20/solid'
import { CardTitle } from '@solidtime/ui'
import { useStorage } from '@vueuse/core'
import { currentMembershipId, useMyMemberships } from '../utils/myMemberships.ts'
@@ -136,7 +136,6 @@ function createTimeEntry(timeEntry: Omit<CreateTimeEntryBody, 'member_id'>) {
const updatedTimeEntry = {
...timeEntry,
member_id: currentMembershipId.value,
end: null,
} as CreateTimeEntryBody
timeEntryCreate.mutate(updatedTimeEntry)
}
@@ -213,6 +212,15 @@ async function stopTimer() {
timeEntryStop.mutate({ ...stoppedTimeEntry, end: dayjs().utc().format() })
}
function discardTimer() {
// If there's an active timer with an ID, delete it from the backend
if (currentTimeEntry.value?.id && currentTimeEntry.value.id !== '') {
timeEntryDelete.mutate(currentTimeEntry.value)
}
currentTimeEntry.value = { ...emptyTimeEntry }
stopLiveTimer()
}
onMounted(async () => {
await listenForBackendEvent('startTimer', () => {
if (lastTimeEntry.value) {
@@ -320,9 +328,8 @@ const showManualTimeEntryModal = ref(false)
<div
v-if="timeEntries && projects && tasks && tags && clients"
class="flex flex-col h-full">
<div class="flex">
<div
class="pl-4 pb-4 pt-2 border-b border-border-primary bg-primary z-10 w-full top-0 left-0">
<div class="flex bg-background">
<div class="pl-4 pb-4 pt-2 border-b border-border-primary z-10 w-full top-0 left-0">
<CardTitle title="Time Tracker" :icon="ClockIcon as Component"></CardTitle>
<div class="relative">
<TimeTrackerRunningInDifferentOrganizationOverlay
@@ -347,6 +354,7 @@ const showManualTimeEntryModal = ref(false)
:createTag
:isActive
:currency
:timeEntries="timeEntries"
@start-live-timer="startLiveTimer"
@stop-live-timer="stopLiveTimer"
@start-timer="startTimer"
@@ -354,16 +362,11 @@ const showManualTimeEntryModal = ref(false)
@update-time-entry="updateCurrentTimeEntry"></TimeTrackerControls>
</div>
</div>
<div class="flex justify-center items-center pt-8 group pr-4">
<MoreOptionsDropdown label="More Time Entry Options">
<button
aria-label="Create Manual time entry"
class="flex items-center space-x-3 rounded w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"
@click="showManualTimeEntryModal = true">
<PlusIcon class="w-5 text-icon-active"></PlusIcon>
<span>Create Manual Time Entry</span>
</button>
</MoreOptionsDropdown>
<div class="flex justify-center items-center pt-5 group pr-4">
<TimeTrackerMoreOptionsDropdown
:has-active-timer="isActive"
@manual-entry="showManualTimeEntryModal = true"
@discard="discardTimer" />
<TimeEntryCreateModal
v-model:show="showManualTimeEntryModal"
:enableEstimatedTime="false"
@@ -412,6 +415,8 @@ const showManualTimeEntryModal = ref(false)
:createProject
:createClient
:currency="currency"
:enableEstimatedTime="false"
:canCreateProject="canCreateProjects"
:updateTimeEntry="
(arg: TimeEntry) => {
timeEntryUpdate.mutate(arg)
@@ -0,0 +1,61 @@
<script setup lang="ts">
import { ClockIcon, CalendarIcon, ChartPieIcon } from '@heroicons/vue/24/outline'
import { useRouter, useRoute } from 'vue-router'
import { computed } from 'vue'
import { Button, TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from '@solidtime/ui'
const router = useRouter()
const route = useRoute()
const navItems = [
{
name: 'Time',
path: '/time',
icon: ClockIcon,
},
{
name: 'Calendar',
path: '/calendar',
icon: CalendarIcon,
},
{
name: 'Statistics',
path: '/statistics',
icon: ChartPieIcon,
},
]
const currentPath = computed(() => route?.path || '/')
function isActive(path: string) {
return currentPath.value === path
}
function navigateTo(path: string) {
router.push(path)
}
</script>
<template>
<TooltipProvider>
<div
class="w-14 bg-background border-r border-border-primary flex flex-col items-center py-3">
<Tooltip v-for="item in navItems" :key="item.path">
<TooltipTrigger as-child>
<Button
variant="ghost"
@click="navigateTo(item.path)"
:class="[
'transition-colors text-text-tertiary w-11 h-11 [&_svg]:size-5',
isActive(item.path) && 'bg-secondary text-white shadow-xs bg-white/5',
]">
<component :is="item.icon" class="w-16 h-16"></component>
</Button>
</TooltipTrigger>
<TooltipContent side="right">
<p>{{ item.name }}</p>
</TooltipContent>
</Tooltip>
</div>
</TooltipProvider>
</template>
+17 -11
View File
@@ -1,25 +1,30 @@
import { createApp } from 'vue'
import App from './App.vue'
import './style.css'
import '@solidtime/ui/style.css'
import { VueQueryPlugin, type VueQueryPluginOptions } from '@tanstack/vue-query'
import router from './router'
const app = createApp(App)
import * as Sentry from '@sentry/electron/renderer'
Sentry.init({
integrations: [Sentry.browserTracingIntegration(), Sentry.replayIntegration()],
// Only initialize Sentry in production
if (import.meta.env.PROD) {
Sentry.init({
integrations: [Sentry.browserTracingIntegration(), Sentry.replayIntegration()],
// Set tracesSampleRate to 1.0 to capture 100%
// of transactions for performance monitoring.
// We recommend adjusting this value in production
tracesSampleRate: 0.1,
// Set tracesSampleRate to 1.0 to capture 100%
// of transactions for performance monitoring.
// We recommend adjusting this value in production
tracesSampleRate: 0.1,
// Capture Replay for 10% of all sessions,
// plus for 100% of sessions with an error
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,
})
// Capture Replay for 10% of all sessions,
// plus for 100% of sessions with an error
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,
})
}
window.addEventListener('keypress', (event) => {
if (event.key === 'Escape') {
@@ -57,5 +62,6 @@ const vueQueryOptions: VueQueryPluginOptions = {
},
}
app.use(router)
app.use(VueQueryPlugin, vueQueryOptions)
app.mount('#app')
+235
View File
@@ -0,0 +1,235 @@
<script setup lang="ts">
import { useQuery, useQueryClient } from '@tanstack/vue-query'
import { computed, ref } from 'vue'
import { TimeEntryCalendar, LoadingSpinner } from '@solidtime/ui'
import { getAllProjects, useProjectCreateMutation } from '../utils/projects.ts'
import { getAllTasks } from '../utils/tasks.ts'
import { getAllTags, useTagCreateMutation } from '../utils/tags.ts'
import { getAllClients, useClientCreateMutation } from '../utils/clients.ts'
import { currentMembershipId, useMyMemberships } from '../utils/myMemberships.ts'
import { dayjs } from '../utils/dayjs.ts'
import { apiClient } from '../utils/api'
import type {
CreateClientBody,
CreateProjectBody,
CreateTagBody,
Project,
Client,
Tag,
TimeEntry,
TimeEntryResponse,
} from '@solidtime/api'
const { currentOrganizationId, currentMembership } = useMyMemberships()
const currentOrganizationLoaded = computed(() => !!currentOrganizationId.value)
const calendarStart = ref<Date | undefined>(undefined)
const calendarEnd = ref<Date | undefined>(undefined)
const enableCalendarQuery = computed(() => {
return !!currentOrganizationId.value && !!calendarStart.value && !!calendarEnd.value
})
// Calculate expanded date range to include previous and next periods
const expandedDateRange = computed(() => {
if (!calendarStart.value || !calendarEnd.value) {
return { start: null, end: null }
}
const duration = dayjs(calendarEnd.value).diff(dayjs(calendarStart.value), 'milliseconds')
// Calculate previous period
const previousStart = dayjs(calendarStart.value).subtract(duration, 'milliseconds')
// Calculate next period
const nextEnd = dayjs(calendarEnd.value).add(duration, 'milliseconds')
// Format as UTC
const formattedStart = previousStart.utc().format()
const formattedEnd = nextEnd.utc().format()
return {
start: formattedStart,
end: formattedEnd,
}
})
const { data: timeEntryResponse, isLoading: timeEntriesLoading } = useQuery<TimeEntryResponse>({
queryKey: computed(() => [
'timeEntry',
'calendar',
{
start: expandedDateRange.value.start,
end: expandedDateRange.value.end,
organization: currentOrganizationId.value,
},
]),
enabled: enableCalendarQuery,
placeholderData: (previousData) => previousData,
queryFn: async () => {
if (!currentOrganizationId.value) {
throw new Error('No organization selected')
}
const result = await apiClient.value.getTimeEntries({
params: {
organization: currentOrganizationId.value,
},
queries: {
start: expandedDateRange.value.start!,
end: expandedDateRange.value.end!,
member_id: currentMembershipId.value || undefined,
},
})
return result
},
})
const currentTimeEntries = computed(() => {
return timeEntryResponse?.value?.data || []
})
const { data: projectsResponse } = useQuery({
queryKey: ['projects', currentOrganizationId],
queryFn: () => getAllProjects(currentOrganizationId.value),
enabled: currentOrganizationLoaded,
})
const projects = computed(() => projectsResponse.value?.data ?? [])
const { data: tasksResponse } = useQuery({
queryKey: ['tasks', currentOrganizationId],
queryFn: () => getAllTasks(currentOrganizationId.value),
enabled: currentOrganizationLoaded,
})
const tasks = computed(() => tasksResponse.value?.data ?? [])
const { data: tagsResponse } = useQuery({
queryKey: ['tags', currentOrganizationId],
queryFn: () => getAllTags(currentOrganizationId.value),
enabled: currentOrganizationLoaded,
})
const tags = computed(() => tagsResponse.value?.data ?? [])
const { data: clientsResponse } = useQuery({
queryKey: ['clients', currentOrganizationId],
queryFn: () => getAllClients(currentOrganizationId.value),
enabled: currentOrganizationLoaded,
})
const clients = computed(() => clientsResponse.value?.data ?? [])
const queryClient = useQueryClient()
async function createTimeEntry(entry: Omit<TimeEntry, 'id' | 'organization_id' | 'user_id'>) {
if (!currentOrganizationId.value || !currentMembershipId.value) {
throw new Error('No organization or member selected')
}
await apiClient.value.createTimeEntry(
{
member_id: currentMembershipId.value,
project_id: (entry.project_id as string | null) ?? null,
task_id: (entry.task_id as string | null) ?? null,
start: entry.start as string,
end: (entry.end as string | null) ?? null,
description: (entry.description as string | null) ?? null,
billable: entry.billable as boolean,
tags: (entry.tags as string[] | null) ?? null,
},
{
params: { organization: currentOrganizationId.value },
}
)
queryClient.invalidateQueries({ queryKey: ['timeEntry', 'calendar'] })
}
async function updateTimeEntry(entry: TimeEntry) {
if (!currentOrganizationId.value) {
throw new Error('No organization selected')
}
await apiClient.value.updateTimeEntry(
{
member_id: (entry.member_id as string) ?? undefined,
project_id: (entry.project_id as string | null) ?? null,
task_id: (entry.task_id as string | null) ?? null,
start: entry.start as string,
end: (entry.end as string | null) ?? null,
description: (entry.description as string | null) ?? null,
billable: entry.billable as boolean,
tags: (entry.tags as string[] | null) ?? null,
},
{
params: {
organization: currentOrganizationId.value,
timeEntry: entry.id as string,
},
}
)
queryClient.invalidateQueries({ queryKey: ['timeEntry', 'calendar'] })
}
async function deleteTimeEntry(timeEntryId: string) {
if (!currentOrganizationId.value) {
throw new Error('No organization selected')
}
await apiClient.value.deleteTimeEntry(undefined, {
params: {
organization: currentOrganizationId.value,
timeEntry: timeEntryId,
},
})
queryClient.invalidateQueries({ queryKey: ['timeEntry', 'calendar'] })
}
const projectCreateMutation = useProjectCreateMutation()
async function createProject(project: CreateProjectBody): Promise<Project | undefined> {
const result = await projectCreateMutation.mutateAsync(project)
return result.data
}
const clientCreateMutation = useClientCreateMutation()
async function createClient(body: CreateClientBody): Promise<Client | undefined> {
const result = await clientCreateMutation.mutateAsync(body)
return result.data
}
const tagCreateMutation = useTagCreateMutation()
async function createTag(name: string): Promise<Tag | undefined> {
const result = await tagCreateMutation.mutateAsync({ name } as CreateTagBody)
return result.data
}
function onDatesChange({ start, end }: { start: Date; end: Date }) {
calendarStart.value = start
calendarEnd.value = end
}
function onRefresh() {
queryClient.invalidateQueries({
queryKey: ['timeEntry', 'calendar'],
})
}
</script>
<template>
<div class="flex-1 h-full">
<div v-if="!currentOrganizationLoaded" class="flex items-center justify-center h-full">
<LoadingSpinner />
</div>
<TimeEntryCalendar
v-else
:time-entries="currentTimeEntries"
:projects="projects"
:tasks="tasks"
:clients="clients"
:tags="tags"
:loading="timeEntriesLoading"
:enable-estimated-time="false"
:currency="currentMembership?.organization?.currency || 'USD'"
:can-create-project="true"
:create-time-entry="createTimeEntry"
:update-time-entry="updateTimeEntry"
:delete-time-entry="deleteTimeEntry"
:create-client="createClient"
:create-project="createProject"
:create-tag="createTag"
@dates-change="onDatesChange"
@refresh="onRefresh" />
</div>
</template>
+9
View File
@@ -0,0 +1,9 @@
<script setup lang="ts">
import MainTimeEntryTable from '../components/MainTimeEntryTable.vue'
</script>
<template>
<div class="flex-1">
<MainTimeEntryTable />
</div>
</template>
+27
View File
@@ -0,0 +1,27 @@
import { createRouter, createWebHashHistory } from 'vue-router'
import TimePage from '../pages/TimePage.vue'
import CalendarPage from '../pages/CalendarPage.vue'
const routes = [
{
path: '/',
redirect: '/time',
},
{
path: '/time',
name: 'time',
component: TimePage,
},
{
path: '/calendar',
name: 'calendar',
component: CalendarPage,
},
]
const router = createRouter({
history: createWebHashHistory(),
routes,
})
export default router
+8 -105
View File
@@ -1,108 +1,11 @@
/* this is a copy from the solidtime config */
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--color-bg-primary: #0f1011;
--color-bg-secondary: #17181a;
--color-bg-tertiary: #2a2c32;
--color-bg-quaternary: #141518;
--color-bg-background: #080808;
--color-text-primary: #ffffff;
--color-text-secondary: #e3e4e6;
--color-text-tertiary: #969799;
--color-text-quaternary: #595a5c;
--color-border-primary: #191b1f;
--color-border-secondary: #23252a;
--color-border-tertiary: #2c2e33;
--color-border-quaternary: #393b42;
--color-input-border-active: rgba(255, 255, 255, 0.3);
--color-accent-primary: 14, 165, 233; /* sky-500 */
--color-accent-secondary: 56, 189, 248;
--color-accent-tertiary: 125, 211, 252;
--color-accent-quaternary: 186, 230, 253;
--theme-color-default-background: var(--color-bg-primary);
--theme-color-icon-default: var(--color-text-tertiary);
--theme-color-icon-active: var(--color-text-secondary);
--theme-color-card-background: var(--color-bg-secondary);
--theme-color-card-background-active: var(--color-bg-tertiary);
--theme-color-card-background-separator: var(--color-border-tertiary);
--theme-color-card-border: var(--color-border-secondary);
--theme-color-card-border-active: var(--color-border-tertiary);
--theme-color-default-background-separator: var(--color-border-primary);
--theme-color-primary-text: var(--color-text-primary);
--theme-color-muted-text: var(--color-text-secondary);
--theme-color-menu-active: var(--color-bg-secondary);
--theme-color-input-border: var(--color-border-quaternary);
--theme-color-input-background: var(--color-bg-secondary);
--theme-color-tab-background: var(--theme-color-card-background);
--theme-color-tab-background-active: var(--theme-color-card-background-active);
--theme-color-tab-border: var(--theme-color-card-border);
--theme-color-row-separator-background: var(--theme-color-default-background-separator);
--theme-color-row-heading-background: var(--theme-color-card-background);
--theme-color-row-border: var(--theme-color-card-border);
--theme-color-row-heading-border: var(--theme-color-card-border);
}
* {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* width */
::-webkit-scrollbar {
width: 5px;
}
/* Track */
::-webkit-scrollbar-track {
background: transparent;
}
/* Handle */
::-webkit-scrollbar-thumb {
background: #888;
border-radius: 2px;
}
/* Handle on hover */
::-webkit-scrollbar-thumb:hover {
background: #555;
}
[x-cloak] {
display: none;
}
body {
background-color: var(--theme-color-default-background);
}
/* Import shared solidtime styles from UI package */
@import '@solidtime/ui/styles.css';
/* Desktop app specific styles - Inter font */
@font-face {
font-family: 'Outfit';
src: url('/fonts/Outfit-Regular.ttf');
font-weight: 400;
}
@font-face {
font-family: 'Outfit';
src: url('/fonts/Outfit-Medium.ttf');
font-weight: 500;
}
@font-face {
font-family: 'Outfit';
src: url('/fonts/Outfit-SemiBold.ttf');
font-weight: 600;
}
@font-face {
font-family: 'Outfit';
src: url('/fonts/Outfit-Bold.ttf');
font-weight: 700;
}
@font-face {
font-family: 'Outfit';
src: url('/fonts/Outfit-ExtraBold.ttf');
font-weight: 800;
font-family: 'Inter';
src: url('/fonts/InterVariable.woff2') format('woff2-variations');
font-weight: 100 900;
font-style: normal;
font-display: swap;
}
+26
View File
@@ -0,0 +1,26 @@
import { usePreferredColorScheme, useStorage } from '@vueuse/core'
import { computed, watch } from 'vue'
type themeOption = 'system' | 'light' | 'dark'
const themeSetting = useStorage<themeOption>('theme', 'system')
const preferredColor = usePreferredColorScheme()
const theme = computed(() => {
if (themeSetting.value === 'system') {
console.log(preferredColor.value)
if (preferredColor.value === 'no-preference') {
return 'dark'
}
return preferredColor.value
}
return themeSetting.value
})
function useTheme() {
document.documentElement.classList.add(theme.value)
watch(theme, (newTheme, oldTheme) => {
document.documentElement.classList.remove(oldTheme)
document.documentElement.classList.add(newTheme)
})
}
export { type themeOption, themeSetting, theme, useTheme }
+11 -54
View File
@@ -1,72 +1,29 @@
/** @type {import('tailwindcss').Config} */
import defaultTheme from 'tailwindcss/defaultTheme'
import defaultTheme from 'tailwindcss/defaultTheme.js'
import forms from '@tailwindcss/forms'
import typography from '@tailwindcss/typography'
import { solidtimeTheme } from '@solidtime/ui/tailwind.theme.js'
export default {
darkMode: ['selector', '.dark'],
content: [
'./index.html',
'./index-mini.html',
'./src/**/*.{vue,js,ts,jsx,tsx}',
'./node_modules/@solidtime/ui/**/*.{vue,js,ts,jsx,tsx}',
],
/* This is a hardcopy from the main repo */
theme: {
extend: {
boxShadow: {
card: '0 4px 7px 0px rgb(0 0 0 / 30%)',
dropdown: '0 4px 7px 0px rgb(0 0 0 / 40%)',
},
containers: {
'2xs': '16rem',
},
...solidtimeTheme,
fontFamily: {
sans: ['Outfit', ...defaultTheme.fontFamily.sans],
},
colors: {
primary: 'var(--color-bg-primary)',
secondary: 'var(--color-bg-secondary)',
tertiary: 'var(--color-bg-tertiary)',
quaternary: 'var(--color-bg-quaternary)',
background: 'var(--color-bg-background)',
'text-primary': 'var(--color-text-primary)',
'text-secondary': 'var(--color-text-secondary)',
'text-tertiary': 'var(--color-text-tertiary)',
'text-quaternary': 'var(--color-text-quaternary)',
'border-primary': 'var(--color-border-primary)',
'border-secondary': 'var(--color-border-secondary)',
'border-tertiary': 'var(--color-border-tertiary)',
'default-background': 'var(--theme-color-default-background)',
'default-background-separator': 'var(--theme-color-default-background-separator)',
'card-background': 'var(--theme-color-card-background)',
'card-background-active': 'var(--theme-color-card-background-active)',
'card-background-separator': 'var(--theme-color-card-background-separator)',
'card-border': 'var(--theme-color-card-border)',
'card-border-active': 'var(--theme-color-card-border-active)',
muted: 'var(--theme-color-muted-text)',
'icon-default': 'var(--theme-color-icon-default)',
'tab-background': 'var(--theme-color-tab-background)',
'tab-background-active': 'var(--theme-color-tab-background-active)',
'tab-border': 'var(--theme-color-tab-border)',
'icon-active': 'var(--theme-color-icon-active)',
'menu-active': 'var(--theme-color-menu-active)',
'input-border': 'var(--theme-color-input-border)',
'input-border-active': 'var(--color-input-border-active)',
'input-background': 'var(--theme-color-input-background)',
'button-secondary-background': 'var(--theme-color-card-background)',
'button-secondary-background-hover': 'var(--theme-color-card-background-active)',
'button-secondary-border': 'var(--theme-color-card-border)',
'row-separator': 'var(--theme-color-row-separator-background)',
'row-heading-background': 'var(--theme-color-row-heading-background)',
'row-heading-border': 'var(--theme-color-row-heading-border)',
accent: {
200: 'rgba(var(--color-accent-quaternary), <alpha-value>)',
300: 'rgba(var(--color-accent-tertiary), <alpha-value>)',
400: 'rgba(var(--color-accent-secondary), <alpha-value>)',
500: 'rgba(var(--color-accent-primary), <alpha-value>)',
},
sans: ['Inter', ...defaultTheme.fontFamily.sans],
},
},
},
plugins: [forms, typography, require('@tailwindcss/container-queries')],
plugins: [
forms,
typography,
require('@tailwindcss/container-queries'),
require('tailwindcss-animate'),
],
}