create-modsemi 0.1.0

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.
Files changed (53) hide show
  1. package/README.md +99 -0
  2. package/dist/index.d.ts +3 -0
  3. package/dist/index.d.ts.map +1 -0
  4. package/dist/index.js +155 -0
  5. package/dist/index.js.map +1 -0
  6. package/package.json +38 -0
  7. package/template/.browserslistrc +4 -0
  8. package/template/.env.example +9 -0
  9. package/template/.github/workflows/ci.yml +36 -0
  10. package/template/.nvmrc +2 -0
  11. package/template/README.md +199 -0
  12. package/template/_gitignore +33 -0
  13. package/template/_package.json +36 -0
  14. package/template/biome.json +37 -0
  15. package/template/modern.config.ts +38 -0
  16. package/template/orval.config.ts +98 -0
  17. package/template/src/api/instance.ts +87 -0
  18. package/template/src/components/Access/index.tsx +32 -0
  19. package/template/src/components/AppBreadcrumb/index.tsx +34 -0
  20. package/template/src/components/UserAvatar/index.less +66 -0
  21. package/template/src/components/UserAvatar/index.tsx +96 -0
  22. package/template/src/config/global.tsx +59 -0
  23. package/template/src/config/navigation.tsx +91 -0
  24. package/template/src/hooks/useAccess.ts +53 -0
  25. package/template/src/hooks/useMenuData.ts +171 -0
  26. package/template/src/hooks/usePageTitle.ts +26 -0
  27. package/template/src/layouts/ProLayout/DoubleLayout.tsx +157 -0
  28. package/template/src/layouts/ProLayout/LayoutBreadcrumb.tsx +32 -0
  29. package/template/src/layouts/ProLayout/MixLayout.tsx +134 -0
  30. package/template/src/layouts/ProLayout/SideLayout.tsx +108 -0
  31. package/template/src/layouts/ProLayout/TopLayout.tsx +98 -0
  32. package/template/src/layouts/ProLayout/index.tsx +75 -0
  33. package/template/src/layouts/SettingDrawer/index.tsx +390 -0
  34. package/template/src/modern-app-env.d.ts +1 -0
  35. package/template/src/modern.runtime.ts +3 -0
  36. package/template/src/pages/Dashboard/Workplace/index.tsx +7 -0
  37. package/template/src/pages/Error/NotFound/index.less +211 -0
  38. package/template/src/pages/Error/NotFound/index.tsx +64 -0
  39. package/template/src/pages/Login/index.less +491 -0
  40. package/template/src/pages/Login/index.tsx +204 -0
  41. package/template/src/pages/Welcome/index.less +351 -0
  42. package/template/src/pages/Welcome/index.tsx +164 -0
  43. package/template/src/routes/$.tsx +14 -0
  44. package/template/src/routes/dashboard/workplace/page.tsx +3 -0
  45. package/template/src/routes/layout.tsx +53 -0
  46. package/template/src/routes/login/page.tsx +3 -0
  47. package/template/src/routes/page.tsx +3 -0
  48. package/template/src/store/authStore.ts +61 -0
  49. package/template/src/store/layoutStore.ts +82 -0
  50. package/template/src/store/pageTitleStore.ts +12 -0
  51. package/template/src/styles/global.less +80 -0
  52. package/template/swagger/sample.json +263 -0
  53. package/template/tsconfig.json +16 -0
