2
0
mirror of https://github.com/frappe/books.git synced 2024-11-09 15:20:56 +00:00

feat: windowstitlebar, sidebar and Dashboard changed to darkmode

This commit is contained in:
pu-raihan 2024-03-23 20:42:55 +05:30
parent 2119c301d7
commit e131c560b9
28 changed files with 310 additions and 249 deletions

View File

@ -11,6 +11,9 @@
"600": "#7C7C7C", "600": "#7C7C7C",
"700": "#525252", "700": "#525252",
"800": "#383838", "800": "#383838",
"850": "#282828",
"875": "#212121",
"890": "#1C1C1C",
"900": "#171717" "900": "#171717"
}, },
"red": { "red": {

View File

@ -36,6 +36,7 @@ export type ConfigMap = {
lastSelectedFilePath: null | string; lastSelectedFilePath: null | string;
language: string language: string
deviceId: string deviceId: string
darkMode: boolean
}; };
export interface ConfigFile { export interface ConfigFile {

View File

@ -1,7 +1,7 @@
<template> <template>
<div <div
id="app" id="app"
class="h-screen flex flex-col font-sans overflow-hidden antialiased" class="dark:bg-gray-900 h-screen flex flex-col font-sans overflow-hidden antialiased"
:dir="languageDirection" :dir="languageDirection"
:language="language" :language="language"
> >
@ -14,7 +14,9 @@
<Desk <Desk
v-if="activeScreen === 'Desk'" v-if="activeScreen === 'Desk'"
class="flex-1" class="flex-1"
:darkMode="darkMode"
@change-db-file="showDbSelector" @change-db-file="showDbSelector"
@toggle-darkmode="toggleDMode"
/> />
<DatabaseSelector <DatabaseSelector
v-if="activeScreen === 'DatabaseSelector'" v-if="activeScreen === 'DatabaseSelector'"
@ -61,6 +63,7 @@ import { Search } from './utils/search';
import { Shortcuts } from './utils/shortcuts'; import { Shortcuts } from './utils/shortcuts';
import { routeTo } from './utils/ui'; import { routeTo } from './utils/ui';
import { useKeys } from './utils/vueUtils'; import { useKeys } from './utils/vueUtils';
import { toggleDarkMode } from 'src/utils/theme';
enum Screen { enum Screen {
Desk = 'Desk', Desk = 'Desk',
@ -106,10 +109,12 @@ export default defineComponent({
activeScreen: null, activeScreen: null,
dbPath: '', dbPath: '',
companyName: '', companyName: '',
darkMode: false,
} as { } as {
activeScreen: null | Screen; activeScreen: null | Screen;
dbPath: string; dbPath: string;
companyName: string; companyName: string;
darkMode: boolean | undefined;
}; };
}, },
computed: { computed: {
@ -124,6 +129,7 @@ export default defineComponent({
}, },
async mounted() { async mounted() {
await this.setInitialScreen(); await this.setInitialScreen();
this.darkMode = fyo.config.get('darkMode') as boolean;
}, },
methods: { methods: {
async setInitialScreen(): Promise<void> { async setInitialScreen(): Promise<void> {
@ -247,6 +253,10 @@ export default defineComponent({
this.searcher = null; this.searcher = null;
this.companyName = ''; this.companyName = '';
}, },
async toggleDMode(): Promise<void> {
toggleDarkMode();
this.darkMode = fyo.config.get('darkMode');
},
}, },
}); });

View File

@ -39,10 +39,10 @@ export default defineComponent({
_class() { _class() {
return { return {
'opacity-50 cursor-not-allowed pointer-events-none': this.disabled, 'opacity-50 cursor-not-allowed pointer-events-none': this.disabled,
'text-white': this.type === 'primary', 'text-white dark:text-black': this.type === 'primary',
'bg-black': this.type === 'primary' && this.background, 'bg-black dark:bg-gray-400': this.type === 'primary' && this.background,
'text-gray-700': this.type !== 'primary', 'text-gray-700 dark:text-gray-200': this.type !== 'primary',
'bg-gray-200': this.type !== 'primary' && this.background, 'bg-gray-200 dark:bg-gray-900': this.type !== 'primary' && this.background,
'h-8': this.background, 'h-8': this.background,
'px-3': this.padding && this.icon, 'px-3': this.padding && this.icon,
'px-6': this.padding && !this.icon, 'px-6': this.padding && !this.icon,

View File

@ -118,7 +118,7 @@
ref="tooltip" ref="tooltip"
:offset="15" :offset="15"
placement="top" placement="top"
class="text-sm shadow-md px-2 py-1 bg-white text-gray-900 border-s-4" class="text-sm shadow-md px-2 py-1 bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-200 border-s-4"
:style="{ borderColor: activeColor }" :style="{ borderColor: activeColor }"
> >
<div class="flex flex-col justify-center items-center"> <div class="flex flex-col justify-center items-center">

View File

@ -45,7 +45,7 @@
:key="i" :key="i"
clip-path="url(#donut-hole)" clip-path="url(#donut-hole)"
:d="getArcPath(cx, cy, radius, start_, theta)" :d="getArcPath(cx, cy, radius, start_, theta)"
:stroke="sectors[i].color" :stroke="getSectorColor(i)"
:stroke-width="thickness + (active === i ? 4 : 0)" :stroke-width="thickness + (active === i ? 4 : 0)"
:style="{ transformOrigin: `${cx}px ${cy}px` }" :style="{ transformOrigin: `${cx}px ${cy}px` }"
class="sector" class="sector"
@ -57,7 +57,11 @@
:x="cx" :x="cx"
:y="cy" :y="cy"
text-anchor="middle" text-anchor="middle"
style="font-size: 6px; font-weight: bold; fill: #415668" :style="{
fontSize: '6px',
fontWeight: 'bold',
fill: darkMode ? '#FFFFFF' : '#415668',
}"
> >
{{ {{
valueFormatter( valueFormatter(
@ -101,6 +105,7 @@ export default {
offsetY: { default: 0, type: Number }, offsetY: { default: 0, type: Number },
textOffsetX: { default: 0, type: Number }, textOffsetX: { default: 0, type: Number },
textOffsetY: { default: 0, type: Number }, textOffsetY: { default: 0, type: Number },
darkMode: Boolean,
}, },
emits: ['change'], emits: ['change'],
computed: { computed: {
@ -150,6 +155,13 @@ export default {
return `M ${startX} ${startY} A ${r} ${r} 0 ${largeArcFlag} ${sweepFlag} ${endX} ${endY}`; return `M ${startX} ${startY} A ${r} ${r} 0 ${largeArcFlag} ${sweepFlag} ${endX} ${endY}`;
}, },
getSectorColor(index) {
if (this.darkMode) {
return this.sectors[index].color.darkColor;
} else {
return this.sectors[index].color.color;
}
},
}, },
}; };
</script> </script>

View File

@ -117,7 +117,7 @@
ref="tooltip" ref="tooltip"
:offset="15" :offset="15"
placement="top" placement="top"
class="text-sm shadow-md px-2 py-1 bg-white text-gray-900 border-s-4" class="text-sm shadow-md px-2 py-1 bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-200 border-s-4"
:style="{ borderColor: colors[yi] }" :style="{ borderColor: colors[yi] }"
> >
<div class="flex flex-col justify-center items-center"> <div class="flex flex-col justify-center items-center">

View File

@ -15,14 +15,19 @@
</div> </div>
</template> </template>
<template #content> <template #content>
<div class="bg-white rounded w-full min-w-40 overflow-hidden"> <div
class="bg-white dark:bg-gray-850 dark:text-white rounded w-full min-w-40 overflow-hidden"
>
<div class="p-1 max-h-64 overflow-auto custom-scroll text-sm"> <div class="p-1 max-h-64 overflow-auto custom-scroll text-sm">
<div v-if="isLoading" class="p-2 text-gray-600 italic"> <div
v-if="isLoading"
class="p-2 text-gray-600 dark:text-gray-400 italic"
>
{{ t`Loading...` }} {{ t`Loading...` }}
</div> </div>
<div <div
v-else-if="dropdownItems.length === 0" v-else-if="dropdownItems.length === 0"
class="p-2 text-gray-600 italic" class="p-2 text-gray-600 dark:text-gray-400 italic"
> >
{{ getEmptyMessage() }} {{ getEmptyMessage() }}
</div> </div>
@ -34,31 +39,18 @@
> >
<div <div
v-if="d.isGroup" v-if="d.isGroup"
class=" class="px-2 pt-3 pb-1 text-xs uppercase text-gray-700 dark:text-gray-400 font-semibold tracking-wider"
px-2
pt-3
pb-1
text-xs
uppercase
text-gray-700
font-semibold
tracking-wider
"
> >
{{ d.label }} {{ d.label }}
</div> </div>
<a <a
v-else v-else
class=" class="block p-2 rounded-md mt-1 first:mt-0 cursor-pointer truncate"
block :class="
p-2 index === highlightedIndex
rounded-md ? 'bg-gray-100 dark:bg-gray-875'
mt-1 : ''
first:mt-0
cursor-pointer
truncate
" "
:class="index === highlightedIndex ? 'bg-gray-100' : ''"
@mouseenter="highlightedIndex = index" @mouseenter="highlightedIndex = index"
@mousedown.prevent @mousedown.prevent
@click="selectItem(d)" @click="selectItem(d)"

View File

@ -7,7 +7,11 @@
<template #target="{ togglePopover }"> <template #target="{ togglePopover }">
<Button :icon="true" @click="togglePopover()"> <Button :icon="true" @click="togglePopover()">
<span class="flex items-center"> <span class="flex items-center">
<Icon name="filter" size="12" class="stroke-current text-gray-700" /> <Icon
name="filter"
size="12"
class="stroke-current text-gray-700 dark:text-gray-400"
/>
<span class="ms-1"> <span class="ms-1">
<template v-if="activeFilterCount > 0"> <template v-if="activeFilterCount > 0">
{{ filterAppliedMessage }} {{ filterAppliedMessage }}
@ -30,18 +34,7 @@
class="flex items-center justify-between text-base gap-2" class="flex items-center justify-between text-base gap-2"
> >
<div <div
class=" class="cursor-pointer w-4 h-4 flex items-center justify-center text-gray-600 hover:text-gray-800 rounded-md group"
cursor-pointer
w-4
h-4
flex
items-center
justify-center
text-gray-600
hover:text-gray-800
rounded-md
group
"
> >
<span class="hidden group-hover:inline-block"> <span class="hidden group-hover:inline-block">
<feather-icon <feather-icon
@ -106,16 +99,7 @@
</template> </template>
</div> </div>
<div <div
class=" class="text-base border-t p-2 flex items-center text-gray-600 cursor-pointer hover:bg-gray-100"
text-base
border-t
p-2
flex
items-center
text-gray-600
cursor-pointer
hover:bg-gray-100
"
@click="addNewFilter" @click="addNewFilter"
> >
<feather-icon name="plus" class="w-4 h-4" /> <feather-icon name="plus" class="w-4 h-4" />
@ -149,7 +133,7 @@ const conditions = [
{ label: t`Is Not Empty`, value: 'is not null' }, { label: t`Is Not Empty`, value: 'is not null' },
] as const; ] as const;
type Condition = typeof conditions[number]['value']; type Condition = (typeof conditions)[number]['value'];
type Filter = { type Filter = {
fieldname: string; fieldname: string;

View File

@ -4,6 +4,7 @@
:is="iconComponent" :is="iconComponent"
:class="iconClasses" :class="iconClasses"
:active="active" :active="active"
:darkMode="darkMode"
/> />
</template> </template>
@ -26,6 +27,7 @@ export default {
props: { props: {
name: { type: String, required: true }, name: { type: String, required: true },
active: { type: Boolean, default: false }, active: { type: Boolean, default: false },
darkMode: { type: Boolean, default: false },
size: { size: {
type: String, type: String,
required: true, required: true,

View File

@ -1,15 +1,30 @@
<script> <script lang="ts">
import { uicolors } from 'src/utils/colors'; import { uicolors } from 'src/utils/colors';
export default { export default {
name: 'IconBase', name: 'IconBase',
props: ['active'], props: {
active: Boolean,
darkMode: Boolean,
},
computed: { computed: {
lightColor() { lightColor(): string {
return this.active ? uicolors.gray['600'] : uicolors.gray['400']; const activeGray = this.darkMode
? uicolors.gray['500']
: uicolors.gray['600'];
const passiveGray = this.darkMode
? uicolors.gray['700']
: uicolors.gray['400'];
return this.active ? activeGray : passiveGray;
}, },
darkColor() { darkColor(): string {
return this.active ? uicolors.gray['800'] : uicolors.gray['600']; const activeGray = this.darkMode
? uicolors.gray['200']
: uicolors.gray['800'];
const passiveGray = this.darkMode
? uicolors.gray['500']
: uicolors.gray['600'];
return this.active ? activeGray : passiveGray;
}, },
}, },
}; };

View File

@ -1,8 +1,8 @@
<template> <template>
<div <div
class="px-4 flex justify-between items-center h-row-largest flex-shrink-0" class="px-4 flex justify-between items-center h-row-largest flex-shrink-0 dark:bg-gray-875"
:class="[ :class="[
border ? 'border-b' : '', border ? 'border-b dark:border-gray-800' : '',
platform !== 'Windows' ? 'window-drag' : '', platform !== 'Windows' ? 'window-drag' : '',
]" ]"
> >
@ -22,7 +22,7 @@
<PageHeaderNavGroup /> <PageHeaderNavGroup />
<h1 <h1
v-if="title" v-if="title"
class="text-xl font-semibold select-none whitespace-nowrap" class="text-xl font-semibold select-none whitespace-nowrap dark:text-white"
> >
{{ title }} {{ title }}
</h1> </h1>

View File

@ -4,9 +4,11 @@
<!-- Back Button --> <!-- Back Button -->
<a <a
ref="backlink" ref="backlink"
class="nav-link border-l border-r border-white" class="nav-link border-l border-r border-white dark:border-gray-850 dark:bg-gray-900"
:class=" :class="
historyState.back ? 'text-gray-700 cursor-pointer' : 'text-gray-400' historyState.back
? 'text-gray-700 dark:text-gray-300 cursor-pointer'
: 'text-gray-400 dark:text-gray-700'
" "
@click="$router.back()" @click="$router.back()"
> >
@ -14,9 +16,11 @@
</a> </a>
<!-- Forward Button --> <!-- Forward Button -->
<a <a
class="nav-link rounded-md rounded-l-none" class="nav-link rounded-md rounded-l-none dark:bg-gray-900"
:class=" :class="
historyState.forward ? 'text-gray-700 cursor-pointer' : 'text-gray-400' historyState.forward
? 'text-gray-700 dark:text-gray-400 cursor-pointer'
: 'text-gray-400 dark:text-gray-700'
" "
@click="$router.forward()" @click="$router.forward()"
> >

View File

@ -12,15 +12,7 @@
v-show="isOpen" v-show="isOpen"
ref="popover" ref="popover"
:class="popoverClass" :class="popoverClass"
class=" class="bg-white dark:bg-gray-850 rounded-md border dark:border-gray-875 shadow-lg popover-container relative z-10"
bg-white
rounded-md
border
shadow-lg
popover-container
relative
z-10
"
:style="{ 'transition-delay': `${isOpen ? entryDelay : exitDelay}ms` }" :style="{ 'transition-delay': `${isOpen ? entryDelay : exitDelay}ms` }"
> >
<slot name="content" :toggle-popover="togglePopover"></slot> <slot name="content" :toggle-popover="togglePopover"></slot>

View File

@ -1,8 +1,15 @@
<template> <template>
<div> <div>
<!-- Search Bar Button --> <!-- Search Bar Button -->
<Button class="px-3 py-2 rounded-r-none" :padding="false" @click="open"> <Button
<feather-icon name="search" class="w-4 h-4 text-gray-700" /> class="px-3 py-2 rounded-r-none dark:bg-gray-900"
:padding="false"
@click="open"
>
<feather-icon
name="search"
class="w-4 h-4 text-gray-700 dark:text-gray-300"
/>
</Button> </Button>
</div> </div>
@ -22,16 +29,7 @@
autocomplete="off" autocomplete="off"
spellcheck="false" spellcheck="false"
:placeholder="t`Type to search...`" :placeholder="t`Type to search...`"
class=" class="bg-gray-100 text-2xl focus:outline-none w-full placeholder-gray-500 text-gray-900 rounded-md p-3"
bg-gray-100
text-2xl
focus:outline-none
w-full
placeholder-gray-500
text-gray-900
rounded-md
p-3
"
@keydown.up="up" @keydown.up="up"
@keydown.down="down" @keydown.down="down"
@keydown.enter="() => select()" @keydown.enter="() => select()"
@ -129,14 +127,7 @@
<button <button
v-for="sf in schemaFilters" v-for="sf in schemaFilters"
:key="sf.value" :key="sf.value"
class=" class="border px-1 py-0.5 rounded-lg border-blue-100 whitespace-nowrap"
border
px-1
py-0.5
rounded-lg
border-blue-100
whitespace-nowrap
"
:class="{ :class="{
'bg-blue-100': searcher?.filters.schemaFilters[sf.value], 'bg-blue-100': searcher?.filters.schemaFilters[sf.value],
}" }"

View File

@ -1,12 +1,12 @@
<template> <template>
<div <div
class="py-2 h-full flex justify-between flex-col bg-gray-25 relative" class="py-2 h-full flex justify-between flex-col bg-gray-25 dark:bg-gray-900 relative"
:class="{ :class="{
'window-drag': platform !== 'Windows', 'window-drag': platform !== 'Windows',
}" }"
> >
<div> <div>
<!-- Company name and DB Switcher --> <!-- Company name -->
<div <div
class="px-4 flex flex-row items-center justify-between mb-4" class="px-4 flex flex-row items-center justify-between mb-4"
:class=" :class="
@ -15,13 +15,7 @@
> >
<h6 <h6
data-testid="company-name" data-testid="company-name"
class=" class="font-semibold dark:text-gray-200 whitespace-nowrap overflow-auto no-scrollbar select-none"
font-semibold
whitespace-nowrap
overflow-auto
no-scrollbar
select-none
"
> >
{{ companyName }} {{ companyName }}
</h6> </h6>
@ -30,10 +24,10 @@
<!-- Sidebar Items --> <!-- Sidebar Items -->
<div v-for="group in groups" :key="group.label"> <div v-for="group in groups" :key="group.label">
<div <div
class="px-4 flex items-center cursor-pointer hover:bg-gray-100 h-10" class="px-4 flex items-center cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-875 h-10"
:class=" :class="
isGroupActive(group) && !group.items isGroupActive(group) && !group.items
? 'bg-gray-100 border-s-4 border-gray-800' ? 'bg-gray-100 dark:bg-gray-875 border-s-4 border-gray-800 dark:border-gray-100'
: '' : ''
" "
@click="routeToSidebarItem(group)" @click="routeToSidebarItem(group)"
@ -44,11 +38,16 @@
:size="group.iconSize || '18'" :size="group.iconSize || '18'"
:height="group.iconHeight ?? 0" :height="group.iconHeight ?? 0"
:active="!!isGroupActive(group)" :active="!!isGroupActive(group)"
:darkMode="darkMode"
:class="isGroupActive(group) && !group.items ? '-ms-1' : ''" :class="isGroupActive(group) && !group.items ? '-ms-1' : ''"
/> />
<div <div
class="ms-2 text-lg text-gray-700" class="ms-2 text-lg text-gray-700"
:class="isGroupActive(group) && !group.items && 'text-gray-900'" :class="
isGroupActive(group) && !group.items
? 'text-gray-900 dark:text-gray-25'
: 'dark:text-gray-300'
"
> >
{{ group.label }} {{ group.label }}
</div> </div>
@ -59,19 +58,11 @@
<div <div
v-for="item in group.items" v-for="item in group.items"
:key="item.label" :key="item.label"
class=" class="text-base h-10 ps-10 cursor-pointer flex items-center hover:bg-gray-100 dark:hover:bg-gray-800"
text-base
h-10
ps-10
cursor-pointer
flex
items-center
hover:bg-gray-100
"
:class=" :class="
isItemActive(item) isItemActive(item)
? 'bg-gray-100 text-gray-900 border-s-4 border-gray-800' ? 'bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-gray-100 border-s-4 border-gray-800 dark:border-gray-100'
: 'text-gray-700' : 'text-gray-700 dark:text-gray-400'
" "
@click="routeToSidebarItem(item)" @click="routeToSidebarItem(item)"
> >
@ -83,16 +74,20 @@
</div> </div>
</div> </div>
<!-- Report Issue and App Version --> <!-- Report Issue, DB Switcher and Darkmode Switcher -->
<div class="window-no-drag flex flex-col gap-2 py-2 px-4"> <div class="window-no-drag flex flex-col gap-2 py-2 px-4">
<button <button
class=" class="flex text-sm text-gray-600 dark:text-gray-500 hover:text-gray-800 dark:hover:text-gray-400 gap-1 items-center"
flex @click="$emit('toggle-darkmode')"
text-sm text-gray-600 >
hover:text-gray-800 <feather-icon :name="darkMode?'sun':'moon'" class="h-4 w-4 flex-shrink-0" />
gap-1 <p>
items-center {{ t`Dark Mode` }}
" </p>
</button>
<button
class="flex text-sm text-gray-600 dark:text-gray-500 hover:text-gray-800 dark:hover:text-gray-400 gap-1 items-center"
@click="openDocumentation" @click="openDocumentation"
> >
<feather-icon name="help-circle" class="h-4 w-4 flex-shrink-0" /> <feather-icon name="help-circle" class="h-4 w-4 flex-shrink-0" />
@ -102,13 +97,7 @@
</button> </button>
<button <button
class=" class="flex text-sm text-gray-600 dark:text-gray-500 hover:text-gray-800 dark:hover:text-gray-400 gap-1 items-center"
flex
text-sm text-gray-600
hover:text-gray-800
gap-1
items-center
"
@click="viewShortcuts = true" @click="viewShortcuts = true"
> >
<feather-icon name="command" class="h-4 w-4 flex-shrink-0" /> <feather-icon name="command" class="h-4 w-4 flex-shrink-0" />
@ -117,13 +106,7 @@
<button <button
data-testid="change-db" data-testid="change-db"
class=" class="flex text-sm text-gray-600 dark:text-gray-500 hover:text-gray-800 dark:hover:text-gray-400 gap-1 items-center"
flex
text-sm text-gray-600
hover:text-gray-800
gap-1
items-center
"
@click="$emit('change-db-file')" @click="$emit('change-db-file')"
> >
<feather-icon name="database" class="h-4 w-4 flex-shrink-0" /> <feather-icon name="database" class="h-4 w-4 flex-shrink-0" />
@ -131,13 +114,7 @@
</button> </button>
<button <button
class=" class="flex text-sm text-gray-600 dark:text-gray-500 hover:text-gray-800 dark:hover:text-gray-400 gap-1 items-center"
flex
text-sm text-gray-600
hover:text-gray-800
gap-1
items-center
"
@click="() => reportIssue()" @click="() => reportIssue()"
> >
<feather-icon name="flag" class="h-4 w-4 flex-shrink-0" /> <feather-icon name="flag" class="h-4 w-4 flex-shrink-0" />
@ -158,17 +135,7 @@
<!-- Hide Sidebar Button --> <!-- Hide Sidebar Button -->
<button <button
class=" class="absolute bottom-0 end-0 text-gray-600 dark:text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-875 rounded p-1 m-4 rtl-rotate-180"
absolute
bottom-0
end-0
text-gray-600
hover:bg-gray-100
rounded
p-1
m-4
rtl-rotate-180
"
@click="() => toggleSidebar()" @click="() => toggleSidebar()"
> >
<feather-icon name="chevrons-left" class="w-4 h-4" /> <feather-icon name="chevrons-left" class="w-4 h-4" />
@ -201,7 +168,10 @@ export default defineComponent({
Modal, Modal,
ShortcutsHelper, ShortcutsHelper,
}, },
emits: ['change-db-file'], props: {
darkMode: Boolean,
},
emits: ['change-db-file', 'toggle-darkmode'],
setup() { setup() {
return { return {
languageDirection: inject(languageDirectionKey), languageDirection: inject(languageDirectionKey),

View File

@ -1,6 +1,6 @@
<template> <template>
<div <div
class="window-drag flex items-center border-b text-gray-900 border-gray-100" class="window-drag flex items-center border-b dark:bg-gray-900 text-gray-900 dark:text-gray-100 border-gray-100 dark:border-gray-800"
style="height: 28px" style="height: 28px"
> >
<Fb class="ms-2" /> <Fb class="ms-2" />

View File

@ -2,20 +2,26 @@
<div> <div>
<!-- Title and Period Selector --> <!-- Title and Period Selector -->
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="font-semibold text-base">{{ t`Cashflow` }}</div> <div class="font-semibold text-base dark:text-white">
{{ t`Cashflow` }}
</div>
<!-- Chart Legend --> <!-- Chart Legend -->
<div v-if="hasData" class="flex text-base gap-8"> <div v-if="hasData" class="flex text-base gap-8">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span class="w-3 h-3 rounded-sm inline-block bg-blue-500" /> <span
<span class="text-gray-900">{{ t`Inflow` }}</span> class="w-3 h-3 rounded-sm inline-block bg-blue-500 dark:bg-blue-600"
/>
<span class="text-gray-900 dark:text-gray-25">{{ t`Inflow` }}</span>
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span class="w-3 h-3 rounded-sm inline-block bg-pink-500" /> <span
<span class="text-gray-900">{{ t`Outflow` }}</span> class="w-3 h-3 rounded-sm inline-block bg-pink-500 dark:bg-pink-600"
/>
<span class="text-gray-900 dark:text-gray-25">{{ t`Outflow` }}</span>
</div> </div>
</div> </div>
<div v-else class="w-16 h-5 bg-gray-200 rounded" /> <div v-else class="w-16 h-5 bg-gray-200 dark:bg-gray-700 rounded" />
<PeriodSelector <PeriodSelector
v-if="hasData" v-if="hasData"
@ -23,7 +29,7 @@
:options="periodOptions" :options="periodOptions"
@change="(value) => (period = value)" @change="(value) => (period = value)"
/> />
<div v-else class="w-20 h-5 bg-gray-200 rounded" /> <div v-else class="w-20 h-5 bg-gray-200 dark:bg-gray-700 rounded" />
</div> </div>
<!-- Line Chart --> <!-- Line Chart -->
@ -32,6 +38,8 @@
class="mt-4" class="mt-4"
:aspect-ratio="4.15" :aspect-ratio="4.15"
:colors="chartData.colors" :colors="chartData.colors"
:gridColor="chartData.gridColor"
:fontColor="chartData.fontColor"
:points="chartData.points" :points="chartData.points"
:x-labels="chartData.xLabels" :x-labels="chartData.xLabels"
:format="chartData.format" :format="chartData.format"
@ -68,6 +76,9 @@ export default defineComponent({
PeriodSelector, PeriodSelector,
LineChart, LineChart,
}, },
props: {
darkMode: Boolean,
},
extends: DashboardChartBase, extends: DashboardChartBase,
data: () => ({ data: () => ({
data: [] as { inflow: number; outflow: number; yearmonth: string }[], data: [] as { inflow: number; outflow: number; yearmonth: string }[],
@ -78,10 +89,16 @@ export default defineComponent({
computed: { computed: {
chartData() { chartData() {
let data = this.data; let data = this.data;
let colors = [uicolors.blue['500'], uicolors.pink['500']]; let colors = [
uicolors.blue[this.darkMode ? '600' : '500'],
uicolors.pink[this.darkMode ? '600' : '500'],
];
if (!this.hasData) { if (!this.hasData) {
data = dummyData; data = dummyData;
colors = [uicolors.gray['200'], uicolors.gray['100']]; colors = [
this.darkMode ? uicolors.gray['700'] : uicolors.gray['200'],
this.darkMode ? uicolors.gray['800'] : uicolors.gray['100'],
];
} }
const xLabels = data.map((cf) => cf.yearmonth); const xLabels = data.map((cf) => cf.yearmonth);
@ -91,7 +108,16 @@ export default defineComponent({
const format = (value: number) => fyo.format(value ?? 0, 'Currency'); const format = (value: number) => fyo.format(value ?? 0, 'Currency');
const yMax = getYMax(points); const yMax = getYMax(points);
return { points, xLabels, colors, format, yMax, formatX: formatXLabels }; return {
points,
xLabels,
colors,
format,
yMax,
formatX: formatXLabels,
gridColor: this.darkMode ? 'rgba(200, 200, 200, 0.2)' : undefined,
fontColor: this.darkMode ? uicolors.gray['400'] : undefined,
};
}, },
}, },
async activated() { async activated() {

View File

@ -2,14 +2,7 @@
<div class="h-screen" style="width: var(--w-desk)"> <div class="h-screen" style="width: var(--w-desk)">
<PageHeader :title="t`Dashboard`"> <PageHeader :title="t`Dashboard`">
<div <div
class=" class="border dark:border-gray-900 rounded bg-gray-50 dark:bg-gray-890 focus-within:bg-gray-100 dark:focus-within:bg-gray-900 flex items-center"
border
rounded
bg-gray-50
focus-within:bg-gray-100
flex
items-center
"
> >
<PeriodSelector <PeriodSelector
class="px-3" class="px-3"
@ -21,43 +14,48 @@
</PageHeader> </PageHeader>
<div <div
class="no-scrollbar overflow-auto" class="no-scrollbar overflow-auto dark:bg-gray-875"
style="height: calc(100vh - var(--h-row-largest) - 1px)" style="height: calc(100vh - var(--h-row-largest) - 1px)"
> >
<div style="min-width: var(--w-desk-fixed)" class="overflow-auto"> <div style="min-width: var(--w-desk-fixed)" class="overflow-auto">
<Cashflow <Cashflow
class="p-4" class="p-4"
:common-period="period" :common-period="period"
:darkMode="darkMode"
@period-change="handlePeriodChange" @period-change="handlePeriodChange"
/> />
<hr /> <hr class="dark:border-gray-800" />
<div class="flex w-full"> <div class="flex w-full">
<UnpaidInvoices <UnpaidInvoices
:schema-name="'SalesInvoice'" :schema-name="'SalesInvoice'"
:common-period="period" :common-period="period"
class="border-e" :darkMode="darkMode"
class="border-e dark:border-gray-800"
@period-change="handlePeriodChange" @period-change="handlePeriodChange"
/> />
<UnpaidInvoices <UnpaidInvoices
:schema-name="'PurchaseInvoice'" :schema-name="'PurchaseInvoice'"
:common-period="period" :common-period="period"
:darkMode="darkMode"
@period-change="handlePeriodChange" @period-change="handlePeriodChange"
/> />
</div> </div>
<hr /> <hr class="dark:border-gray-800" />
<div class="flex"> <div class="flex">
<ProfitAndLoss <ProfitAndLoss
class="w-full p-4 border-e" class="w-full p-4 border-e dark:border-gray-800"
:common-period="period" :common-period="period"
:darkMode="darkMode"
@period-change="handlePeriodChange" @period-change="handlePeriodChange"
/> />
<Expenses <Expenses
class="w-full p-4" class="w-full p-4"
:common-period="period" :common-period="period"
:darkMode="darkMode"
@period-change="handlePeriodChange" @period-change="handlePeriodChange"
/> />
</div> </div>
<hr /> <hr class="dark:border-gray-800" />
</div> </div>
</div> </div>
</div> </div>
@ -82,6 +80,9 @@ export default {
PeriodSelector, PeriodSelector,
UnpaidInvoices, UnpaidInvoices,
}, },
props: {
darkMode: Boolean,
},
data() { data() {
return { period: 'This Year' }; return { period: 'This Year' };
}, },

View File

@ -9,7 +9,7 @@
<div v-show="hasData" class="flex relative"> <div v-show="hasData" class="flex relative">
<!-- Chart Legend --> <!-- Chart Legend -->
<div class="w-1/2 flex flex-col gap-4 justify-center"> <div class="w-1/2 flex flex-col gap-4 justify-center dark:text-gray-25">
<!-- Ledgend Item --> <!-- Ledgend Item -->
<div <div
v-for="(d, i) in expenses" v-for="(d, i) in expenses"
@ -36,6 +36,7 @@
:text-offset-x="6.5" :text-offset-x="6.5"
:value-formatter="(value: number) => fyo.format(value, 'Currency')" :value-formatter="(value: number) => fyo.format(value, 'Currency')"
:total-label="t`Total Spending`" :total-label="t`Total Spending`"
:darkMode="darkMode"
@change="(value: number) => (active = value)" @change="(value: number) => (active = value)"
/> />
</div> </div>
@ -45,7 +46,7 @@
v-if="expenses.length === 0" v-if="expenses.length === 0"
class="flex-1 w-full h-full flex-center my-20" class="flex-1 w-full h-full flex-center my-20"
> >
<span class="text-base text-gray-600"> <span class="text-base text-gray-600 dark:text-gray-500">
{{ t`No expenses in this period` }} {{ t`No expenses in this period` }}
</span> </span>
</div> </div>
@ -64,8 +65,8 @@ import PeriodSelector from './PeriodSelector.vue';
import SectionHeader from './SectionHeader.vue'; import SectionHeader from './SectionHeader.vue';
// Linting broken in this file cause of `extends: ...` // Linting broken in this file cause of `extends: ...`
/* /*
eslint-disable @typescript-eslint/no-unsafe-argument, eslint-disable @typescript-eslint/no-unsafe-argument,
@typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-return,
@typescript-eslint/restrict-plus-operands @typescript-eslint/restrict-plus-operands
*/ */
@ -76,14 +77,17 @@ export default defineComponent({
PeriodSelector, PeriodSelector,
SectionHeader, SectionHeader,
}, },
props: {
darkMode: Boolean,
},
extends: DashboardChartBase, extends: DashboardChartBase,
data: () => ({ data: () => ({
active: null as null | number, active: null as null | number,
expenses: [] as { expenses: [] as {
account: string; account: string;
total: number; total: number;
color: string; color: { color: string; darkColor: string };
class: string; class: { class: string; darkClass: string };
}[], }[],
}), }),
computed: { computed: {
@ -93,7 +97,11 @@ export default defineComponent({
hasData(): boolean { hasData(): boolean {
return this.expenses.length > 0; return this.expenses.length > 0;
}, },
sectors(): { color: string; label: string; value: number }[] { sectors(): {
color: { color: string; darkColor: string };
label: string;
value: number;
}[] {
return this.expenses.map(({ account, color, total }) => ({ return this.expenses.map(({ account, color, total }) => ({
color, color,
label: truncate(account, { length: 21 }), label: truncate(account, { length: 21 }),
@ -111,7 +119,6 @@ export default defineComponent({
fromDate.toISO(), fromDate.toISO(),
toDate.toISO() toDate.toISO()
); );
const shades = [ const shades = [
{ class: 'bg-pink-500', hex: uicolors.pink['500'] }, { class: 'bg-pink-500', hex: uicolors.pink['500'] },
{ class: 'bg-pink-400', hex: uicolors.pink['400'] }, { class: 'bg-pink-400', hex: uicolors.pink['400'] },
@ -120,13 +127,25 @@ export default defineComponent({
{ class: 'bg-pink-100', hex: uicolors.pink['100'] }, { class: 'bg-pink-100', hex: uicolors.pink['100'] },
]; ];
const darkshades = [
{ class: 'bg-pink-600', hex: uicolors.pink['600'] },
{ class: 'bg-pink-500', hex: uicolors.pink['500'] },
{ class: 'bg-pink-400', hex: uicolors.pink['400'] },
{ class: 'bg-pink-300', hex: uicolors.pink['300'] },
{
class: 'bg-pink-200 dark:bg-opacity-80',
hex: uicolors.pink['200'] + 'CC',
},
];
this.expenses = topExpenses this.expenses = topExpenses
.filter((e) => e.total > 0) .filter((e) => e.total > 0)
.map((d, i) => { .map((d, i) => {
return { return {
...d, account: d.account,
color: shades[i].hex, total: d.total,
class: shades[i].class, color: { color: shades[i].hex, darkColor: darkshades[i].hex },
class: { class: shades[i].class, darkClass: darkshades[i].class },
}; };
}); });
}, },

View File

@ -9,19 +9,12 @@
}" }"
> >
<div <div
class=" class="text-sm flex focus:outline-none hover:text-gray-800 dark:hover:text-gray-100 focus:text-gray-800 dark:focus:text-gray-100 items-center py-1 rounded-md leading-relaxed cursor-pointer"
text-sm :class="
flex !value
focus:outline-none ? 'text-gray-600 dark:text-gray-500'
hover:text-gray-800 : 'text-gray-900 dark:text-gray-300'
focus:text-gray-800
items-center
py-1
rounded-md
leading-relaxed
cursor-pointer
" "
:class="!value ? 'text-gray-600' : 'text-gray-900'"
tabindex="0" tabindex="0"
@click="toggleDropdown()" @click="toggleDropdown()"
@keydown.down="highlightItemDown" @keydown.down="highlightItemDown"

View File

@ -15,6 +15,8 @@
class="mt-4" class="mt-4"
:aspect-ratio="2.05" :aspect-ratio="2.05"
:colors="chartData.colors" :colors="chartData.colors"
:gridColor="chartData.gridColor"
:fontColor="chartData.fontColor"
:points="chartData.points" :points="chartData.points"
:x-labels="chartData.xLabels" :x-labels="chartData.xLabels"
:format="chartData.format" :format="chartData.format"
@ -23,7 +25,7 @@
:y-min="chartData.yMin" :y-min="chartData.yMin"
/> />
<div v-else class="flex-1 w-full h-full flex-center my-20"> <div v-else class="flex-1 w-full h-full flex-center my-20">
<span class="text-base text-gray-600"> <span class="text-base text-gray-600 dark:text-gray-500">
{{ t`No transactions yet` }} {{ t`No transactions yet` }}
</span> </span>
</div> </div>
@ -42,8 +44,8 @@ import SectionHeader from './SectionHeader.vue';
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
// Linting broken in this file cause of `extends: ...` // Linting broken in this file cause of `extends: ...`
/* /*
eslint-disable @typescript-eslint/no-unsafe-argument, eslint-disable @typescript-eslint/no-unsafe-argument,
@typescript-eslint/no-unsafe-return @typescript-eslint/no-unsafe-return
*/ */
export default defineComponent({ export default defineComponent({
@ -53,6 +55,9 @@ export default defineComponent({
SectionHeader, SectionHeader,
BarChart, BarChart,
}, },
props: {
darkMode: Boolean,
},
extends: DashboardChartBase, extends: DashboardChartBase,
data: () => ({ data: () => ({
data: [] as { yearmonth: string; balance: number }[], data: [] as { yearmonth: string; balance: number }[],
@ -63,7 +68,10 @@ export default defineComponent({
chartData() { chartData() {
const points = [this.data.map((d) => d.balance)]; const points = [this.data.map((d) => d.balance)];
const colors = [ const colors = [
{ positive: uicolors.blue['500'], negative: uicolors.pink['500'] }, {
positive: uicolors.blue[this.darkMode ? '600' : '500'],
negative: uicolors.pink[this.darkMode ? '600' : '500'],
},
]; ];
const format = (value: number) => fyo.format(value ?? 0, 'Currency'); const format = (value: number) => fyo.format(value ?? 0, 'Currency');
const yMax = getYMax(points); const yMax = getYMax(points);
@ -76,6 +84,9 @@ export default defineComponent({
yMax, yMax,
yMin, yMin,
formatX: formatXLabels, formatX: formatXLabels,
gridColor: this.darkMode ? 'rgba(200, 200, 200, 0.2)' : undefined,
fontColor: this.darkMode ? uicolors.gray['400'] : undefined,
zeroLineColor: this.darkMode ? uicolors.gray['400'] : undefined,
}; };
}, },
}, },

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="flex items-baseline justify-between"> <div class="flex items-baseline justify-between dark:text-white">
<span class="font-semibold text-base"><slot name="title"></slot></span> <span class="font-semibold text-base"><slot name="title"></slot></span>
<slot name="action"></slot> <slot name="action"></slot>
</div> </div>

View File

@ -14,34 +14,38 @@
<div class="flex justify-between"> <div class="flex justify-between">
<!-- Paid --> <!-- Paid -->
<div <div
class="text-sm font-medium" class="text-sm font-medium dark:text-gray-25"
:class="{ :class="{
'bg-gray-200 text-gray-200 rounded': !count, 'bg-gray-200 dark:bg-gray-700 text-gray-200 dark:text-gray-700 rounded':
!count,
'cursor-pointer': paidCount > 0, 'cursor-pointer': paidCount > 0,
}" }"
:title="paidCount > 0 ? t`View Paid Invoices` : ''" :title="paidCount > 0 ? t`View Paid Invoices` : ''"
@click="() => routeToInvoices('paid')" @click="() => routeToInvoices('paid')"
> >
{{ fyo.format(paid, 'Currency') }} {{ fyo.format(paid, 'Currency') }}
<span :class="{ 'text-gray-900 font-normal': count }">{{ <span
t`Paid` :class="{ 'text-gray-900 dark:text-gray-200 font-normal': count }"
}}</span> >{{ t`Paid` }}</span
>
</div> </div>
<!-- Unpaid --> <!-- Unpaid -->
<div <div
class="text-sm font-medium" class="text-sm font-medium dark:text-gray-25"
:class="{ :class="{
'bg-gray-200 text-gray-200 rounded': !count, 'bg-gray-200 dark:bg-gray-700 text-gray-200 dark:text-gray-700 rounded':
!count,
'cursor-pointer': unpaidCount > 0, 'cursor-pointer': unpaidCount > 0,
}" }"
:title="unpaidCount > 0 ? t`View Unpaid Invoices` : ''" :title="unpaidCount > 0 ? t`View Unpaid Invoices` : ''"
@click="() => routeToInvoices('unpaid')" @click="() => routeToInvoices('unpaid')"
> >
{{ fyo.format(unpaid, 'Currency') }} {{ fyo.format(unpaid, 'Currency') }}
<span :class="{ 'text-gray-900 font-normal': count }">{{ <span
t`Unpaid` :class="{ 'text-gray-900 dark:text-gray-200 font-normal': count }"
}}</span> >{{ t`Unpaid` }}</span
>
</div> </div>
</div> </div>
@ -64,7 +68,7 @@
:offset="15" :offset="15"
:show="show" :show="show"
placement="top" placement="top"
class="text-sm shadow-md px-2 py-1 bg-white text-gray-900 border-s-4" class="text-sm shadow-md px-2 py-1 bg-white dark:bg-gray-900 text-gray-900 dark:text-white border-s-4"
:style="{ borderColor: colors }" :style="{ borderColor: colors }"
> >
<div class="flex justify-between gap-4"> <div class="flex justify-between gap-4">
@ -110,6 +114,7 @@ export default defineComponent({
extends: BaseDashboardChart, extends: BaseDashboardChart,
props: { props: {
schemaName: { type: String as PropType<string>, required: true }, schemaName: { type: String as PropType<string>, required: true },
darkMode: Boolean,
}, },
data() { data() {
return { return {
@ -144,25 +149,24 @@ export default defineComponent({
if (this.schemaName === ModelNameEnum.SalesInvoice) { if (this.schemaName === ModelNameEnum.SalesInvoice) {
return 'blue'; return 'blue';
} }
return 'pink'; return 'pink';
}, },
colors(): string { colors(): string {
return uicolors[this.color]['500']; return uicolors[this.color][this.darkMode ? '600' : '500'];
}, },
paidColor(): string { paidColor(): string {
if (!this.hasData) { if (!this.hasData) {
return 'bg-gray-400'; return this.darkMode ? 'bg-gray-700' : 'bg-gray-400';
} }
return `bg-${this.color}-500`; return `bg-${this.color}-${this.darkMode ? '600' : '500'}`;
}, },
unpaidColor(): string { unpaidColor(): string {
if (!this.hasData) { if (!this.hasData) {
return 'bg-gray-200'; return `bg-gray-${this.darkMode ? '800' : '200'}`;
} }
return `bg-${this.color}-200`; return `bg-${this.color}-${this.darkMode ? '700 bg-opacity-20' : '200'}`;
}, },
}, },
async activated() { async activated() {

View File

@ -8,15 +8,22 @@ import { toggleSidebar } from 'src/utils/ui';
<!-- eslint-disable vue/require-explicit-emits --> <!-- eslint-disable vue/require-explicit-emits -->
<Sidebar <Sidebar
v-show="showSidebar" v-show="showSidebar"
class="flex-shrink-0 border-e whitespace-nowrap w-sidebar" class="flex-shrink-0 border-e dark:border-gray-800 whitespace-nowrap w-sidebar"
:darkMode="darkMode"
@change-db-file="$emit('change-db-file')" @change-db-file="$emit('change-db-file')"
@toggle-darkmode="$emit('toggle-darkmode')"
/> />
</Transition> </Transition>
<div class="flex flex-1 overflow-y-hidden bg-white"> <div class="flex flex-1 overflow-y-hidden bg-white dark:bg-gray-875">
<router-view v-slot="{ Component }"> <router-view v-slot="{ Component }">
<keep-alive> <keep-alive>
<component :is="Component" :key="$route.path" class="flex-1" /> <component
:is="Component"
:key="$route.path"
:darkMode="darkMode"
class="flex-1"
/>
</keep-alive> </keep-alive>
</router-view> </router-view>
@ -26,6 +33,7 @@ import { toggleSidebar } from 'src/utils/ui';
<component <component
:is="Component" :is="Component"
:key="route.query.schemaName + route.query.name" :key="route.query.schemaName + route.query.name"
:darkMode="darkMode"
/> />
</div> </div>
</Transition> </Transition>
@ -35,19 +43,7 @@ import { toggleSidebar } from 'src/utils/ui';
<!-- Show Sidebar Button --> <!-- Show Sidebar Button -->
<button <button
v-show="!showSidebar" v-show="!showSidebar"
class=" class="absolute bottom-0 start-0 text-gray-600 bg-gray-100 rounded rtl-rotate-180 p-1 m-4 opacity-0 hover:opacity-100 hover:shadow-md"
absolute
bottom-0
start-0
text-gray-600
bg-gray-100
rounded
rtl-rotate-180
p-1
m-4
opacity-0
hover:opacity-100 hover:shadow-md
"
@click="() => toggleSidebar()" @click="() => toggleSidebar()"
> >
<feather-icon name="chevrons-right" class="w-4 h-4" /> <feather-icon name="chevrons-right" class="w-4 h-4" />
@ -62,7 +58,10 @@ export default defineComponent({
components: { components: {
Sidebar, Sidebar,
}, },
emits: ['change-db-file'], props: {
darkMode: Boolean,
},
emits: ['change-db-file', 'toggle-darkmode'],
}); });
</script> </script>

View File

@ -11,6 +11,7 @@ import registerIpcRendererListeners from './renderer/registerIpcRendererListener
import router from './router'; import router from './router';
import { stringifyCircular } from './utils'; import { stringifyCircular } from './utils';
import { setLanguageMap } from './utils/language'; import { setLanguageMap } from './utils/language';
import { setDarkMode } from './utils/theme';
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
(async () => { (async () => {
@ -20,6 +21,9 @@ import { setLanguageMap } from './utils/language';
} }
fyo.store.language = language || 'English'; fyo.store.language = language || 'English';
const darkMode = fyo.config.get('darkMode') as boolean;
setDarkMode(darkMode);
registerIpcRendererListeners(); registerIpcRendererListeners();
const { isDevelopment, platform, version } = await ipc.getEnv(); const { isDevelopment, platform, version } = await ipc.getEnv();

20
src/utils/theme.ts Normal file
View File

@ -0,0 +1,20 @@
import { fyo } from 'src/initFyo';
export async function toggleDarkMode(): Promise<void> {
const darkMode = fyo.config.get('darkMode');
if (darkMode) {
document.documentElement.classList.remove('dark');
fyo.config.set('darkMode', false);
return;
}
document.documentElement.classList.add('dark');
fyo.config.set('darkMode', true);
}
export function setDarkMode(darkMode: boolean): void {
if (darkMode) {
document.documentElement.classList.add('dark');
return;
}
document.documentElement.classList.remove('dark');
}

View File

@ -4,6 +4,7 @@ const colors = JSON.parse(
); );
module.exports = { module.exports = {
darkMode: 'class',
purge: false, purge: false,
theme: { theme: {
fontFamily: { fontFamily: {
@ -66,7 +67,14 @@ module.exports = {
}, },
variants: { variants: {
margin: ['responsive', 'first', 'last', 'hover', 'focus'], margin: ['responsive', 'first', 'last', 'hover', 'focus'],
backgroundColor: ['responsive', 'first', 'hover', 'focus', 'focus-within'], backgroundColor: [
'responsive',
'first',
'hover',
'focus',
'focus-within',
'dark',
],
display: ['group-hover'], display: ['group-hover'],
}, },
plugins: [require('tailwindcss-rtl')], plugins: [require('tailwindcss-rtl')],