@elsapiens/cli 0.1.0 → 0.1.2
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 +115 -54
- package/dist/index.js +109 -36
- package/dist/index.js.map +1 -1
- package/dist/templates/app/app.ejs +295 -13
- package/dist/templates/app/env-example.ejs +3 -0
- package/dist/templates/app/eslint-config.ejs +47 -0
- package/dist/templates/app/help-topics.ejs +135 -0
- package/dist/templates/app/husky-pre-commit.ejs +8 -0
- package/dist/templates/app/index-css.ejs +536 -1
- package/dist/templates/app/main.ejs +81 -4
- package/dist/templates/app/package.ejs +28 -3
- package/dist/templates/app/page-dashboard.ejs +99 -60
- package/dist/templates/app/page-settings.ejs +268 -91
- package/dist/templates/app/postcss-config.ejs +6 -0
- package/dist/templates/app/services-setup.ejs +158 -0
- package/dist/templates/app/tailwind-config.ejs +105 -0
- package/dist/templates/app/test-setup.ejs +1 -0
- package/dist/templates/app/translations-common-en.ejs +23 -0
- package/dist/templates/app/translations-common-index.ejs +18 -0
- package/dist/templates/app/translations-common.ejs +94 -0
- package/dist/templates/app/translations-settings-en.ejs +49 -0
- package/dist/templates/app/translations-settings-index.ejs +18 -0
- package/dist/templates/app/tsconfig-node.ejs +10 -0
- package/dist/templates/app/vite-env.ejs +13 -0
- package/dist/templates/app/vitest-config.ejs +30 -0
- package/dist/templates/pages/list.ejs +636 -165
- package/dist/templates/pages/settings.ejs +208 -136
- package/package.json +4 -2
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
1
2
|
import {
|
|
2
3
|
Button,
|
|
3
4
|
Input,
|
|
@@ -6,110 +7,286 @@ import {
|
|
|
6
7
|
Card,
|
|
7
8
|
CardHeader,
|
|
8
9
|
CardTitle,
|
|
10
|
+
CardDescription,
|
|
9
11
|
CardContent,
|
|
12
|
+
Form,
|
|
13
|
+
FormField,
|
|
14
|
+
Separator,
|
|
10
15
|
} from '@elsapiens/ui';
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
16
|
+
import { PageHeaderWithBreadcrumbs } from '@elsapiens/layout';
|
|
17
|
+
import { User, Bell, Shield, Palette, Mail, Globe, Moon, Save, Maximize2, Sparkles } from 'lucide-react';
|
|
18
|
+
import { Link } from 'react-router-dom';
|
|
19
|
+
import { useTheme, useLocale, usePageHeader, SUPPORTED_LOCALES, BACKGROUND_STYLES, type Theme, type Locale, type SpacingMode, type BackgroundStyle } from '@elsapiens/providers';
|
|
20
|
+
import { loadSettings } from '../translations/settings/index';
|
|
21
|
+
|
|
22
|
+
const languageOptions = Object.values(SUPPORTED_LOCALES).map((config) => ({
|
|
23
|
+
value: config.code,
|
|
24
|
+
label: `${config.nativeName} (${config.name})`,
|
|
25
|
+
}));
|
|
13
26
|
|
|
14
27
|
export default function Settings() {
|
|
28
|
+
const { theme, setTheme, spacing, setSpacing, backgroundStyle, setBackgroundStyle } = useTheme();
|
|
29
|
+
const { locale, setLocale, t, loadTranslations } = useLocale();
|
|
15
30
|
const [notifications, setNotifications] = useState(true);
|
|
16
31
|
const [emailUpdates, setEmailUpdates] = useState(false);
|
|
17
32
|
|
|
33
|
+
// Load settings translations
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
loadTranslations(loadSettings);
|
|
36
|
+
}, [locale, loadTranslations]);
|
|
37
|
+
|
|
38
|
+
// Set page header info
|
|
39
|
+
usePageHeader({
|
|
40
|
+
title: t('settings.title'),
|
|
41
|
+
description: t('settings.description'),
|
|
42
|
+
breadcrumbs: [
|
|
43
|
+
{ label: t('nav.home'), href: '/', as: Link },
|
|
44
|
+
{ label: t('settings.title') },
|
|
45
|
+
],
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Theme options with translations
|
|
49
|
+
const themeOptions = [
|
|
50
|
+
{ value: 'light', label: t('settings.theme.light') },
|
|
51
|
+
{ value: 'dark', label: t('settings.theme.dark') },
|
|
52
|
+
{ value: 'system', label: t('settings.theme.system') },
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
// Spacing options with translations
|
|
56
|
+
const spacingOptions = [
|
|
57
|
+
{ value: 'compact', label: t('settings.spacing.compact') },
|
|
58
|
+
{ value: 'normal', label: t('settings.spacing.normal') },
|
|
59
|
+
{ value: 'spacious', label: t('settings.spacing.spacious') },
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
// Background style options
|
|
63
|
+
const backgroundOptions = BACKGROUND_STYLES.map((bg) => ({
|
|
64
|
+
value: bg.value,
|
|
65
|
+
label: bg.label,
|
|
66
|
+
}));
|
|
67
|
+
|
|
18
68
|
return (
|
|
19
|
-
<div className="
|
|
20
|
-
|
|
21
|
-
<div className="mb-8 pl-6">
|
|
22
|
-
<h1 className="text-2xl font-bold text-foreground">Settings</h1>
|
|
23
|
-
<p className="text-muted-foreground">Manage your account and preferences</p>
|
|
24
|
-
</div>
|
|
69
|
+
<div className="flex flex-col">
|
|
70
|
+
<PageHeaderWithBreadcrumbs variant="card" linkComponent={Link} />
|
|
25
71
|
|
|
26
|
-
<div className="
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
<
|
|
30
|
-
<
|
|
31
|
-
<
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
First Name
|
|
40
|
-
</label>
|
|
41
|
-
<Input defaultValue="John" hideMessage />
|
|
72
|
+
<div className="el-p-lg" style={{ overflowX: 'clip' }}>
|
|
73
|
+
<div className="el-space-y-section max-w-3xl">
|
|
74
|
+
{/* Profile Section */}
|
|
75
|
+
<Card>
|
|
76
|
+
<CardHeader>
|
|
77
|
+
<div className="flex items-center el-gap-sm">
|
|
78
|
+
<div className="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center">
|
|
79
|
+
<User className="w-5 h-5 text-primary" />
|
|
80
|
+
</div>
|
|
81
|
+
<div>
|
|
82
|
+
<CardTitle>{t('settings.profile')}</CardTitle>
|
|
83
|
+
<CardDescription>{t('settings.profileDescription')}</CardDescription>
|
|
84
|
+
</div>
|
|
42
85
|
</div>
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
86
|
+
</CardHeader>
|
|
87
|
+
<CardContent className="el-space-y-content">
|
|
88
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 el-gap-field">
|
|
89
|
+
<FormField label={t('settings.firstName')}>
|
|
90
|
+
<Input defaultValue="John" />
|
|
91
|
+
</FormField>
|
|
92
|
+
<FormField label={t('settings.lastName')}>
|
|
93
|
+
<Input defaultValue="Doe" />
|
|
94
|
+
</FormField>
|
|
48
95
|
</div>
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
</
|
|
54
|
-
<
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
<
|
|
65
|
-
<
|
|
66
|
-
<
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
96
|
+
<div className="el-gap-field">
|
|
97
|
+
<FormField label={t('settings.email')}>
|
|
98
|
+
<Input type="email" defaultValue="john.doe@example.com" />
|
|
99
|
+
</FormField>
|
|
100
|
+
</div>
|
|
101
|
+
<div className="el-gap-field flex justify-end">
|
|
102
|
+
<Button>
|
|
103
|
+
<Save className="w-4 h-4 el-mr-xs" />
|
|
104
|
+
{t('settings.saveChanges')}
|
|
105
|
+
</Button>
|
|
106
|
+
</div>
|
|
107
|
+
</CardContent>
|
|
108
|
+
</Card>
|
|
109
|
+
|
|
110
|
+
{/* Appearance Section */}
|
|
111
|
+
<Card>
|
|
112
|
+
<CardHeader>
|
|
113
|
+
<div className="flex items-center el-gap-sm">
|
|
114
|
+
<div className="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center">
|
|
115
|
+
<Palette className="w-5 h-5 text-primary" />
|
|
116
|
+
</div>
|
|
117
|
+
<div>
|
|
118
|
+
<CardTitle>{t('settings.appearance')}</CardTitle>
|
|
119
|
+
<CardDescription>{t('settings.appearanceDescription')}</CardDescription>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
</CardHeader>
|
|
123
|
+
<CardContent className="el-space-y-content">
|
|
124
|
+
{/* Spacing Mode Setting */}
|
|
125
|
+
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between el-gap-md el-py-sm">
|
|
126
|
+
<div className="flex items-start el-gap-sm">
|
|
127
|
+
<Maximize2 className="w-5 h-5 text-muted-foreground el-mt-xs flex-shrink-0" />
|
|
128
|
+
<div>
|
|
129
|
+
<p className="font-medium text-foreground">{t('settings.spacing')}</p>
|
|
130
|
+
<p className="text-sm text-muted-foreground">{t('settings.spacingDescription')}</p>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
<div className="sm:w-48 flex-shrink-0">
|
|
134
|
+
<Select
|
|
135
|
+
options={spacingOptions}
|
|
136
|
+
value={spacing}
|
|
137
|
+
onChange={(e) => setSpacing(e.target.value as SpacingMode)}
|
|
138
|
+
/>
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
<Separator />
|
|
143
|
+
|
|
144
|
+
{/* Theme Setting */}
|
|
145
|
+
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between el-gap-md el-py-sm">
|
|
146
|
+
<div className="flex items-start el-gap-sm">
|
|
147
|
+
<Moon className="w-5 h-5 text-muted-foreground el-mt-xs flex-shrink-0" />
|
|
148
|
+
<div>
|
|
149
|
+
<p className="font-medium text-foreground">{t('settings.theme')}</p>
|
|
150
|
+
<p className="text-sm text-muted-foreground">{t('settings.themeDescription')}</p>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
<div className="sm:w-48 flex-shrink-0">
|
|
154
|
+
<Select
|
|
155
|
+
options={themeOptions}
|
|
156
|
+
value={theme}
|
|
157
|
+
onChange={(e) => setTheme(e.target.value as Theme)}
|
|
158
|
+
/>
|
|
159
|
+
</div>
|
|
75
160
|
</div>
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
<
|
|
161
|
+
|
|
162
|
+
<Separator />
|
|
163
|
+
|
|
164
|
+
{/* Background Effect Setting */}
|
|
165
|
+
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between el-gap-md el-py-sm">
|
|
166
|
+
<div className="flex items-start el-gap-sm">
|
|
167
|
+
<Sparkles className="w-5 h-5 text-muted-foreground el-mt-xs flex-shrink-0" />
|
|
168
|
+
<div>
|
|
169
|
+
<p className="font-medium text-foreground">{t('settings.background')}</p>
|
|
170
|
+
<p className="text-sm text-muted-foreground">{t('settings.backgroundDescription')}</p>
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
<div className="sm:w-48 flex-shrink-0">
|
|
174
|
+
<Select
|
|
175
|
+
options={backgroundOptions}
|
|
176
|
+
value={backgroundStyle}
|
|
177
|
+
onChange={(e) => setBackgroundStyle(e.target.value as BackgroundStyle)}
|
|
178
|
+
/>
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
<Separator />
|
|
183
|
+
|
|
184
|
+
{/* Language Setting */}
|
|
185
|
+
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between el-gap-md el-py-sm">
|
|
186
|
+
<div className="flex items-start el-gap-sm">
|
|
187
|
+
<Globe className="w-5 h-5 text-muted-foreground el-mt-xs flex-shrink-0" />
|
|
188
|
+
<div>
|
|
189
|
+
<p className="font-medium text-foreground">{t('settings.language')}</p>
|
|
190
|
+
<p className="text-sm text-muted-foreground">{t('settings.languageDescription')}</p>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
<div className="sm:w-48 flex-shrink-0">
|
|
194
|
+
<Select
|
|
195
|
+
options={languageOptions}
|
|
196
|
+
value={locale}
|
|
197
|
+
onChange={(e) => setLocale(e.target.value as Locale)}
|
|
198
|
+
/>
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
</CardContent>
|
|
202
|
+
</Card>
|
|
203
|
+
|
|
204
|
+
{/* Notifications Section */}
|
|
205
|
+
<Card>
|
|
206
|
+
<CardHeader>
|
|
207
|
+
<div className="flex items-center el-gap-sm">
|
|
208
|
+
<div className="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center">
|
|
209
|
+
<Bell className="w-5 h-5 text-primary" />
|
|
210
|
+
</div>
|
|
211
|
+
<div>
|
|
212
|
+
<CardTitle>{t('settings.notifications')}</CardTitle>
|
|
213
|
+
<CardDescription>{t('settings.notificationsDescription')}</CardDescription>
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
</CardHeader>
|
|
217
|
+
<CardContent className="el-space-y-content">
|
|
218
|
+
{/* Push Notifications */}
|
|
219
|
+
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between el-gap-md el-py-sm">
|
|
220
|
+
<div className="flex items-start el-gap-sm">
|
|
221
|
+
<Bell className="w-5 h-5 text-muted-foreground el-mt-xs flex-shrink-0" />
|
|
222
|
+
<div>
|
|
223
|
+
<p className="font-medium text-foreground">{t('settings.pushNotifications')}</p>
|
|
224
|
+
<p className="text-sm text-muted-foreground">{t('settings.pushNotificationsDesc')}</p>
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
<div className="flex-shrink-0">
|
|
228
|
+
<Toggle checked={notifications} onChange={setNotifications} />
|
|
229
|
+
</div>
|
|
230
|
+
</div>
|
|
231
|
+
|
|
232
|
+
<Separator />
|
|
233
|
+
|
|
234
|
+
{/* Email Updates */}
|
|
235
|
+
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between el-gap-md el-py-sm">
|
|
236
|
+
<div className="flex items-start el-gap-sm">
|
|
237
|
+
<Mail className="w-5 h-5 text-muted-foreground el-mt-xs flex-shrink-0" />
|
|
238
|
+
<div>
|
|
239
|
+
<p className="font-medium text-foreground">{t('settings.emailUpdates')}</p>
|
|
240
|
+
<p className="text-sm text-muted-foreground">{t('settings.emailUpdatesDesc')}</p>
|
|
241
|
+
</div>
|
|
242
|
+
</div>
|
|
243
|
+
<div className="flex-shrink-0">
|
|
244
|
+
<Toggle checked={emailUpdates} onChange={setEmailUpdates} />
|
|
245
|
+
</div>
|
|
246
|
+
</div>
|
|
247
|
+
</CardContent>
|
|
248
|
+
</Card>
|
|
249
|
+
|
|
250
|
+
{/* Security Section */}
|
|
251
|
+
<Card>
|
|
252
|
+
<CardHeader>
|
|
253
|
+
<div className="flex items-center el-gap-sm">
|
|
254
|
+
<div className="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center">
|
|
255
|
+
<Shield className="w-5 h-5 text-primary" />
|
|
256
|
+
</div>
|
|
257
|
+
<div>
|
|
258
|
+
<CardTitle>{t('settings.security')}</CardTitle>
|
|
259
|
+
<CardDescription>{t('settings.securityDescription')}</CardDescription>
|
|
260
|
+
</div>
|
|
82
261
|
</div>
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
</CardContent>
|
|
112
|
-
</Card>
|
|
262
|
+
</CardHeader>
|
|
263
|
+
<CardContent>
|
|
264
|
+
<Form onSubmit={(e) => e.preventDefault()} className="el-space-y-content">
|
|
265
|
+
{/* Hidden username field for accessibility - helps password managers */}
|
|
266
|
+
<input type="text" name="username" autoComplete="username" className="sr-only" aria-hidden="true" tabIndex={-1} />
|
|
267
|
+
<div className="el-gap-field">
|
|
268
|
+
<FormField label={t('settings.currentPassword')}>
|
|
269
|
+
<Input type="password" placeholder={t('settings.currentPasswordPlaceholder')} autoComplete="current-password" />
|
|
270
|
+
</FormField>
|
|
271
|
+
</div>
|
|
272
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 el-gap-field">
|
|
273
|
+
<FormField label={t('settings.newPassword')}>
|
|
274
|
+
<Input type="password" placeholder={t('settings.newPasswordPlaceholder')} autoComplete="new-password" />
|
|
275
|
+
</FormField>
|
|
276
|
+
<FormField label={t('settings.confirmPassword')}>
|
|
277
|
+
<Input type="password" placeholder={t('settings.confirmPasswordPlaceholder')} autoComplete="new-password" />
|
|
278
|
+
</FormField>
|
|
279
|
+
</div>
|
|
280
|
+
<div className="el-gap-field flex justify-end">
|
|
281
|
+
<Button type="submit">
|
|
282
|
+
<Shield className="w-4 h-4 el-mr-xs" />
|
|
283
|
+
{t('settings.updatePassword')}
|
|
284
|
+
</Button>
|
|
285
|
+
</div>
|
|
286
|
+
</Form>
|
|
287
|
+
</CardContent>
|
|
288
|
+
</Card>
|
|
289
|
+
</div>
|
|
113
290
|
</div>
|
|
114
291
|
</div>
|
|
115
292
|
);
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createShortcutManager,
|
|
3
|
+
createLogger,
|
|
4
|
+
createApiClient,
|
|
5
|
+
type ShortcutManager,
|
|
6
|
+
type Logger,
|
|
7
|
+
type ApiClient,
|
|
8
|
+
} from '@elsapiens/services';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Application Services
|
|
12
|
+
*
|
|
13
|
+
* This file initializes and exports the core services used throughout the application.
|
|
14
|
+
* Import these services where needed in your components.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// Configuration from environment
|
|
18
|
+
const API_BASE_URL = import.meta.env.VITE_API_URL || '/api';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Logger instance
|
|
22
|
+
* Use for consistent logging throughout the application
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* logger.info('User logged in', { userId: '123' });
|
|
26
|
+
* logger.error('Failed to fetch data', error);
|
|
27
|
+
*/
|
|
28
|
+
export const logger: Logger = createLogger({
|
|
29
|
+
level: import.meta.env.DEV ? 'debug' : 'info',
|
|
30
|
+
context: '<%= pascalName %>',
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* API Client instance
|
|
35
|
+
* Use for making HTTP requests to your backend
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* const response = await apiClient.get('/users');
|
|
39
|
+
* const data = await apiClient.post('/users', { name: 'John' });
|
|
40
|
+
*/
|
|
41
|
+
export const apiClient: ApiClient = createApiClient({
|
|
42
|
+
baseUrl: API_BASE_URL,
|
|
43
|
+
timeout: 30000,
|
|
44
|
+
onError: (error) => {
|
|
45
|
+
logger.error('API Error', error);
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Shortcut Manager instance
|
|
51
|
+
*
|
|
52
|
+
* Uses a stack-based context system for handling shortcuts in modals/popups.
|
|
53
|
+
* Only the topmost context receives keyboard events for context-scoped shortcuts.
|
|
54
|
+
* Global shortcuts (scope: 'global') always receive events regardless of context.
|
|
55
|
+
*
|
|
56
|
+
* ## Shortcut Scopes
|
|
57
|
+
*
|
|
58
|
+
* - `scope: 'global'` - Always active, receives events regardless of context stack
|
|
59
|
+
* - `scope: 'context'` - Only active when its context is at the top of the stack
|
|
60
|
+
*
|
|
61
|
+
* ## Context Stack Usage
|
|
62
|
+
*
|
|
63
|
+
* When opening a modal/popup:
|
|
64
|
+
* ```ts
|
|
65
|
+
* shortcutManager.pushContext('my-modal');
|
|
66
|
+
* ```
|
|
67
|
+
*
|
|
68
|
+
* When closing a modal/popup:
|
|
69
|
+
* ```ts
|
|
70
|
+
* shortcutManager.popContext('my-modal');
|
|
71
|
+
* ```
|
|
72
|
+
*
|
|
73
|
+
* ## Example: Modal with context-specific shortcuts
|
|
74
|
+
*
|
|
75
|
+
* ```tsx
|
|
76
|
+
* function MyModal({ onClose }: { onClose: () => void }) {
|
|
77
|
+
* useEffect(() => {
|
|
78
|
+
* // Push context when modal opens
|
|
79
|
+
* shortcutManager.pushContext('my-modal');
|
|
80
|
+
*
|
|
81
|
+
* // Register modal-specific shortcuts
|
|
82
|
+
* const unregister = shortcutManager.register({
|
|
83
|
+
* id: 'close-modal',
|
|
84
|
+
* keys: 'escape',
|
|
85
|
+
* label: 'Close',
|
|
86
|
+
* category: 'modal',
|
|
87
|
+
* scope: 'context',
|
|
88
|
+
* context: 'my-modal',
|
|
89
|
+
* action: () => onClose(),
|
|
90
|
+
* });
|
|
91
|
+
*
|
|
92
|
+
* return () => {
|
|
93
|
+
* unregister();
|
|
94
|
+
* shortcutManager.popContext('my-modal');
|
|
95
|
+
* };
|
|
96
|
+
* }, [onClose]);
|
|
97
|
+
*
|
|
98
|
+
* return <div>Modal content</div>;
|
|
99
|
+
* }
|
|
100
|
+
* ```
|
|
101
|
+
*/
|
|
102
|
+
export const shortcutManager: ShortcutManager = createShortcutManager();
|
|
103
|
+
|
|
104
|
+
// Register global shortcuts (always active regardless of context)
|
|
105
|
+
shortcutManager.register({
|
|
106
|
+
id: 'help',
|
|
107
|
+
keys: '?',
|
|
108
|
+
label: 'Open Help',
|
|
109
|
+
category: 'General',
|
|
110
|
+
scope: 'global',
|
|
111
|
+
action: () => {
|
|
112
|
+
// Toggle help panel - implement based on your HelpProvider
|
|
113
|
+
logger.debug('Help shortcut triggered');
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
shortcutManager.register({
|
|
118
|
+
id: 'search',
|
|
119
|
+
keys: 'ctrl+k',
|
|
120
|
+
label: 'Open Search',
|
|
121
|
+
category: 'General',
|
|
122
|
+
scope: 'global',
|
|
123
|
+
action: () => {
|
|
124
|
+
// Open search/command palette - implement based on your needs
|
|
125
|
+
logger.debug('Search shortcut triggered');
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// Shortcut manager detach function (set when attached)
|
|
130
|
+
let detachShortcuts: (() => void) | null = null;
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Attach shortcut manager to document
|
|
134
|
+
* Call this after the app mounts
|
|
135
|
+
*/
|
|
136
|
+
export function attachShortcuts(): void {
|
|
137
|
+
detachShortcuts = shortcutManager.attach();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Initialize all services
|
|
142
|
+
* Call this in your main.tsx before rendering the app
|
|
143
|
+
*/
|
|
144
|
+
export function initializeServices(): void {
|
|
145
|
+
logger.info('Services initialized');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Cleanup all services
|
|
150
|
+
* Call this when unmounting the app (e.g., in useEffect cleanup)
|
|
151
|
+
*/
|
|
152
|
+
export function cleanupServices(): void {
|
|
153
|
+
if (detachShortcuts) {
|
|
154
|
+
detachShortcuts();
|
|
155
|
+
detachShortcuts = null;
|
|
156
|
+
}
|
|
157
|
+
logger.info('Services cleaned up');
|
|
158
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/** @type {import('tailwindcss').Config} */
|
|
2
|
+
export default {
|
|
3
|
+
content: [
|
|
4
|
+
'./index.html',
|
|
5
|
+
'./src/**/*.{js,ts,jsx,tsx}',
|
|
6
|
+
'./node_modules/@elsapiens/*/dist/**/*.{js,ts,jsx,tsx}',
|
|
7
|
+
],
|
|
8
|
+
safelist: [
|
|
9
|
+
'hide-native-scrollbar',
|
|
10
|
+
'scrollbar-visible',
|
|
11
|
+
'scrollbar-hide',
|
|
12
|
+
'scrollbar-custom',
|
|
13
|
+
],
|
|
14
|
+
darkMode: 'class',
|
|
15
|
+
theme: {
|
|
16
|
+
extend: {
|
|
17
|
+
colors: {
|
|
18
|
+
// Wisteria palette (for accent colors - changes with color scheme)
|
|
19
|
+
wisteria: {
|
|
20
|
+
50: 'hsl(var(--wisteria-50))',
|
|
21
|
+
100: 'hsl(var(--wisteria-100))',
|
|
22
|
+
200: 'hsl(var(--wisteria-200))',
|
|
23
|
+
300: 'hsl(var(--wisteria-300))',
|
|
24
|
+
400: 'hsl(var(--wisteria-400))',
|
|
25
|
+
500: 'hsl(var(--wisteria-500))',
|
|
26
|
+
600: 'hsl(var(--wisteria-600))',
|
|
27
|
+
700: 'hsl(var(--wisteria-700))',
|
|
28
|
+
800: 'hsl(var(--wisteria-800))',
|
|
29
|
+
850: 'hsl(var(--wisteria-850))',
|
|
30
|
+
900: 'hsl(var(--wisteria-900))',
|
|
31
|
+
950: 'hsl(var(--wisteria-950))',
|
|
32
|
+
},
|
|
33
|
+
// Neutral gray scale (never changes)
|
|
34
|
+
neutral: {
|
|
35
|
+
50: 'hsl(var(--neutral-50))',
|
|
36
|
+
100: 'hsl(var(--neutral-100))',
|
|
37
|
+
200: 'hsl(var(--neutral-200))',
|
|
38
|
+
300: 'hsl(var(--neutral-300))',
|
|
39
|
+
400: 'hsl(var(--neutral-400))',
|
|
40
|
+
500: 'hsl(var(--neutral-500))',
|
|
41
|
+
600: 'hsl(var(--neutral-600))',
|
|
42
|
+
700: 'hsl(var(--neutral-700))',
|
|
43
|
+
800: 'hsl(var(--neutral-800))',
|
|
44
|
+
900: 'hsl(var(--neutral-900))',
|
|
45
|
+
950: 'hsl(var(--neutral-950))',
|
|
46
|
+
},
|
|
47
|
+
// Semantic colors
|
|
48
|
+
border: 'hsl(var(--border))',
|
|
49
|
+
input: 'hsl(var(--input))',
|
|
50
|
+
ring: 'hsl(var(--ring))',
|
|
51
|
+
background: 'hsl(var(--background))',
|
|
52
|
+
foreground: 'hsl(var(--foreground))',
|
|
53
|
+
primary: {
|
|
54
|
+
50: 'hsl(var(--wisteria-50))',
|
|
55
|
+
100: 'hsl(var(--wisteria-100))',
|
|
56
|
+
200: 'hsl(var(--wisteria-200))',
|
|
57
|
+
300: 'hsl(var(--wisteria-300))',
|
|
58
|
+
400: 'hsl(var(--wisteria-400))',
|
|
59
|
+
500: 'hsl(var(--wisteria-500))',
|
|
60
|
+
600: 'hsl(var(--wisteria-600))',
|
|
61
|
+
700: 'hsl(var(--wisteria-700))',
|
|
62
|
+
800: 'hsl(var(--wisteria-800))',
|
|
63
|
+
900: 'hsl(var(--wisteria-900))',
|
|
64
|
+
950: 'hsl(var(--wisteria-950))',
|
|
65
|
+
DEFAULT: 'hsl(var(--primary))',
|
|
66
|
+
foreground: 'hsl(var(--primary-foreground))',
|
|
67
|
+
},
|
|
68
|
+
secondary: {
|
|
69
|
+
DEFAULT: 'hsl(var(--secondary))',
|
|
70
|
+
foreground: 'hsl(var(--secondary-foreground))',
|
|
71
|
+
},
|
|
72
|
+
destructive: {
|
|
73
|
+
DEFAULT: 'hsl(var(--destructive))',
|
|
74
|
+
foreground: 'hsl(var(--destructive-foreground))',
|
|
75
|
+
},
|
|
76
|
+
muted: {
|
|
77
|
+
DEFAULT: 'hsl(var(--muted))',
|
|
78
|
+
foreground: 'hsl(var(--muted-foreground))',
|
|
79
|
+
},
|
|
80
|
+
accent: {
|
|
81
|
+
DEFAULT: 'hsl(var(--accent))',
|
|
82
|
+
foreground: 'hsl(var(--accent-foreground))',
|
|
83
|
+
},
|
|
84
|
+
popover: {
|
|
85
|
+
DEFAULT: 'hsl(var(--popover))',
|
|
86
|
+
foreground: 'hsl(var(--popover-foreground))',
|
|
87
|
+
},
|
|
88
|
+
card: {
|
|
89
|
+
DEFAULT: 'hsl(var(--card))',
|
|
90
|
+
foreground: 'hsl(var(--card-foreground))',
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
borderRadius: {
|
|
94
|
+
lg: 'var(--radius)',
|
|
95
|
+
md: 'calc(var(--radius) - 2px)',
|
|
96
|
+
sm: 'calc(var(--radius) - 4px)',
|
|
97
|
+
},
|
|
98
|
+
fontFamily: {
|
|
99
|
+
sans: ['Inter', 'system-ui', '-apple-system', 'sans-serif'],
|
|
100
|
+
mono: ['JetBrains Mono', 'ui-monospace', 'monospace'],
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
plugins: [],
|
|
105
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import '@testing-library/jest-dom';
|