snice 3.5.0 → 3.7.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 (66) hide show
  1. package/bin/snice.js +2 -1
  2. package/bin/templates/CLAUDE.md +24 -2
  3. package/bin/templates/pwa/README.md +173 -0
  4. package/bin/templates/pwa/global.d.ts +10 -0
  5. package/bin/templates/pwa/index.html +17 -0
  6. package/bin/templates/pwa/package.json +25 -0
  7. package/bin/templates/pwa/public/icons/.gitkeep +6 -0
  8. package/bin/templates/pwa/public/manifest.json +24 -0
  9. package/bin/templates/pwa/public/vite.svg +1 -0
  10. package/bin/templates/pwa/src/daemons/notifications.ts +148 -0
  11. package/bin/templates/pwa/src/guards/auth.ts +10 -0
  12. package/bin/templates/pwa/src/main.ts +42 -0
  13. package/bin/templates/pwa/src/middleware/auth.ts +15 -0
  14. package/bin/templates/pwa/src/middleware/error.ts +25 -0
  15. package/bin/templates/pwa/src/middleware/retry.ts +27 -0
  16. package/bin/templates/pwa/src/pages/dashboard.ts +142 -0
  17. package/bin/templates/pwa/src/pages/login.ts +161 -0
  18. package/bin/templates/pwa/src/pages/notifications.ts +156 -0
  19. package/bin/templates/pwa/src/pages/profile.ts +163 -0
  20. package/bin/templates/pwa/src/router.ts +16 -0
  21. package/bin/templates/pwa/src/services/auth.ts +48 -0
  22. package/bin/templates/pwa/src/services/jwt.ts +35 -0
  23. package/bin/templates/pwa/src/services/storage.ts +24 -0
  24. package/bin/templates/pwa/src/styles/global.css +55 -0
  25. package/bin/templates/pwa/src/types/auth.ts +21 -0
  26. package/bin/templates/pwa/src/types/notifications.ts +9 -0
  27. package/bin/templates/pwa/src/utils/fetch.ts +39 -0
  28. package/bin/templates/pwa/tsconfig.json +23 -0
  29. package/bin/templates/pwa/vite.config.ts +94 -0
  30. package/dist/components/audio-recorder/snice-audio-recorder.d.ts +14 -4
  31. package/dist/components/audio-recorder/snice-audio-recorder.js +248 -71
  32. package/dist/components/audio-recorder/snice-audio-recorder.js.map +1 -1
  33. package/dist/components/audio-recorder/snice-audio-recorder.types.d.ts +2 -0
  34. package/dist/components/music-player/snice-music-player.d.ts +72 -0
  35. package/dist/components/music-player/snice-music-player.js +730 -0
  36. package/dist/components/music-player/snice-music-player.js.map +1 -0
  37. package/dist/components/music-player/snice-music-player.types.d.ts +43 -0
  38. package/dist/components/timer/snice-timer.d.ts +27 -0
  39. package/dist/components/timer/snice-timer.js +197 -0
  40. package/dist/components/timer/snice-timer.js.map +1 -0
  41. package/dist/components/timer/snice-timer.types.d.ts +10 -0
  42. package/dist/fetcher.d.ts +65 -0
  43. package/dist/index.cjs +92 -3
  44. package/dist/index.cjs.map +1 -1
  45. package/dist/index.d.ts +2 -0
  46. package/dist/index.esm.js +92 -4
  47. package/dist/index.esm.js.map +1 -1
  48. package/dist/index.iife.js +92 -3
  49. package/dist/index.iife.js.map +1 -1
  50. package/dist/symbols.cjs +1 -1
  51. package/dist/symbols.esm.js +1 -1
  52. package/dist/transitions.cjs +1 -1
  53. package/dist/transitions.esm.js +1 -1
  54. package/dist/types/context.d.ts +7 -1
  55. package/dist/types/router-options.d.ts +6 -0
  56. package/docs/ai/api.md +33 -1
  57. package/docs/ai/components/music-player.md +134 -0
  58. package/docs/ai/components/terminal.md +147 -0
  59. package/docs/ai/components/timer.md +43 -0
  60. package/docs/ai/patterns.md +47 -0
  61. package/docs/components/music-player.md +314 -0
  62. package/docs/components/terminal.md +451 -0
  63. package/docs/components/timer.md +143 -0
  64. package/docs/fetcher.md +447 -0
  65. package/docs/routing.md +2 -0
  66. package/package.json +2 -1