@@ -0,0 +1,351 @@
1
+ // ============================================================
2
+ // Welcome Page Styles
3
+ // 歡迎頁 — 企業指揮中心美學
4
+ // ============================================================
5
+
6
+ @keyframes float-up {
7
+ from {
8
+ opacity: 0;
9
+ transform: translateY(24px);
10
+ }
11
+ to {
12
+ opacity: 1;
13
+ transform: translateY(0);
14
+ }
15
+ }
16
+
17
+ @keyframes shimmer {
18
+ 0% { background-position: -200% center; }
19
+ 100% { background-position: 200% center; }
20
+ }
21
+
22
+ @keyframes pulse-ring {
23
+ 0% { transform: scale(0.92); opacity: 0.6; }
24
+ 50% { transform: scale(1); opacity: 1; }
25
+ 100% { transform: scale(0.92); opacity: 0.6; }
26
+ }
27
+
28
+ @keyframes grid-fade {
29
+ from { opacity: 0; }
30
+ to { opacity: 1; }
31
+ }
32
+
33
+ .welcome-page {
34
+ position: relative;
35
+ min-height: calc(100vh - 56px);
36
+ overflow: hidden;
37
+ background-color: var(--semi-color-bg-0);
38
+
39
+ // 背景網格紋理
40
+ &::before {
41
+ content: '';
42
+ position: absolute;
43
+ inset: 0;
44
+ background-image:
45
+ linear-gradient(var(--semi-color-border) 1px, transparent 1px),
46
+ linear-gradient(90deg, var(--semi-color-border) 1px, transparent 1px);
47
+ background-size: 40px 40px;
48
+ opacity: 0.4;
49
+ pointer-events: none;
50
+ animation: grid-fade 1s ease forwards;
51
+ }
52
+
53
+ // 頂部光暈
54
+ &::after {
55
+ content: '';
56
+ position: absolute;
57
+ top: -200px;
58
+ left: 50%;
59
+ transform: translateX(-50%);
60
+ width: 800px;
61
+ height: 500px;
62
+ border-radius: 50%;
63
+ background: radial-gradient(
64
+ ellipse at center,
65
+ color-mix(in srgb, var(--semi-color-primary) 12%, transparent) 0%,
66
+ transparent 70%
67
+ );
68
+ pointer-events: none;
69
+ }
70
+ }
71
+
72
+ .welcome-inner {
73
+ position: relative;
74
+ z-index: 1;
75
+ max-width: 1100px;
76
+ margin: 0 auto;
77
+ padding: 72px 32px 64px;
78
+ }
79
+
80
+ // ── HERO ──────────────────────────────────────────────
81
+ .hero {
82
+ text-align: center;
83
+ margin-bottom: 80px;
84
+ }
85
+
86
+ .hero-badge {
87
+ display: inline-flex;
88
+ align-items: center;
89
+ gap: 6px;
90
+ padding: 5px 14px;
91
+ border-radius: 20px;
92
+ border: 1px solid var(--semi-color-primary-light-hover);
93
+ background: color-mix(in srgb, var(--semi-color-primary) 8%, transparent);
94
+ font-size: 12px;
95
+ font-weight: 600;
96
+ color: var(--semi-color-primary);
97
+ letter-spacing: 0.08em;
98
+ text-transform: uppercase;
99
+ margin-bottom: 28px;
100
+ animation: float-up 0.6s ease both;
101
+
102
+ .hero-badge-dot {
103
+ width: 6px;
104
+ height: 6px;
105
+ border-radius: 50%;
106
+ background: var(--semi-color-primary);
107
+ animation: pulse-ring 2s ease-in-out infinite;
108
+ }
109
+ }
110
+
111
+ .hero-title {
112
+ font-size: clamp(40px, 6vw, 64px);
113
+ font-weight: 800;
114
+ line-height: 1.1;
115
+ letter-spacing: -0.03em;
116
+ color: var(--semi-color-text-0);
117
+ margin: 0 0 8px;
118
+ animation: float-up 0.6s ease 0.1s both;
119
+
120
+ .hero-title-brand {
121
+ background: linear-gradient(
122
+ 130deg,
123
+ var(--semi-color-primary) 0%,
124
+ #7b93ff 50%,
125
+ var(--semi-color-primary) 100%
126
+ );
127
+ background-size: 200% auto;
128
+ -webkit-background-clip: text;
129
+ -webkit-text-fill-color: transparent;
130
+ background-clip: text;
131
+ animation: shimmer 4s linear infinite;
132
+ }
133
+ }
134
+
135
+ .hero-subtitle {
136
+ font-size: 17px;
137
+ color: var(--semi-color-text-2);
138
+ line-height: 1.7;
139
+ max-width: 520px;
140
+ margin: 20px auto 40px;
141
+ animation: float-up 0.6s ease 0.2s both;
142
+ }
143
+
144
+ .hero-actions {
145
+ display: flex;
146
+ align-items: center;
147
+ justify-content: center;
148
+ gap: 12px;
149
+ flex-wrap: wrap;
150
+ animation: float-up 0.6s ease 0.3s both;
151
+ }
152
+
153
+ // ── STATS ─────────────────────────────────────────────
154
+ .stats-row {
155
+ display: grid;
156
+ grid-template-columns: repeat(4, 1fr);
157
+ gap: 16px;
158
+ margin-bottom: 64px;
159
+ animation: float-up 0.6s ease 0.35s both;
160
+
161
+ @media (max-width: 720px) {
162
+ grid-template-columns: repeat(2, 1fr);
163
+ }
164
+ }
165
+
166
+ .stat-card {
167
+ padding: 24px 20px;
168
+ border-radius: 12px;
169
+ border: 1px solid var(--semi-color-border);
170
+ background: var(--semi-color-bg-1);
171
+ text-align: center;
172
+ transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
173
+
174
+ &:hover {
175
+ transform: translateY(-3px);
176
+ box-shadow: 0 8px 32px color-mix(in srgb, var(--semi-color-primary) 15%, transparent);
177
+ border-color: var(--semi-color-primary-light-hover);
178
+ }
179
+
180
+ .stat-value {
181
+ font-size: 32px;
182
+ font-weight: 800;
183
+ color: var(--semi-color-primary);
184
+ line-height: 1;
185
+ margin-bottom: 6px;
186
+ letter-spacing: -0.02em;
187
+ }
188
+
189
+ .stat-label {
190
+ font-size: 12px;
191
+ color: var(--semi-color-text-2);
192
+ font-weight: 500;
193
+ letter-spacing: 0.04em;
194
+ }
195
+ }
196
+
197
+ // ── FEATURE SECTION ───────────────────────────────────
198
+ .section-header {
199
+ margin-bottom: 32px;
200
+ animation: float-up 0.6s ease 0.4s both;
201
+
202
+ .section-label {
203
+ font-size: 11px;
204
+ font-weight: 700;
205
+ letter-spacing: 0.12em;
206
+ text-transform: uppercase;
207
+ color: var(--semi-color-primary);
208
+ margin-bottom: 8px;
209
+ }
210
+
211
+ .section-title {
212
+ font-size: 26px;
213
+ font-weight: 700;
214
+ color: var(--semi-color-text-0);
215
+ margin: 0 0 8px;
216
+ letter-spacing: -0.02em;
217
+ }
218
+
219
+ .section-desc {
220
+ font-size: 14px;
221
+ color: var(--semi-color-text-2);
222
+ }
223
+ }
224
+
225
+ .layout-grid {
226
+ display: grid;
227
+ grid-template-columns: repeat(2, 1fr);
228
+ gap: 16px;
229
+ margin-bottom: 64px;
230
+ animation: float-up 0.6s ease 0.45s both;
231
+
232
+ @media (max-width: 640px) {
233
+ grid-template-columns: 1fr;
234
+ }
235
+ }
236
+
237
+ .layout-card {
238
+ position: relative;
239
+ border-radius: 14px;
240
+ border: 1px solid var(--semi-color-border);
241
+ background: var(--semi-color-bg-1);
242
+ padding: 24px;
243
+ overflow: hidden;
244
+ cursor: default;
245
+ transition: transform 0.25s ease, box-shadow 0.25s ease, border-color 0.25s ease;
246
+
247
+ &:hover {
248
+ transform: translateY(-4px);
249
+ box-shadow: 0 12px 40px color-mix(in srgb, var(--semi-color-primary) 14%, transparent);
250
+ border-color: var(--semi-color-primary-light-hover);
251
+
252
+ .layout-card-bg {
253
+ opacity: 1;
254
+ }
255
+ }
256
+
257
+ // 角落裝飾光暈
258
+ .layout-card-bg {
259
+ position: absolute;
260
+ top: -40px;
261
+ right: -40px;
262
+ width: 120px;
263
+ height: 120px;
264
+ border-radius: 50%;
265
+ background: radial-gradient(
266
+ circle,
267
+ color-mix(in srgb, var(--semi-color-primary) 18%, transparent) 0%,
268
+ transparent 70%
269
+ );
270
+ opacity: 0;
271
+ transition: opacity 0.3s ease;
272
+ pointer-events: none;
273
+ }
274
+
275
+ .layout-card-icon {
276
+ width: 44px;
277
+ height: 44px;
278
+ border-radius: 10px;
279
+ background: color-mix(in srgb, var(--semi-color-primary) 10%, transparent);
280
+ border: 1px solid color-mix(in srgb, var(--semi-color-primary) 25%, transparent);
281
+ display: flex;
282
+ align-items: center;
283
+ justify-content: center;
284
+ font-size: 20px;
285
+ margin-bottom: 16px;
286
+ color: var(--semi-color-primary);
287
+ }
288
+
289
+ .layout-card-title {
290
+ font-size: 16px;
291
+ font-weight: 700;
292
+ color: var(--semi-color-text-0);
293
+ margin-bottom: 8px;
294
+ }
295
+
296
+ .layout-card-desc {
297
+ font-size: 13px;
298
+ color: var(--semi-color-text-2);
299
+ line-height: 1.6;
300
+ }
301
+
302
+ .layout-card-tag {
303
+ display: inline-block;
304
+ margin-top: 14px;
305
+ padding: 3px 10px;
306
+ border-radius: 6px;
307
+ font-size: 11px;
308
+ font-weight: 600;
309
+ background: color-mix(in srgb, var(--semi-color-primary) 10%, transparent);
310
+ color: var(--semi-color-primary);
311
+ border: 1px solid color-mix(in srgb, var(--semi-color-primary) 20%, transparent);
312
+ }
313
+ }
314
+
315
+ // ── TECH STACK ────────────────────────────────────────
316
+ .tech-row {
317
+ display: flex;
318
+ align-items: center;
319
+ gap: 12px;
320
+ flex-wrap: wrap;
321
+ padding-top: 40px;
322
+ border-top: 1px solid var(--semi-color-border);
323
+ animation: float-up 0.6s ease 0.5s both;
324
+
325
+ .tech-label {
326
+ font-size: 12px;
327
+ color: var(--semi-color-text-3);
328
+ font-weight: 500;
329
+ margin-right: 4px;
330
+ }
331
+
332
+ .tech-badge {
333
+ display: inline-flex;
334
+ align-items: center;
335
+ gap: 5px;
336
+ padding: 5px 12px;
337
+ border-radius: 8px;
338
+ border: 1px solid var(--semi-color-border);
339
+ background: var(--semi-color-bg-1);
340
+ font-size: 12px;
341
+ font-weight: 600;
342
+ color: var(--semi-color-text-1);
343
+ transition: border-color 0.2s ease, background 0.2s ease;
344
+
345
+ &:hover {
346
+ border-color: var(--semi-color-primary-light-hover);
347
+ background: color-mix(in srgb, var(--semi-color-primary) 6%, transparent);
348
+ color: var(--semi-color-primary);
349
+ }
350
+ }
351
+ }
@@ -0,0 +1,164 @@
1
+ import {
2
+ IconCode,
3
+ IconColumnsStroked,
4
+ IconDoubleChevronRight,
5
+ IconGridView,
6
+ IconSidebar,
7
+ IconTerminal,
8
+ IconVersionStroked,
9
+ } from '@douyinfe/semi-icons';
10
+ import { Button, Typography } from '@douyinfe/semi-ui-19';
11
+ import { useNavigate } from '@modern-js/runtime/router';
12
+ import { useLayoutStore } from '../../store/layoutStore';
13
+ import './index.less';
14
+
15
+ const { Text } = Typography;
16
+
17
+ const STATS = [
18
+ { value: '4', label: '佈局模式' },
19
+ { value: '∞', label: '主題可配置' },
20
+ { value: '100%', label: 'TypeScript' },
21
+ { value: 'v3', label: 'Modern.js' },
22
+ ];
23
+
24
+ const LAYOUTS = [
25
+ {
26
+ icon: <IconSidebar />,
27
+ title: 'Side 側邊導航',
28
+ desc: '左側固定選單,子項可巢狀展開,適合功能豐富的後台系統。',
29
+ tag: '最常用',
30
+ mode: 'side',
31
+ },
32
+ {
33
+ icon: <IconColumnsStroked />,
34
+ title: 'Top 頂部導航',
35
+ desc: '水平頂欄選單,子項以 Dropdown 展開,適合寬螢幕內容型頁面。',
36
+ tag: '寬螢幕',
37
+ mode: 'top',
38
+ },
39
+ {
40
+ icon: <IconGridView />,
41
+ title: 'Mix 混合導航',
42
+ desc: '頂部顯示一級模組、左側顯示二級子項,大型 SaaS 後台首選。',
43
+ tag: '企業推薦',
44
+ mode: 'mix',
45
+ },
46
+ {
47
+ icon: <IconVersionStroked />,
48
+ title: 'Double 雙欄導航',
49
+ desc: '極窄圖示欄 + 文字選單欄,容納超多模組,兼顧空間與導航效率。',
50
+ tag: '預設模式',
51
+ mode: 'double',
52
+ },
53
+ ] as const;
54
+
55
+ const TECH = [
56
+ { label: 'Modern.js', icon: '⚡' },
57
+ { label: 'Semi Design', icon: '🎨' },
58
+ { label: 'Zustand', icon: '🐻' },
59
+ { label: 'TypeScript', icon: '🔷' },
60
+ { label: 'Less', icon: '💅' },
61
+ ];
62
+
63
+ export default function Welcome() {
64
+ const navigate = useNavigate();
65
+ const { setLayoutMode } = useLayoutStore();
66
+
67
+ const handleSwitchLayout = (mode: (typeof LAYOUTS)[number]['mode']) => {
68
+ setLayoutMode(mode);
69
+ navigate('/dashboard/workplace');
70
+ };
71
+
72
+ return (
73
+ <div className="welcome-page">
74
+ <div className="welcome-inner">
75
+ {/* Hero */}
76
+ <section className="hero">
77
+ <div className="hero-badge">
78
+ <span className="hero-badge-dot" />
79
+ ModSemi Framework · v0.1.0
80
+ </div>
81
+
82
+ <h1 className="hero-title">
83
+ 現代企業級
84
+ <br />
85
+ <span className="hero-title-brand">中後台框架</span>
86
+ </h1>
87
+
88
+ <p className="hero-subtitle">
89
+ 基於 Modern.js + Semi Design
90
+ 構建,提供四種佈局模式、全域主題切換與配置化導航,讓你專注於業務,而非重複搭建基礎設施。
91
+ </p>
92
+
93
+ <div className="hero-actions">
94
+ <Button
95
+ theme="solid"
96
+ type="primary"
97
+ size="large"
98
+ icon={<IconDoubleChevronRight />}
99
+ iconPosition="right"
100
+ onClick={() => navigate('/dashboard/workplace')}
101
+ >
102
+ 開始使用
103
+ </Button>
104
+ <Button
105
+ theme="borderless"
106
+ type="tertiary"
107
+ size="large"
108
+ icon={<IconCode />}
109
+ >
110
+ 查看原始碼
111
+ </Button>
112
+ </div>
113
+ </section>
114
+
115
+ {/* Stats */}
116
+ <div className="stats-row">
117
+ {STATS.map(s => (
118
+ <div key={s.label} className="stat-card">
119
+ <div className="stat-value">{s.value}</div>
120
+ <div className="stat-label">{s.label}</div>
121
+ </div>
122
+ ))}
123
+ </div>
124
+
125
+ {/* Layout Modes */}
126
+ <div className="section-header">
127
+ <div className="section-label">核心能力</div>
128
+ <div className="section-title">四種佈局模式,即選即用</div>
129
+ <div className="section-desc">
130
+ 點擊任意卡片切換至對應佈局,或透過右下角齒輪按鈕進行設定。
131
+ </div>
132
+ </div>
133
+
134
+ <div className="layout-grid">
135
+ {LAYOUTS.map(item => (
136
+ <button
137
+ key={item.mode}
138
+ type="button"
139
+ className="layout-card"
140
+ onClick={() => handleSwitchLayout(item.mode)}
141
+ style={{ textAlign: 'left', width: '100%' }}
142
+ >
143
+ <div className="layout-card-bg" />
144
+ <div className="layout-card-icon">{item.icon}</div>
145
+ <div className="layout-card-title">{item.title}</div>
146
+ <div className="layout-card-desc">{item.desc}</div>
147
+ <span className="layout-card-tag">{item.tag}</span>
148
+ </button>
149
+ ))}
150
+ </div>
151
+
152
+ {/* Tech Stack */}
153
+ <div className="tech-row">
154
+ <span className="tech-label">技術棧</span>
155
+ {TECH.map(t => (
156
+ <span key={t.label} className="tech-badge">
157
+ {t.icon} {t.label}
158
+ </span>
159
+ ))}
160
+ </div>
161
+ </div>
162
+ </div>
163
+ );
164
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Catch-all 路由(Modern.js 通配符檔案)
3
+ *
4
+ * 任何未被其他 page.tsx 匹配到的路徑都會落到這裡。
5
+ * 實際渲染哪個元件由 src/config/global.tsx 的 errorPages.notFound 決定。
6
+ */
7
+
8
+ import { globalConfig } from '../config/global';
9
+
10
+ const NotFound = globalConfig.errorPages.notFound;
11
+
12
+ export default function NotFoundRoute() {
13
+ return <NotFound />;
14
+ }
@@ -0,0 +1,3 @@
1
+ import Workplace from '../../../pages/Dashboard/Workplace';
2
+
3
+ export default Workplace;
@@ -0,0 +1,53 @@
1
+ import { Navigate, Outlet, useLocation } from '@modern-js/runtime/router';
2
+ import { ProLayout } from '../layouts/ProLayout';
3
+ import { SettingDrawer } from '../layouts/SettingDrawer';
4
+ import { useAuthStore } from '../store/authStore';
5
+ import '../styles/global.less';
6
+
7
+ export default function Layout() {
8
+ const location = useLocation();
9
+ const { isLoggedIn } = useAuthStore();
10
+ const isLoginPage = location.pathname === '/login';
11
+
12
+ // 未登入且不在登入頁 → 直接宣告式重導,無 useEffect 競爭問題
13
+ if (!isLoggedIn && !isLoginPage) {
14
+ return <Navigate to="/login" replace />;
15
+ }
16
+
17
+ // 登入頁不套 ProLayout
18
+ if (isLoginPage) {
19
+ return <Outlet />;
20
+ }
21
+
22
+ const logo = (
23
+ <div
24
+ style={{
25
+ width: 28,
26
+ height: 28,
27
+ borderRadius: 8,
28
+ background: 'linear-gradient(135deg, #4f7dff 0%, #7b52ff 100%)',
29
+ display: 'flex',
30
+ alignItems: 'center',
31
+ justifyContent: 'center',
32
+ color: '#fff',
33
+ fontWeight: 800,
34
+ fontSize: 14,
35
+ flexShrink: 0,
36
+ boxShadow: '0 2px 8px rgba(79, 125, 255, 0.4)',
37
+ fontFamily: "'Outfit', sans-serif",
38
+ letterSpacing: '-0.01em',
39
+ }}
40
+ >
41
+ M
42
+ </div>
43
+ );
44
+
45
+ return (
46
+ <>
47
+ <ProLayout title="ModSemi" logo={logo}>
48
+ <Outlet />
49
+ </ProLayout>
50
+ <SettingDrawer />
51
+ </>
52
+ );
53
+ }
@@ -0,0 +1,3 @@
1
+ import LoginPage from '../../pages/Login';
2
+
3
+ export default LoginPage;
@@ -0,0 +1,3 @@
1
+ import Welcome from '../pages/Welcome';
2
+
3
+ export default Welcome;
@@ -0,0 +1,61 @@
1
+ import { create } from 'zustand';
2
+ import { persist } from 'zustand/middleware';
3
+
4
+ export interface CurrentUser {
5
+ name: string;
6
+ avatar?: string;
7
+ email?: string;
8
+ roles: string[];
9
+ }
10
+
11
+ export interface AuthState {
12
+ currentUser: CurrentUser | null;
13
+ isLoggedIn: boolean;
14
+ }
15
+
16
+ export interface AuthActions {
17
+ login: (username: string, password: string) => boolean;
18
+ logout: () => void;
19
+ }
20
+
21
+ const MOCK_USERS = [
22
+ {
23
+ username: 'admin',
24
+ password: 'admin.modsemi',
25
+ user: {
26
+ name: 'Admin',
27
+ email: 'admin@modsemi.dev',
28
+ roles: ['admin'] as string[],
29
+ avatar: undefined as string | undefined,
30
+ },
31
+ },
32
+ ];
33
+
34
+ export const useAuthStore = create<AuthState & AuthActions>()(
35
+ persist(
36
+ set => ({
37
+ currentUser: null,
38
+ isLoggedIn: false,
39
+
40
+ login: (username: string, password: string) => {
41
+ const found = MOCK_USERS.find(
42
+ u => u.username === username && u.password === password,
43
+ );
44
+ if (found) {
45
+ set({ currentUser: found.user, isLoggedIn: true });
46
+ return true;
47
+ }
48
+ return false;
49
+ },
50
+
51
+ logout: () => set({ currentUser: null, isLoggedIn: false }),
52
+ }),
53
+ {
54
+ name: 'modsemi-auth',
55
+ partialize: state => ({
56
+ currentUser: state.currentUser,
57
+ isLoggedIn: state.isLoggedIn,
58
+ }),
59
+ },
60
+ ),
61
+ );