mirror of
https://github.com/solidtime-io/solidtime-desktop.git
synced 2026-05-07 20:32:27 +00:00
add calendar view and light mode support
This commit is contained in:
+26
-12
@@ -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',
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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.
Binary file not shown.
+51
-37
File diff suppressed because one or more lines are too long
@@ -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
@@ -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')
|
||||
|
||||
@@ -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>
|
||||
@@ -0,0 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import MainTimeEntryTable from '../components/MainTimeEntryTable.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-1">
|
||||
<MainTimeEntryTable />
|
||||
</div>
|
||||
</template>
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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'),
|
||||
],
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user