@@ -0,0 +1,142 @@
1
+ import { page } from '../router';
2
+ import { render, styles, html, css, context } from 'snice';
3
+ import type { Placard, Context } from 'snice';
4
+ import type { AppContext } from '../types/auth';
5
+ import { authGuard } from '../guards/auth';
6
+
7
+ const placard: Placard = {
8
+ name: 'dashboard',
9
+ title: 'Dashboard',
10
+ icon: '📊',
11
+ show: true,
12
+ order: 1
13
+ };
14
+
15
+ @page({ tag: 'dashboard-page', routes: ['/', '/dashboard'], guards: [authGuard], placard })
16
+ export class DashboardPage extends HTMLElement {
17
+ userName = '';
18
+
19
+ @context()
20
+ handleContext(ctx: Context<AppContext>) {
21
+ this.userName = ctx.application.user?.name || 'User';
22
+ }
23
+
24
+ @render()
25
+ renderContent() {
26
+ return html`
27
+ <div class="container">
28
+ <h1>Welcome, ${this.userName}!</h1>
29
+ <p class="subtitle">This is your dashboard</p>
30
+
31
+ <div class="grid">
32
+ <snice-card>
33
+ <h3>⚡ Features</h3>
34
+ <ul>
35
+ <li>JWT Authentication</li>
36
+ <li>Protected Routes</li>
37
+ <li>Middleware Pattern</li>
38
+ <li>Service Worker</li>
39
+ <li>Live Notifications</li>
40
+ </ul>
41
+ </snice-card>
42
+
43
+ <snice-card>
44
+ <h3>🛠️ Architecture</h3>
45
+ <ul>
46
+ <li><strong>Utils:</strong> Pure functions</li>
47
+ <li><strong>Services:</strong> Business logic</li>
48
+ <li><strong>Middleware:</strong> Fetch interceptors</li>
49
+ <li><strong>Daemons:</strong> Lifecycle classes</li>
50
+ <li><strong>Guards:</strong> Route protection</li>
51
+ </ul>
52
+ </snice-card>
53
+
54
+ <snice-card>
55
+ <h3>📦 What's Included</h3>
56
+ <ul>
57
+ <li>TypeScript configuration</li>
58
+ <li>Vite + SWC setup</li>
59
+ <li>PWA manifest</li>
60
+ <li>Mock authentication</li>
61
+ <li>WebSocket daemon example</li>
62
+ </ul>
63
+ </snice-card>
64
+
65
+ <snice-card>
66
+ <h3>🚀 Get Started</h3>
67
+ <p>Check out the other pages:</p>
68
+ <div class="links">
69
+ <a href="#/profile">
70
+ <snice-button variant="secondary">View Profile</snice-button>
71
+ </a>
72
+ <a href="#/notifications">
73
+ <snice-button variant="secondary">Notifications</snice-button>
74
+ </a>
75
+ </div>
76
+ </snice-card>
77
+ </div>
78
+ </div>
79
+ `;
80
+ }
81
+
82
+ @styles()
83
+ componentStyles() {
84
+ return css`
85
+ .container {
86
+ padding: 2rem;
87
+ max-width: 1200px;
88
+ margin: 0 auto;
89
+ }
90
+
91
+ h1 {
92
+ color: var(--primary-color);
93
+ margin: 0 0 0.5rem 0;
94
+ }
95
+
96
+ .subtitle {
97
+ color: var(--text-light);
98
+ margin: 0 0 2rem 0;
99
+ }
100
+
101
+ .grid {
102
+ display: grid;
103
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
104
+ gap: 1.5rem;
105
+ }
106
+
107
+ snice-card {
108
+ padding: 1.5rem;
109
+ }
110
+
111
+ h3 {
112
+ margin: 0 0 1rem 0;
113
+ color: var(--primary-color);
114
+ }
115
+
116
+ ul {
117
+ margin: 0;
118
+ padding-left: 1.5rem;
119
+ }
120
+
121
+ li {
122
+ margin: 0.5rem 0;
123
+ color: var(--text-color);
124
+ }
125
+
126
+ .links {
127
+ display: flex;
128
+ flex-direction: column;
129
+ gap: 0.75rem;
130
+ margin-top: 1rem;
131
+ }
132
+
133
+ .links a {
134
+ text-decoration: none;
135
+ }
136
+
137
+ snice-button {
138
+ width: 100%;
139
+ }
140
+ `;
141
+ }
142
+ }
@@ -0,0 +1,161 @@
1
+ import { page } from '../router';
2
+ import { render, styles, html, css, context } from 'snice';
3
+ import type { Placard, Context } from 'snice';
4
+ import type { AppContext } from '../types/auth';
5
+ import { login } from '../services/auth';
6
+
7
+ const placard: Placard = {
8
+ name: 'login',
9
+ title: 'Login',
10
+ show: false
11
+ };
12
+
13
+ @page({ tag: 'login-page', routes: ['/login'], placard })
14
+ export class LoginPage extends HTMLElement {
15
+ email = 'demo@example.com';
16
+ password = 'demo';
17
+ error = '';
18
+ loading = false;
19
+
20
+ private ctx?: Context<AppContext>;
21
+
22
+ @context()
23
+ handleContext(ctx: Context<AppContext>) {
24
+ this.ctx = ctx;
25
+ }
26
+
27
+ async handleSubmit(e: Event) {
28
+ e.preventDefault();
29
+ this.error = '';
30
+ this.loading = true;
31
+
32
+ try {
33
+ const result = await login({ email: this.email, password: this.password });
34
+
35
+ // Update context with new auth state
36
+ if (this.ctx) {
37
+ this.ctx.application.user = result.user;
38
+ this.ctx.application.isAuthenticated = true;
39
+ this.ctx.update();
40
+ }
41
+
42
+ window.location.href = '#/dashboard';
43
+ } catch (err) {
44
+ this.error = err instanceof Error ? err.message : 'Login failed';
45
+ } finally {
46
+ this.loading = false;
47
+ }
48
+ }
49
+
50
+ @render()
51
+ renderContent() {
52
+ return html`
53
+ <div class="container">
54
+ <snice-card class="login-card">
55
+ <h1>{{projectName}}</h1>
56
+ <p class="subtitle">Sign in to your account</p>
57
+
58
+ <form @submit=${this.handleSubmit}>
59
+ <snice-input
60
+ label="Email"
61
+ type="email"
62
+ .value=${this.email}
63
+ @input=${(e: Event) => this.email = (e.target as HTMLInputElement).value}
64
+ required
65
+ ></snice-input>
66
+
67
+ <snice-input
68
+ label="Password"
69
+ type="password"
70
+ .value=${this.password}
71
+ @input=${(e: Event) => this.password = (e.target as HTMLInputElement).value}
72
+ required
73
+ ></snice-input>
74
+
75
+ <if ${this.error}>
76
+ <snice-alert variant="error" class="error">
77
+ ${this.error}
78
+ </snice-alert>
79
+ </if>
80
+
81
+ <snice-button
82
+ type="submit"
83
+ variant="primary"
84
+ size="large"
85
+ ?loading=${this.loading}
86
+ full-width
87
+ >
88
+ Sign In
89
+ </snice-button>
90
+ </form>
91
+
92
+ <div class="hint">
93
+ <p>Demo credentials:</p>
94
+ <p><strong>Email:</strong> demo@example.com</p>
95
+ <p><strong>Password:</strong> demo</p>
96
+ </div>
97
+ </snice-card>
98
+ </div>
99
+ `;
100
+ }
101
+
102
+ @styles()
103
+ componentStyles() {
104
+ return css`
105
+ :host {
106
+ display: block;
107
+ min-height: 100vh;
108
+ background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
109
+ }
110
+
111
+ .container {
112
+ display: flex;
113
+ align-items: center;
114
+ justify-content: center;
115
+ min-height: 100vh;
116
+ padding: 2rem;
117
+ }
118
+
119
+ .login-card {
120
+ width: 100%;
121
+ max-width: 400px;
122
+ padding: 2rem;
123
+ }
124
+
125
+ h1 {
126
+ text-align: center;
127
+ color: var(--primary-color);
128
+ margin: 0 0 0.5rem 0;
129
+ }
130
+
131
+ .subtitle {
132
+ text-align: center;
133
+ color: var(--text-light);
134
+ margin: 0 0 2rem 0;
135
+ }
136
+
137
+ form {
138
+ display: flex;
139
+ flex-direction: column;
140
+ gap: 1rem;
141
+ }
142
+
143
+ .error {
144
+ margin: 0;
145
+ }
146
+
147
+ .hint {
148
+ margin-top: 1.5rem;
149
+ padding-top: 1.5rem;
150
+ border-top: 1px solid var(--border-color);
151
+ text-align: center;
152
+ font-size: 0.875rem;
153
+ color: var(--text-light);
154
+ }
155
+
156
+ .hint p {
157
+ margin: 0.25rem 0;
158
+ }
159
+ `;
160
+ }
161
+ }
@@ -0,0 +1,156 @@
1
+ import { page } from '../router';
2
+ import { render, styles, html, css, ready, dispose } from 'snice';
3
+ import type { Placard } from 'snice';
4
+ import { authGuard } from '../guards/auth';
5
+ import { getNotificationsDaemon } from '../daemons/notifications';
6
+ import type { Notification } from '../types/notifications';
7
+
8
+ const placard: Placard = {
9
+ name: 'notifications',
10
+ title: 'Notifications',
11
+ icon: '🔔',
12
+ show: true,
13
+ order: 3
14
+ };
15
+
16
+ @page({ tag: 'notifications-page', routes: ['/notifications'], guards: [authGuard], placard })
17
+ export class NotificationsPage extends HTMLElement {
18
+ notifications: Notification[] = [];
19
+ private unsubscribe: (() => void) | null = null;
20
+
21
+ @ready()
22
+ initialize() {
23
+ const daemon = getNotificationsDaemon();
24
+ this.unsubscribe = daemon.subscribe((notification) => {
25
+ this.notifications = [notification, ...this.notifications];
26
+ });
27
+ }
28
+
29
+ @dispose()
30
+ cleanup() {
31
+ if (this.unsubscribe) {
32
+ this.unsubscribe();
33
+ this.unsubscribe = null;
34
+ }
35
+ }
36
+
37
+ getVariant(type: string): string {
38
+ const variants: Record<string, string> = {
39
+ info: 'info',
40
+ success: 'success',
41
+ warning: 'warning',
42
+ error: 'danger'
43
+ };
44
+ return variants[type] || 'info';
45
+ }
46
+
47
+ clearAll() {
48
+ this.notifications = [];
49
+ }
50
+
51
+ remove(id: string) {
52
+ this.notifications = this.notifications.filter(n => n.id !== id);
53
+ }
54
+
55
+ @render()
56
+ renderContent() {
57
+ return html`
58
+ <div class="container">
59
+ <div class="header">
60
+ <h1>Notifications</h1>
61
+ <if ${this.notifications.length > 0}>
62
+ <snice-button
63
+ variant="secondary"
64
+ size="small"
65
+ @click=${this.clearAll}
66
+ >
67
+ Clear All
68
+ </snice-button>
69
+ </if>
70
+ </div>
71
+
72
+ <if ${this.notifications.length === 0}>
73
+ <snice-empty-state
74
+ icon="🔔"
75
+ title="No notifications"
76
+ description="You'll see live notifications here as they arrive"
77
+ ></snice-empty-state>
78
+ </if>
79
+
80
+ <if ${this.notifications.length > 0}>
81
+ <div class="notifications">
82
+ ${this.notifications.map(notification => html`
83
+ <snice-alert
84
+ key=${notification.id}
85
+ variant="${this.getVariant(notification.type)}"
86
+ dismissible
87
+ @dismiss=${() => this.remove(notification.id)}
88
+ >
89
+ <strong>${notification.title}</strong>
90
+ <p>${notification.message}</p>
91
+ <small>${new Date(notification.timestamp).toLocaleTimeString()}</small>
92
+ </snice-alert>
93
+ `)}
94
+ </div>
95
+ </if>
96
+ </div>
97
+ `;
98
+ }
99
+
100
+ @styles()
101
+ componentStyles() {
102
+ return css`
103
+ .container {
104
+ padding: 2rem;
105
+ max-width: 800px;
106
+ margin: 0 auto;
107
+ }
108
+
109
+ .header {
110
+ display: flex;
111
+ justify-content: space-between;
112
+ align-items: center;
113
+ margin-bottom: 2rem;
114
+ }
115
+
116
+ h1 {
117
+ margin: 0;
118
+ color: var(--primary-color);
119
+ }
120
+
121
+ .notifications {
122
+ display: flex;
123
+ flex-direction: column;
124
+ gap: 1rem;
125
+ }
126
+
127
+ snice-alert {
128
+ animation: slideIn 0.3s ease-out;
129
+ }
130
+
131
+ @keyframes slideIn {
132
+ from {
133
+ opacity: 0;
134
+ transform: translateX(-20px);
135
+ }
136
+ to {
137
+ opacity: 1;
138
+ transform: translateX(0);
139
+ }
140
+ }
141
+
142
+ snice-alert strong {
143
+ display: block;
144
+ margin-bottom: 0.5rem;
145
+ }
146
+
147
+ snice-alert p {
148
+ margin: 0 0 0.5rem 0;
149
+ }
150
+
151
+ snice-alert small {
152
+ opacity: 0.7;
153
+ }
154
+ `;
155
+ }
156
+ }
@@ -0,0 +1,163 @@
1
+ import { page } from '../router';
2
+ import { render, styles, html, css, context } from 'snice';
3
+ import type { Placard, Context } from 'snice';
4
+ import type { AppContext, User } from '../types/auth';
5
+ import { authGuard } from '../guards/auth';
6
+ import { logout } from '../services/auth';
7
+
8
+ const placard: Placard = {
9
+ name: 'profile',
10
+ title: 'Profile',
11
+ icon: '👤',
12
+ show: true,
13
+ order: 2
14
+ };
15
+
16
+ @page({ tag: 'profile-page', routes: ['/profile'], guards: [authGuard], placard })
17
+ export class ProfilePage extends HTMLElement {
18
+ user: User | null = null;
19
+ private ctx?: Context<AppContext>;
20
+
21
+ @context()
22
+ handleContext(ctx: Context<AppContext>) {
23
+ this.ctx = ctx;
24
+ this.user = ctx.application.user;
25
+ }
26
+
27
+ async handleLogout() {
28
+ await logout();
29
+
30
+ // Update context to reflect logged out state
31
+ if (this.ctx) {
32
+ this.ctx.application.user = null;
33
+ this.ctx.application.isAuthenticated = false;
34
+ this.ctx.update();
35
+ }
36
+
37
+ window.location.href = '#/login';
38
+ }
39
+
40
+ @render()
41
+ renderContent() {
42
+ if (!this.user) {
43
+ return html`<div class="container"><snice-spinner></snice-spinner></div>`;
44
+ }
45
+
46
+ return html`
47
+ <div class="container">
48
+ <snice-card class="profile-card">
49
+ <div class="header">
50
+ <snice-avatar
51
+ src="${this.user.avatar}"
52
+ name="${this.user.name}"
53
+ size="large"
54
+ ></snice-avatar>
55
+ <h1>${this.user.name}</h1>
56
+ <p class="email">${this.user.email}</p>
57
+ </div>
58
+
59
+ <div class="section">
60
+ <h3>Account Information</h3>
61
+ <div class="info-grid">
62
+ <div class="info-item">
63
+ <span class="label">User ID:</span>
64
+ <span class="value">${this.user.id}</span>
65
+ </div>
66
+ <div class="info-item">
67
+ <span class="label">Email:</span>
68
+ <span class="value">${this.user.email}</span>
69
+ </div>
70
+ <div class="info-item">
71
+ <span class="label">Name:</span>
72
+ <span class="value">${this.user.name}</span>
73
+ </div>
74
+ </div>
75
+ </div>
76
+
77
+ <div class="section">
78
+ <h3>Session</h3>
79
+ <p>You are currently logged in with JWT authentication.</p>
80
+ <snice-button
81
+ variant="danger"
82
+ @click=${this.handleLogout}
83
+ >
84
+ Sign Out
85
+ </snice-button>
86
+ </div>
87
+ </snice-card>
88
+ </div>
89
+ `;
90
+ }
91
+
92
+ @styles()
93
+ componentStyles() {
94
+ return css`
95
+ .container {
96
+ padding: 2rem;
97
+ max-width: 800px;
98
+ margin: 0 auto;
99
+ }
100
+
101
+ .profile-card {
102
+ padding: 2rem;
103
+ }
104
+
105
+ .header {
106
+ text-align: center;
107
+ padding-bottom: 2rem;
108
+ border-bottom: 1px solid var(--border-color);
109
+ }
110
+
111
+ snice-avatar {
112
+ margin-bottom: 1rem;
113
+ }
114
+
115
+ h1 {
116
+ margin: 0 0 0.5rem 0;
117
+ color: var(--primary-color);
118
+ }
119
+
120
+ .email {
121
+ margin: 0;
122
+ color: var(--text-light);
123
+ }
124
+
125
+ .section {
126
+ padding-top: 2rem;
127
+ }
128
+
129
+ h3 {
130
+ margin: 0 0 1rem 0;
131
+ color: var(--primary-color);
132
+ }
133
+
134
+ .info-grid {
135
+ display: flex;
136
+ flex-direction: column;
137
+ gap: 1rem;
138
+ }
139
+
140
+ .info-item {
141
+ display: flex;
142
+ justify-content: space-between;
143
+ padding: 0.75rem;
144
+ background: var(--bg-secondary);
145
+ border-radius: var(--radius-sm);
146
+ }
147
+
148
+ .label {
149
+ font-weight: 600;
150
+ color: var(--text-color);
151
+ }
152
+
153
+ .value {
154
+ color: var(--text-light);
155
+ }
156
+
157
+ .section p {
158
+ margin: 0 0 1rem 0;
159
+ color: var(--text-light);
160
+ }
161
+ `;
162
+ }
163
+ }
@@ -0,0 +1,16 @@
1
+ import { Router } from 'snice';
2
+ import type { AppContext } from './types/auth';
3
+ import { getUser } from './services/storage';
4
+ import { isAuthenticated } from './services/auth';
5
+
6
+ const { page, initialize, navigate } = Router<AppContext>({
7
+ target: '#app',
8
+ type: 'hash',
9
+ layout: 'snice-layout',
10
+ context: {
11
+ user: getUser(),
12
+ isAuthenticated: isAuthenticated()
13
+ }
14
+ });
15
+
16
+ export { page, initialize, navigate };
@@ -0,0 +1,48 @@
1
+ import type { LoginCredentials, LoginResponse, User } from '../types/auth';
2
+ import { setToken, setUser, clearToken, getToken } from './storage';
3
+ import { isTokenExpired } from './jwt';
4
+
5
+ // Mock API - replace with real API calls
6
+ const MOCK_USER: User = {
7
+ id: '1',
8
+ email: 'demo@example.com',
9
+ name: 'Demo User',
10
+ avatar: 'https://i.pravatar.cc/150?img=1'
11
+ };
12
+
13
+ const MOCK_TOKEN = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIiwiZW1haWwiOiJkZW1vQGV4YW1wbGUuY29tIiwiaWF0IjoxNjE2MjM5MDIyLCJleHAiOjk5OTk5OTk5OTl9.mock';
14
+
15
+ export async function login(credentials: LoginCredentials): Promise<LoginResponse> {
16
+ // Simulate API call
17
+ await new Promise(resolve => setTimeout(resolve, 500));
18
+
19
+ // Mock validation
20
+ if (credentials.email === 'demo@example.com' && credentials.password === 'demo') {
21
+ setToken(MOCK_TOKEN);
22
+ setUser(MOCK_USER);
23
+
24
+ return {
25
+ token: MOCK_TOKEN,
26
+ user: MOCK_USER
27
+ };
28
+ }
29
+
30
+ throw new Error('Invalid credentials');
31
+ }
32
+
33
+ export async function logout(): Promise<void> {
34
+ clearToken();
35
+ }
36
+
37
+ export function isAuthenticated(): boolean {
38
+ const token = getToken();
39
+ if (!token) return false;
40
+ return !isTokenExpired(token);
41
+ }
42
+
43
+ export async function refreshToken(): Promise<string> {
44
+ // Mock refresh - in real app, call refresh endpoint
45
+ await new Promise(resolve => setTimeout(resolve, 300));
46
+ setToken(MOCK_TOKEN);
47
+ return MOCK_TOKEN;
48
+ }