@pyreweb/fabric 1.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +119 -0
- package/dist/fabric.cjs.js +18109 -0
- package/dist/fabric.css +2180 -0
- package/dist/fabric.esm.js +18062 -0
- package/dist/fabric.min.js +18112 -0
- package/dist/types/components/atoms/FAvatar/FAvatar.test.d.ts +1 -0
- package/dist/types/components/atoms/FBadge/FBadge.test.d.ts +1 -0
- package/dist/types/components/atoms/FButton/FButton.test.d.ts +1 -0
- package/dist/types/components/atoms/FCheckbox/FCheckbox.test.d.ts +1 -0
- package/dist/types/components/atoms/FDivider/FDivider.test.d.ts +1 -0
- package/dist/types/components/atoms/FIcon/FIcon.test.d.ts +1 -0
- package/dist/types/components/atoms/FInput/FInput.test.d.ts +1 -0
- package/dist/types/components/atoms/FLoader/FLoader.test.d.ts +1 -0
- package/dist/types/components/atoms/FRadio/FRadio.test.d.ts +1 -0
- package/dist/types/components/atoms/FTextarea/FTextarea.test.d.ts +1 -0
- package/dist/types/components/atoms/FToggle/FToggle.test.d.ts +1 -0
- package/dist/types/components/atoms/FTypography/FTypography.test.d.ts +1 -0
- package/dist/types/components/atoms/index.d.ts +13 -0
- package/dist/types/components/molecules/FAccordionItem/FAccordionItem.test.d.ts +1 -0
- package/dist/types/components/molecules/FAlert/FAlert.test.d.ts +1 -0
- package/dist/types/components/molecules/FBreadcrumb/FBreadcrumb.test.d.ts +1 -0
- package/dist/types/components/molecules/FButtonGroup/FButtonGroup.test.d.ts +1 -0
- package/dist/types/components/molecules/FCard/FCard.test.d.ts +1 -0
- package/dist/types/components/molecules/FDatePicker/FDatePicker.test.d.ts +1 -0
- package/dist/types/components/molecules/FEmptyState/FEmptyState.test.d.ts +1 -0
- package/dist/types/components/molecules/FFilePreview/FFilePreview.test.d.ts +1 -0
- package/dist/types/components/molecules/FFormField/FFormField.test.d.ts +1 -0
- package/dist/types/components/molecules/FListItem/FListItem.test.d.ts +1 -0
- package/dist/types/components/molecules/FPagination/FPagination.test.d.ts +1 -0
- package/dist/types/components/molecules/FSearchBar/FSearchBar.test.d.ts +1 -0
- package/dist/types/components/molecules/FSelect/FSelect.test.d.ts +1 -0
- package/dist/types/components/molecules/FStatCard/FStatCard.test.d.ts +1 -0
- package/dist/types/components/molecules/FTabs/FTabs.test.d.ts +1 -0
- package/dist/types/components/molecules/FToast/FToast.test.d.ts +1 -0
- package/dist/types/components/molecules/index.d.ts +18 -0
- package/dist/types/components/organisms/FActivityFeed/FActivityFeed.test.d.ts +1 -0
- package/dist/types/components/organisms/FDataTable/FDataTable.test.d.ts +1 -0
- package/dist/types/components/organisms/FDrawer/FDrawer.test.d.ts +1 -0
- package/dist/types/components/organisms/FFileUpload/FFileUpload.test.d.ts +1 -0
- package/dist/types/components/organisms/FFilterSidebar/FFilterSidebar.test.d.ts +1 -0
- package/dist/types/components/organisms/FForm/FForm.test.d.ts +1 -0
- package/dist/types/components/organisms/FModal/FModal.test.d.ts +1 -0
- package/dist/types/components/organisms/FNavigationSidebar/FNavigationSidebar.test.d.ts +1 -0
- package/dist/types/components/organisms/FOnboardingStepper/FOnboardingStepper.test.d.ts +1 -0
- package/dist/types/components/organisms/FOnboardingStepper/FStepperProgress.test.d.ts +1 -0
- package/dist/types/components/organisms/FPageHeader/FPageHeader.test.d.ts +1 -0
- package/dist/types/components/organisms/FProfileSection/FProfileSection.test.d.ts +1 -0
- package/dist/types/components/organisms/FToastProvider/FToastProvider.test.d.ts +1 -0
- package/dist/types/components/organisms/FUserMenu/FUserMenu.test.d.ts +1 -0
- package/dist/types/components/organisms/index.d.ts +14 -0
- package/dist/types/components/utils/FThemeProvider.test.d.ts +1 -0
- package/dist/types/components/utils/index.d.ts +2 -0
- package/dist/types/components.d.ts +602 -0
- package/dist/types/composables/index.d.ts +12 -0
- package/dist/types/composables/useDataTableState.d.ts +106 -0
- package/dist/types/composables/useDataTableState.test.d.ts +1 -0
- package/dist/types/composables/useFormValidation.d.ts +49 -0
- package/dist/types/composables/useFormValidation.test.d.ts +1 -0
- package/dist/types/composables/useSidebarState.d.ts +65 -0
- package/dist/types/composables/useSidebarState.test.d.ts +1 -0
- package/dist/types/index.d.ts +19 -0
- package/dist/types/types.d.ts +529 -0
- package/package.json +100 -0
- package/src/components/atoms/FAvatar/FAvatar.stories.js +100 -0
- package/src/components/atoms/FAvatar/FAvatar.test.ts +95 -0
- package/src/components/atoms/FAvatar/FAvatar.vue +190 -0
- package/src/components/atoms/FBadge/FBadge.stories.js +129 -0
- package/src/components/atoms/FBadge/FBadge.test.ts +93 -0
- package/src/components/atoms/FBadge/FBadge.vue +103 -0
- package/src/components/atoms/FButton/FButton.stories.js +122 -0
- package/src/components/atoms/FButton/FButton.test.ts +98 -0
- package/src/components/atoms/FButton/FButton.vue +147 -0
- package/src/components/atoms/FCheckbox/FCheckbox.stories.js +96 -0
- package/src/components/atoms/FCheckbox/FCheckbox.test.ts +64 -0
- package/src/components/atoms/FCheckbox/FCheckbox.vue +76 -0
- package/src/components/atoms/FDivider/FDivider.stories.js +104 -0
- package/src/components/atoms/FDivider/FDivider.test.ts +80 -0
- package/src/components/atoms/FDivider/FDivider.vue +117 -0
- package/src/components/atoms/FIcon/FIcon.stories.js +189 -0
- package/src/components/atoms/FIcon/FIcon.test.ts +99 -0
- package/src/components/atoms/FIcon/FIcon.vue +192 -0
- package/src/components/atoms/FInput/FInput.stories.js +119 -0
- package/src/components/atoms/FInput/FInput.test.ts +79 -0
- package/src/components/atoms/FInput/FInput.vue +88 -0
- package/src/components/atoms/FLoader/FLoader.stories.js +109 -0
- package/src/components/atoms/FLoader/FLoader.test.ts +66 -0
- package/src/components/atoms/FLoader/FLoader.vue +97 -0
- package/src/components/atoms/FRadio/FRadio.stories.js +105 -0
- package/src/components/atoms/FRadio/FRadio.test.ts +75 -0
- package/src/components/atoms/FRadio/FRadio.vue +119 -0
- package/src/components/atoms/FTextarea/FTextarea.stories.js +126 -0
- package/src/components/atoms/FTextarea/FTextarea.test.ts +94 -0
- package/src/components/atoms/FTextarea/FTextarea.vue +156 -0
- package/src/components/atoms/FToggle/FToggle.stories.js +108 -0
- package/src/components/atoms/FToggle/FToggle.test.ts +96 -0
- package/src/components/atoms/FToggle/FToggle.vue +123 -0
- package/src/components/atoms/FTypography/FTypography.stories.js +127 -0
- package/src/components/atoms/FTypography/FTypography.test.ts +93 -0
- package/src/components/atoms/FTypography/FTypography.vue +78 -0
- package/src/components/atoms/index.ts +27 -0
- package/src/components/molecules/FAccordionItem/FAccordionItem.stories.js +71 -0
- package/src/components/molecules/FAccordionItem/FAccordionItem.test.ts +61 -0
- package/src/components/molecules/FAccordionItem/FAccordionItem.vue +105 -0
- package/src/components/molecules/FAlert/FAlert.stories.js +87 -0
- package/src/components/molecules/FAlert/FAlert.test.ts +59 -0
- package/src/components/molecules/FAlert/FAlert.vue +108 -0
- package/src/components/molecules/FBreadcrumb/FBreadcrumb.stories.js +90 -0
- package/src/components/molecules/FBreadcrumb/FBreadcrumb.test.ts +76 -0
- package/src/components/molecules/FBreadcrumb/FBreadcrumb.vue +117 -0
- package/src/components/molecules/FButtonGroup/FButtonGroup.stories.js +82 -0
- package/src/components/molecules/FButtonGroup/FButtonGroup.test.ts +44 -0
- package/src/components/molecules/FButtonGroup/FButtonGroup.vue +31 -0
- package/src/components/molecules/FCard/FCard.stories.js +136 -0
- package/src/components/molecules/FCard/FCard.test.ts +87 -0
- package/src/components/molecules/FCard/FCard.vue +75 -0
- package/src/components/molecules/FDatePicker/FDatePicker.stories.js +305 -0
- package/src/components/molecules/FDatePicker/FDatePicker.test.ts +282 -0
- package/src/components/molecules/FDatePicker/FDatePicker.vue +750 -0
- package/src/components/molecules/FEmptyState/FEmptyState.stories.js +98 -0
- package/src/components/molecules/FEmptyState/FEmptyState.test.ts +82 -0
- package/src/components/molecules/FEmptyState/FEmptyState.vue +89 -0
- package/src/components/molecules/FFilePreview/FFilePreview.stories.js +130 -0
- package/src/components/molecules/FFilePreview/FFilePreview.test.ts +70 -0
- package/src/components/molecules/FFilePreview/FFilePreview.vue +125 -0
- package/src/components/molecules/FFormField/FFormField.stories.js +149 -0
- package/src/components/molecules/FFormField/FFormField.test.ts +85 -0
- package/src/components/molecules/FFormField/FFormField.vue +107 -0
- package/src/components/molecules/FListItem/FListItem.stories.js +158 -0
- package/src/components/molecules/FListItem/FListItem.test.ts +93 -0
- package/src/components/molecules/FListItem/FListItem.vue +113 -0
- package/src/components/molecules/FPagination/FPagination.stories.js +132 -0
- package/src/components/molecules/FPagination/FPagination.test.ts +79 -0
- package/src/components/molecules/FPagination/FPagination.vue +206 -0
- package/src/components/molecules/FSearchBar/FSearchBar.stories.js +129 -0
- package/src/components/molecules/FSearchBar/FSearchBar.test.ts +81 -0
- package/src/components/molecules/FSearchBar/FSearchBar.vue +180 -0
- package/src/components/molecules/FSelect/FSelect.stories.js +333 -0
- package/src/components/molecules/FSelect/FSelect.test.ts +478 -0
- package/src/components/molecules/FSelect/FSelect.vue +551 -0
- package/src/components/molecules/FStatCard/FStatCard.stories.js +144 -0
- package/src/components/molecules/FStatCard/FStatCard.test.ts +78 -0
- package/src/components/molecules/FStatCard/FStatCard.vue +106 -0
- package/src/components/molecules/FTabs/FTab.vue +63 -0
- package/src/components/molecules/FTabs/FTabs.stories.js +277 -0
- package/src/components/molecules/FTabs/FTabs.test.ts +264 -0
- package/src/components/molecules/FTabs/FTabs.vue +273 -0
- package/src/components/molecules/FToast/FToast.stories.js +150 -0
- package/src/components/molecules/FToast/FToast.test.ts +157 -0
- package/src/components/molecules/FToast/FToast.vue +283 -0
- package/src/components/molecules/index.ts +37 -0
- package/src/components/organisms/FActivityFeed/FActivityFeed.stories.js +217 -0
- package/src/components/organisms/FActivityFeed/FActivityFeed.test.ts +134 -0
- package/src/components/organisms/FActivityFeed/FActivityFeed.vue +589 -0
- package/src/components/organisms/FDataTable/FDataTable.stories.js +370 -0
- package/src/components/organisms/FDataTable/FDataTable.test.ts +248 -0
- package/src/components/organisms/FDataTable/FDataTable.vue +808 -0
- package/src/components/organisms/FDrawer/FDrawer.stories.js +296 -0
- package/src/components/organisms/FDrawer/FDrawer.test.ts +142 -0
- package/src/components/organisms/FDrawer/FDrawer.vue +303 -0
- package/src/components/organisms/FFileUpload/FFileUpload.stories.js +162 -0
- package/src/components/organisms/FFileUpload/FFileUpload.test.ts +103 -0
- package/src/components/organisms/FFileUpload/FFileUpload.vue +616 -0
- package/src/components/organisms/FFilterSidebar/FFilterSidebar.stories.js +161 -0
- package/src/components/organisms/FFilterSidebar/FFilterSidebar.test.ts +92 -0
- package/src/components/organisms/FFilterSidebar/FFilterSidebar.vue +458 -0
- package/src/components/organisms/FForm/FForm.stories.js +270 -0
- package/src/components/organisms/FForm/FForm.test.ts +63 -0
- package/src/components/organisms/FForm/FForm.vue +19 -0
- package/src/components/organisms/FModal/FModal.stories.js +227 -0
- package/src/components/organisms/FModal/FModal.test.ts +181 -0
- package/src/components/organisms/FModal/FModal.vue +319 -0
- package/src/components/organisms/FNavigationSidebar/FNavigationSidebar.stories.js +176 -0
- package/src/components/organisms/FNavigationSidebar/FNavigationSidebar.test.ts +95 -0
- package/src/components/organisms/FNavigationSidebar/FNavigationSidebar.vue +577 -0
- package/src/components/organisms/FOnboardingStepper/FOnboardingStepper.stories.js +197 -0
- package/src/components/organisms/FOnboardingStepper/FOnboardingStepper.test.ts +114 -0
- package/src/components/organisms/FOnboardingStepper/FOnboardingStepper.vue +212 -0
- package/src/components/organisms/FOnboardingStepper/FStepperProgress.stories.js +122 -0
- package/src/components/organisms/FOnboardingStepper/FStepperProgress.test.ts +130 -0
- package/src/components/organisms/FOnboardingStepper/FStepperProgress.vue +146 -0
- package/src/components/organisms/FPageHeader/FPageHeader.stories.js +142 -0
- package/src/components/organisms/FPageHeader/FPageHeader.test.ts +83 -0
- package/src/components/organisms/FPageHeader/FPageHeader.vue +241 -0
- package/src/components/organisms/FProfileSection/FProfileSection.stories.js +190 -0
- package/src/components/organisms/FProfileSection/FProfileSection.test.ts +85 -0
- package/src/components/organisms/FProfileSection/FProfileSection.vue +562 -0
- package/src/components/organisms/FToastProvider/FToastProvider.stories.js +290 -0
- package/src/components/organisms/FToastProvider/FToastProvider.test.ts +215 -0
- package/src/components/organisms/FToastProvider/FToastProvider.vue +214 -0
- package/src/components/organisms/FUserMenu/FUserMenu.stories.js +170 -0
- package/src/components/organisms/FUserMenu/FUserMenu.test.ts +102 -0
- package/src/components/organisms/FUserMenu/FUserMenu.vue +407 -0
- package/src/components/organisms/index.ts +29 -0
- package/src/components/utils/FThemeProvider.stories.js +236 -0
- package/src/components/utils/FThemeProvider.test.ts +244 -0
- package/src/components/utils/FThemeProvider.vue +191 -0
- package/src/components/utils/index.ts +3 -0
- package/src/components.d.ts +602 -0
- package/src/composables/README.md +233 -0
- package/src/composables/index.ts +25 -0
- package/src/composables/useDataTableState.test.ts +378 -0
- package/src/composables/useDataTableState.ts +361 -0
- package/src/composables/useFormValidation.test.ts +198 -0
- package/src/composables/useFormValidation.ts +178 -0
- package/src/composables/useSidebarState.test.ts +307 -0
- package/src/composables/useSidebarState.ts +201 -0
- package/src/env.d.ts +14 -0
- package/src/index.ts +167 -0
- package/src/styles/tailwind.css +173 -0
- package/src/types.ts +740 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import FThemeProvider from './FThemeProvider.vue';
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
title: 'Utils/FThemeProvider',
|
|
5
|
+
component: FThemeProvider,
|
|
6
|
+
argTypes: {
|
|
7
|
+
defaultTheme: {
|
|
8
|
+
control: { type: 'select' },
|
|
9
|
+
options: ['light', 'dark', 'auto'],
|
|
10
|
+
description: 'Default theme to use when no preference is stored'
|
|
11
|
+
},
|
|
12
|
+
storageKey: {
|
|
13
|
+
control: { type: 'text' },
|
|
14
|
+
description: 'Key used for localStorage persistence'
|
|
15
|
+
},
|
|
16
|
+
enablePersistence: {
|
|
17
|
+
control: { type: 'boolean' },
|
|
18
|
+
description: 'Enable or disable localStorage persistence'
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const Template = (args, { argTypes }) => ({
|
|
24
|
+
components: { FThemeProvider },
|
|
25
|
+
props: Object.keys(argTypes),
|
|
26
|
+
template: `
|
|
27
|
+
<f-theme-provider v-bind="$props" v-slot="{ theme, toggleTheme, setTheme }">
|
|
28
|
+
<div class="min-h-screen p-8 transition-colors duration-300"
|
|
29
|
+
:style="{
|
|
30
|
+
backgroundColor: 'var(--theme-background)',
|
|
31
|
+
color: 'var(--theme-foreground)'
|
|
32
|
+
}">
|
|
33
|
+
<div class="max-w-4xl mx-auto space-y-6">
|
|
34
|
+
<!-- Theme Controls -->
|
|
35
|
+
<div class="flex gap-4 mb-8">
|
|
36
|
+
<button
|
|
37
|
+
@click="toggleTheme"
|
|
38
|
+
class="px-4 py-2 rounded-lg font-medium transition-colors"
|
|
39
|
+
:style="{
|
|
40
|
+
backgroundColor: 'var(--theme-primary)',
|
|
41
|
+
color: 'var(--theme-primary-foreground)'
|
|
42
|
+
}">
|
|
43
|
+
Basculer le Thème
|
|
44
|
+
</button>
|
|
45
|
+
<button
|
|
46
|
+
@click="setTheme('light')"
|
|
47
|
+
class="px-4 py-2 rounded-lg border transition-colors"
|
|
48
|
+
:style="{
|
|
49
|
+
borderColor: 'var(--theme-border)',
|
|
50
|
+
backgroundColor: theme === 'light' ? 'var(--theme-primary)' : 'var(--theme-card)',
|
|
51
|
+
color: theme === 'light' ? 'var(--theme-primary-foreground)' : 'var(--theme-foreground)'
|
|
52
|
+
}">
|
|
53
|
+
Mode Clair
|
|
54
|
+
</button>
|
|
55
|
+
<button
|
|
56
|
+
@click="setTheme('dark')"
|
|
57
|
+
class="px-4 py-2 rounded-lg border transition-colors"
|
|
58
|
+
:style="{
|
|
59
|
+
borderColor: 'var(--theme-border)',
|
|
60
|
+
backgroundColor: theme === 'dark' ? 'var(--theme-primary)' : 'var(--theme-card)',
|
|
61
|
+
color: theme === 'dark' ? 'var(--theme-primary-foreground)' : 'var(--theme-foreground)'
|
|
62
|
+
}">
|
|
63
|
+
Mode Sombre
|
|
64
|
+
</button>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<!-- Current Theme Display -->
|
|
68
|
+
<div class="p-4 rounded-lg border"
|
|
69
|
+
:style="{
|
|
70
|
+
backgroundColor: 'var(--theme-card)',
|
|
71
|
+
borderColor: 'var(--theme-border)'
|
|
72
|
+
}">
|
|
73
|
+
<h3 class="text-lg font-semibold mb-2">
|
|
74
|
+
Thème actuel : <strong>{{ theme }}</strong>
|
|
75
|
+
</h3>
|
|
76
|
+
<p :style="{ color: 'var(--theme-muted-foreground)' }">
|
|
77
|
+
Le thème est automatiquement persisté dans localStorage et appliqué à tous les composants.
|
|
78
|
+
</p>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<!-- Color Palette Demo -->
|
|
82
|
+
<div class="space-y-4">
|
|
83
|
+
<h2 class="text-2xl font-bold">Palette de Couleurs</h2>
|
|
84
|
+
|
|
85
|
+
<!-- Primary Colors -->
|
|
86
|
+
<div>
|
|
87
|
+
<h3 class="text-lg font-semibold mb-2">Primaire</h3>
|
|
88
|
+
<div class="flex gap-2 flex-wrap">
|
|
89
|
+
<div v-for="shade in [50, 100, 200, 300, 400, 500, 600, 700, 800, 900]"
|
|
90
|
+
:key="shade"
|
|
91
|
+
class="w-16 h-16 rounded flex items-center justify-center text-xs font-bold"
|
|
92
|
+
:style="{
|
|
93
|
+
backgroundColor: 'var(--color-primary-' + shade + ')',
|
|
94
|
+
color: shade >= 500 ? 'white' : 'black'
|
|
95
|
+
}">
|
|
96
|
+
{{ shade }}
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
<!-- Success Colors -->
|
|
102
|
+
<div>
|
|
103
|
+
<h3 class="text-lg font-semibold mb-2">Succès</h3>
|
|
104
|
+
<div class="flex gap-2 flex-wrap">
|
|
105
|
+
<div v-for="shade in [50, 100, 200, 300, 400, 500, 600, 700, 800, 900]"
|
|
106
|
+
:key="shade"
|
|
107
|
+
class="w-16 h-16 rounded flex items-center justify-center text-xs font-bold"
|
|
108
|
+
:style="{
|
|
109
|
+
backgroundColor: 'var(--color-success-' + shade + ')',
|
|
110
|
+
color: shade >= 500 ? 'white' : 'black'
|
|
111
|
+
}">
|
|
112
|
+
{{ shade }}
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
<!-- Danger Colors -->
|
|
118
|
+
<div>
|
|
119
|
+
<h3 class="text-lg font-semibold mb-2">Danger</h3>
|
|
120
|
+
<div class="flex gap-2 flex-wrap">
|
|
121
|
+
<div v-for="shade in [50, 100, 200, 300, 400, 500, 600, 700, 800, 900]"
|
|
122
|
+
:key="shade"
|
|
123
|
+
class="w-16 h-16 rounded flex items-center justify-center text-xs font-bold"
|
|
124
|
+
:style="{
|
|
125
|
+
backgroundColor: 'var(--color-danger-' + shade + ')',
|
|
126
|
+
color: shade >= 500 ? 'white' : 'black'
|
|
127
|
+
}">
|
|
128
|
+
{{ shade }}
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
<!-- Warning Colors -->
|
|
134
|
+
<div>
|
|
135
|
+
<h3 class="text-lg font-semibold mb-2">Avertissement</h3>
|
|
136
|
+
<div class="flex gap-2 flex-wrap">
|
|
137
|
+
<div v-for="shade in [50, 100, 200, 300, 400, 500, 600, 700, 800, 900]"
|
|
138
|
+
:key="shade"
|
|
139
|
+
class="w-16 h-16 rounded flex items-center justify-center text-xs font-bold"
|
|
140
|
+
:style="{
|
|
141
|
+
backgroundColor: 'var(--color-warning-' + shade + ')',
|
|
142
|
+
color: shade >= 500 ? 'white' : 'black'
|
|
143
|
+
}">
|
|
144
|
+
{{ shade }}
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
<!-- Semantic Theme Variables -->
|
|
151
|
+
<div class="space-y-4 mt-8">
|
|
152
|
+
<h2 class="text-2xl font-bold">Variables Thématiques Sémantiques</h2>
|
|
153
|
+
<div class="grid grid-cols-2 gap-4">
|
|
154
|
+
<div class="p-4 rounded-lg" :style="{ backgroundColor: 'var(--theme-card)', border: '1px solid var(--theme-border)' }">
|
|
155
|
+
<p class="font-mono text-sm mb-2">--theme-background</p>
|
|
156
|
+
<div class="w-full h-12 rounded" :style="{ backgroundColor: 'var(--theme-background)', border: '1px solid var(--theme-border)' }"></div>
|
|
157
|
+
</div>
|
|
158
|
+
<div class="p-4 rounded-lg" :style="{ backgroundColor: 'var(--theme-card)', border: '1px solid var(--theme-border)' }">
|
|
159
|
+
<p class="font-mono text-sm mb-2">--theme-foreground</p>
|
|
160
|
+
<div class="w-full h-12 rounded" :style="{ backgroundColor: 'var(--theme-foreground)' }"></div>
|
|
161
|
+
</div>
|
|
162
|
+
<div class="p-4 rounded-lg" :style="{ backgroundColor: 'var(--theme-card)', border: '1px solid var(--theme-border)' }">
|
|
163
|
+
<p class="font-mono text-sm mb-2">--theme-card</p>
|
|
164
|
+
<div class="w-full h-12 rounded" :style="{ backgroundColor: 'var(--theme-card)', border: '1px solid var(--theme-border)' }"></div>
|
|
165
|
+
</div>
|
|
166
|
+
<div class="p-4 rounded-lg" :style="{ backgroundColor: 'var(--theme-card)', border: '1px solid var(--theme-border)' }">
|
|
167
|
+
<p class="font-mono text-sm mb-2">--theme-muted</p>
|
|
168
|
+
<div class="w-full h-12 rounded" :style="{ backgroundColor: 'var(--theme-muted)' }"></div>
|
|
169
|
+
</div>
|
|
170
|
+
<div class="p-4 rounded-lg" :style="{ backgroundColor: 'var(--theme-card)', border: '1px solid var(--theme-border)' }">
|
|
171
|
+
<p class="font-mono text-sm mb-2">--theme-primary</p>
|
|
172
|
+
<div class="w-full h-12 rounded" :style="{ backgroundColor: 'var(--theme-primary)' }"></div>
|
|
173
|
+
</div>
|
|
174
|
+
<div class="p-4 rounded-lg" :style="{ backgroundColor: 'var(--theme-card)', border: '1px solid var(--theme-border)' }">
|
|
175
|
+
<p class="font-mono text-sm mb-2">--theme-border</p>
|
|
176
|
+
<div class="w-full h-12 rounded border-4" :style="{ borderColor: 'var(--theme-border)' }"></div>
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
<!-- Example Cards -->
|
|
182
|
+
<div class="space-y-4 mt-8">
|
|
183
|
+
<h2 class="text-2xl font-bold">Exemples de Cartes</h2>
|
|
184
|
+
<div class="grid grid-cols-3 gap-4">
|
|
185
|
+
<div class="p-6 rounded-lg" :style="{ backgroundColor: 'var(--theme-card)', border: '1px solid var(--theme-border)' }">
|
|
186
|
+
<h3 class="text-lg font-semibold mb-2">Carte 1</h3>
|
|
187
|
+
<p :style="{ color: 'var(--theme-muted-foreground)' }">
|
|
188
|
+
Contenu de la première carte avec un style thématique.
|
|
189
|
+
</p>
|
|
190
|
+
</div>
|
|
191
|
+
<div class="p-6 rounded-lg" :style="{ backgroundColor: 'var(--theme-card)', border: '1px solid var(--theme-border)' }">
|
|
192
|
+
<h3 class="text-lg font-semibold mb-2">Carte 2</h3>
|
|
193
|
+
<p :style="{ color: 'var(--theme-muted-foreground)' }">
|
|
194
|
+
Contenu de la deuxième carte.
|
|
195
|
+
</p>
|
|
196
|
+
</div>
|
|
197
|
+
<div class="p-6 rounded-lg" :style="{ backgroundColor: 'var(--theme-card)', border: '1px solid var(--theme-border)' }">
|
|
198
|
+
<h3 class="text-lg font-semibold mb-2">Carte 3</h3>
|
|
199
|
+
<p :style="{ color: 'var(--theme-muted-foreground)' }">
|
|
200
|
+
Contenu de la troisième carte.
|
|
201
|
+
</p>
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
</f-theme-provider>
|
|
208
|
+
`
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
export const Default = Template.bind({});
|
|
212
|
+
Default.args = {
|
|
213
|
+
defaultTheme: 'light',
|
|
214
|
+
storageKey: 'fabric-theme',
|
|
215
|
+
enablePersistence: true
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
export const DarkMode = Template.bind({});
|
|
219
|
+
DarkMode.args = {
|
|
220
|
+
defaultTheme: 'dark',
|
|
221
|
+
storageKey: 'fabric-theme',
|
|
222
|
+
enablePersistence: true
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
export const AutoMode = Template.bind({});
|
|
226
|
+
AutoMode.args = {
|
|
227
|
+
defaultTheme: 'auto',
|
|
228
|
+
storageKey: 'fabric-theme',
|
|
229
|
+
enablePersistence: true
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
export const NoPersistence = Template.bind({});
|
|
233
|
+
NoPersistence.args = {
|
|
234
|
+
defaultTheme: 'light',
|
|
235
|
+
enablePersistence: false
|
|
236
|
+
};
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { mount, createLocalVue } from '@vue/test-utils';
|
|
2
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
3
|
+
import FThemeProvider from './FThemeProvider.vue';
|
|
4
|
+
|
|
5
|
+
const localVue = createLocalVue();
|
|
6
|
+
|
|
7
|
+
describe('FThemeProvider', () => {
|
|
8
|
+
let wrapper;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
// Clear localStorage before each test
|
|
12
|
+
localStorage.clear();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
if (wrapper) {
|
|
17
|
+
wrapper.destroy();
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('renders with default light theme', () => {
|
|
22
|
+
wrapper = mount(FThemeProvider, {
|
|
23
|
+
localVue,
|
|
24
|
+
slots: {
|
|
25
|
+
default: '<div>Content</div>'
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
expect(wrapper.attributes('data-theme')).toBe('light');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('renders with dark theme when specified', () => {
|
|
33
|
+
wrapper = mount(FThemeProvider, {
|
|
34
|
+
localVue,
|
|
35
|
+
propsData: {
|
|
36
|
+
defaultTheme: 'dark'
|
|
37
|
+
},
|
|
38
|
+
slots: {
|
|
39
|
+
default: '<div>Content</div>'
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
expect(wrapper.attributes('data-theme')).toBe('dark');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('exposes theme, toggleTheme, and setTheme via scoped slot', () => {
|
|
47
|
+
wrapper = mount(FThemeProvider, {
|
|
48
|
+
localVue,
|
|
49
|
+
scopedSlots: {
|
|
50
|
+
default: function (props) {
|
|
51
|
+
return this.$createElement('div', [
|
|
52
|
+
this.$createElement(
|
|
53
|
+
'span',
|
|
54
|
+
{ attrs: { id: 'theme' } },
|
|
55
|
+
props.theme
|
|
56
|
+
),
|
|
57
|
+
this.$createElement('button', {
|
|
58
|
+
attrs: { id: 'toggle' },
|
|
59
|
+
on: { click: props.toggleTheme }
|
|
60
|
+
}),
|
|
61
|
+
this.$createElement('button', {
|
|
62
|
+
attrs: { id: 'set-dark' },
|
|
63
|
+
on: {
|
|
64
|
+
click: () => {
|
|
65
|
+
props.setTheme('dark');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
]);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
expect(wrapper.find('#theme').text()).toBe('light');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('toggles theme when toggleTheme is called', async () => {
|
|
78
|
+
wrapper = mount(FThemeProvider, {
|
|
79
|
+
localVue,
|
|
80
|
+
scopedSlots: {
|
|
81
|
+
default: function (props) {
|
|
82
|
+
return this.$createElement('div', [
|
|
83
|
+
this.$createElement('button', {
|
|
84
|
+
attrs: { id: 'toggle' },
|
|
85
|
+
on: { click: props.toggleTheme }
|
|
86
|
+
})
|
|
87
|
+
]);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
expect(wrapper.attributes('data-theme')).toBe('light');
|
|
93
|
+
|
|
94
|
+
await wrapper.find('#toggle').trigger('click');
|
|
95
|
+
await wrapper.vm.$nextTick();
|
|
96
|
+
|
|
97
|
+
expect(wrapper.attributes('data-theme')).toBe('dark');
|
|
98
|
+
|
|
99
|
+
await wrapper.find('#toggle').trigger('click');
|
|
100
|
+
await wrapper.vm.$nextTick();
|
|
101
|
+
|
|
102
|
+
expect(wrapper.attributes('data-theme')).toBe('light');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('sets specific theme when setTheme is called', async () => {
|
|
106
|
+
wrapper = mount(FThemeProvider, {
|
|
107
|
+
localVue,
|
|
108
|
+
scopedSlots: {
|
|
109
|
+
default: function (props) {
|
|
110
|
+
return this.$createElement('div', [
|
|
111
|
+
this.$createElement('button', {
|
|
112
|
+
attrs: { id: 'set-dark' },
|
|
113
|
+
on: {
|
|
114
|
+
click: () => {
|
|
115
|
+
props.setTheme('dark');
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
]);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
expect(wrapper.attributes('data-theme')).toBe('light');
|
|
125
|
+
|
|
126
|
+
await wrapper.find('#set-dark').trigger('click');
|
|
127
|
+
await wrapper.vm.$nextTick();
|
|
128
|
+
|
|
129
|
+
expect(wrapper.attributes('data-theme')).toBe('dark');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('persists theme to localStorage when enablePersistence is true', async () => {
|
|
133
|
+
wrapper = mount(FThemeProvider, {
|
|
134
|
+
localVue,
|
|
135
|
+
propsData: {
|
|
136
|
+
enablePersistence: true,
|
|
137
|
+
storageKey: 'test-theme'
|
|
138
|
+
},
|
|
139
|
+
scopedSlots: {
|
|
140
|
+
default: function (props) {
|
|
141
|
+
return this.$createElement('button', {
|
|
142
|
+
attrs: { id: 'toggle' },
|
|
143
|
+
on: { click: props.toggleTheme }
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
await wrapper.find('#toggle').trigger('click');
|
|
150
|
+
await wrapper.vm.$nextTick();
|
|
151
|
+
|
|
152
|
+
expect(localStorage.getItem('test-theme')).toBe('dark');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('does not persist theme when enablePersistence is false', async () => {
|
|
156
|
+
wrapper = mount(FThemeProvider, {
|
|
157
|
+
localVue,
|
|
158
|
+
propsData: {
|
|
159
|
+
enablePersistence: false,
|
|
160
|
+
storageKey: 'test-theme'
|
|
161
|
+
},
|
|
162
|
+
scopedSlots: {
|
|
163
|
+
default: function (props) {
|
|
164
|
+
return this.$createElement('button', {
|
|
165
|
+
attrs: { id: 'toggle' },
|
|
166
|
+
on: { click: props.toggleTheme }
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
await wrapper.find('#toggle').trigger('click');
|
|
173
|
+
await wrapper.vm.$nextTick();
|
|
174
|
+
|
|
175
|
+
expect(localStorage.getItem('test-theme')).toBeNull();
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('loads theme from localStorage on mount', () => {
|
|
179
|
+
localStorage.setItem('fabric-theme', 'dark');
|
|
180
|
+
|
|
181
|
+
wrapper = mount(FThemeProvider, {
|
|
182
|
+
localVue,
|
|
183
|
+
propsData: {
|
|
184
|
+
enablePersistence: true
|
|
185
|
+
},
|
|
186
|
+
slots: {
|
|
187
|
+
default: '<div>Content</div>'
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
expect(wrapper.attributes('data-theme')).toBe('dark');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('emits theme-change event when theme changes', async () => {
|
|
195
|
+
wrapper = mount(FThemeProvider, {
|
|
196
|
+
localVue,
|
|
197
|
+
scopedSlots: {
|
|
198
|
+
default: function (props) {
|
|
199
|
+
return this.$createElement('button', {
|
|
200
|
+
attrs: { id: 'toggle' },
|
|
201
|
+
on: { click: props.toggleTheme }
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
await wrapper.find('#toggle').trigger('click');
|
|
208
|
+
await wrapper.vm.$nextTick();
|
|
209
|
+
|
|
210
|
+
expect(wrapper.emitted('theme-change')).toBeTruthy();
|
|
211
|
+
const emitted = wrapper.emitted('theme-change');
|
|
212
|
+
if (emitted) {
|
|
213
|
+
// First emission is from initialization (light), second is from toggle (dark)
|
|
214
|
+
expect(emitted.length).toBeGreaterThanOrEqual(2);
|
|
215
|
+
expect(emitted[1][0]).toBe('dark');
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('handles auto theme based on system preference', () => {
|
|
220
|
+
// Mock matchMedia to return dark preference
|
|
221
|
+
window.matchMedia = (query: string) => ({
|
|
222
|
+
matches: query === '(prefers-color-scheme: dark)',
|
|
223
|
+
media: query,
|
|
224
|
+
onchange: null,
|
|
225
|
+
addListener: () => {},
|
|
226
|
+
removeListener: () => {},
|
|
227
|
+
addEventListener: () => {},
|
|
228
|
+
removeEventListener: () => {},
|
|
229
|
+
dispatchEvent: () => true
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
wrapper = mount(FThemeProvider, {
|
|
233
|
+
localVue,
|
|
234
|
+
propsData: {
|
|
235
|
+
defaultTheme: 'auto'
|
|
236
|
+
},
|
|
237
|
+
slots: {
|
|
238
|
+
default: '<div>Content</div>'
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
expect(wrapper.attributes('data-theme')).toBe('dark');
|
|
243
|
+
});
|
|
244
|
+
});
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div :data-theme="currentTheme">
|
|
3
|
+
<slot
|
|
4
|
+
:theme="currentTheme"
|
|
5
|
+
:toggle-theme="toggleTheme"
|
|
6
|
+
:set-theme="setTheme"
|
|
7
|
+
/>
|
|
8
|
+
</div>
|
|
9
|
+
</template>
|
|
10
|
+
|
|
11
|
+
<script>
|
|
12
|
+
/**
|
|
13
|
+
* FThemeProvider - Theme management component for Fabric Design System
|
|
14
|
+
*
|
|
15
|
+
* This component provides theme switching functionality (light/dark mode)
|
|
16
|
+
* and manages theme persistence using localStorage.
|
|
17
|
+
*
|
|
18
|
+
* See the Storybook stories for usage examples.
|
|
19
|
+
*/
|
|
20
|
+
export default {
|
|
21
|
+
name: 'FThemeProvider',
|
|
22
|
+
provide() {
|
|
23
|
+
return {
|
|
24
|
+
theme: () => this.currentTheme,
|
|
25
|
+
toggleTheme: this.toggleTheme,
|
|
26
|
+
setTheme: this.setTheme
|
|
27
|
+
};
|
|
28
|
+
},
|
|
29
|
+
props: {
|
|
30
|
+
/**
|
|
31
|
+
* Default theme to use when no preference is stored
|
|
32
|
+
* @type {'light' | 'dark' | 'auto'}
|
|
33
|
+
* @default 'light'
|
|
34
|
+
*/
|
|
35
|
+
defaultTheme: {
|
|
36
|
+
type: String,
|
|
37
|
+
default: 'light',
|
|
38
|
+
validator: (value) => ['light', 'dark', 'auto'].includes(value)
|
|
39
|
+
},
|
|
40
|
+
/**
|
|
41
|
+
* Key used for localStorage persistence
|
|
42
|
+
* @type {string}
|
|
43
|
+
* @default 'fabric-theme'
|
|
44
|
+
*/
|
|
45
|
+
storageKey: {
|
|
46
|
+
type: String,
|
|
47
|
+
default: 'fabric-theme'
|
|
48
|
+
},
|
|
49
|
+
/**
|
|
50
|
+
* Enable or disable localStorage persistence
|
|
51
|
+
* @type {boolean}
|
|
52
|
+
* @default true
|
|
53
|
+
*/
|
|
54
|
+
enablePersistence: {
|
|
55
|
+
type: Boolean,
|
|
56
|
+
default: true
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
data() {
|
|
60
|
+
return {
|
|
61
|
+
currentTheme: this.defaultTheme === 'light' ? 'light' : 'dark',
|
|
62
|
+
storedTheme: null,
|
|
63
|
+
mediaQuery: null
|
|
64
|
+
};
|
|
65
|
+
},
|
|
66
|
+
created() {
|
|
67
|
+
this.initializeTheme();
|
|
68
|
+
},
|
|
69
|
+
mounted() {
|
|
70
|
+
this.setupMediaQuery();
|
|
71
|
+
},
|
|
72
|
+
beforeDestroy() {
|
|
73
|
+
if (this.mediaQuery) {
|
|
74
|
+
this.mediaQuery.removeEventListener(
|
|
75
|
+
'change',
|
|
76
|
+
this.handleMediaQueryChange
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
methods: {
|
|
81
|
+
/**
|
|
82
|
+
* Initialize theme from localStorage or system preference
|
|
83
|
+
*/
|
|
84
|
+
initializeTheme() {
|
|
85
|
+
let theme = this.defaultTheme;
|
|
86
|
+
|
|
87
|
+
// Try to get theme from localStorage if persistence is enabled
|
|
88
|
+
if (this.enablePersistence && typeof window !== 'undefined') {
|
|
89
|
+
try {
|
|
90
|
+
const stored = localStorage.getItem(this.storageKey);
|
|
91
|
+
if (stored && ['light', 'dark', 'auto'].includes(stored)) {
|
|
92
|
+
theme = stored;
|
|
93
|
+
this.storedTheme = stored;
|
|
94
|
+
}
|
|
95
|
+
} catch (error) {
|
|
96
|
+
console.warn('Failed to read theme from localStorage:', error);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
this.applyTheme(theme);
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Setup media query listener for auto theme
|
|
105
|
+
*/
|
|
106
|
+
setupMediaQuery() {
|
|
107
|
+
if (typeof window === 'undefined') return;
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
this.mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
111
|
+
this.mediaQuery.addEventListener('change', this.handleMediaQueryChange);
|
|
112
|
+
} catch (error) {
|
|
113
|
+
console.warn('Failed to setup media query listener:', error);
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Handle media query changes (for auto theme)
|
|
119
|
+
*/
|
|
120
|
+
handleMediaQueryChange() {
|
|
121
|
+
// Only re-apply if stored theme is 'auto'
|
|
122
|
+
if (this.storedTheme === 'auto') {
|
|
123
|
+
this.applyTheme('auto');
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Resolve 'auto' theme to actual theme based on system preference
|
|
129
|
+
*/
|
|
130
|
+
resolveTheme(theme) {
|
|
131
|
+
if (theme === 'auto') {
|
|
132
|
+
if (
|
|
133
|
+
typeof window !== 'undefined' &&
|
|
134
|
+
window.matchMedia &&
|
|
135
|
+
window.matchMedia('(prefers-color-scheme: dark)').matches
|
|
136
|
+
) {
|
|
137
|
+
return 'dark';
|
|
138
|
+
}
|
|
139
|
+
return 'light';
|
|
140
|
+
}
|
|
141
|
+
return theme;
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Apply the given theme
|
|
146
|
+
* @param {'light' | 'dark' | 'auto'} theme
|
|
147
|
+
*/
|
|
148
|
+
applyTheme(theme) {
|
|
149
|
+
const resolvedTheme = this.resolveTheme(theme);
|
|
150
|
+
this.currentTheme = resolvedTheme;
|
|
151
|
+
this.storedTheme = theme; // Remember the original preference
|
|
152
|
+
|
|
153
|
+
// Emit theme change event
|
|
154
|
+
this.$emit('theme-change', resolvedTheme);
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Toggle between light and dark themes
|
|
159
|
+
*/
|
|
160
|
+
toggleTheme() {
|
|
161
|
+
const newTheme = this.currentTheme === 'light' ? 'dark' : 'light';
|
|
162
|
+
this.setTheme(newTheme);
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Set a specific theme
|
|
167
|
+
* @param {'light' | 'dark' | 'auto'} theme
|
|
168
|
+
*/
|
|
169
|
+
setTheme(theme) {
|
|
170
|
+
if (!['light', 'dark', 'auto'].includes(theme)) {
|
|
171
|
+
console.warn(
|
|
172
|
+
`Invalid theme: ${theme}. Must be 'light', 'dark', or 'auto'.`
|
|
173
|
+
);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
this.storedTheme = theme;
|
|
178
|
+
this.applyTheme(theme);
|
|
179
|
+
|
|
180
|
+
// Persist to localStorage if enabled
|
|
181
|
+
if (this.enablePersistence && typeof window !== 'undefined') {
|
|
182
|
+
try {
|
|
183
|
+
localStorage.setItem(this.storageKey, theme);
|
|
184
|
+
} catch (error) {
|
|
185
|
+
console.warn('Failed to save theme to localStorage:', error);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
</script>
|