anhedral 0.1.1 → 0.1.4
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 +145 -35
- package/dist/cli.d.ts +10 -0
- package/dist/cli.js +67 -0
- package/dist/commands.d.ts +3 -8
- package/dist/commands.js +14 -64
- package/dist/dependencies.d.ts +35 -0
- package/dist/dependencies.js +175 -0
- package/dist/index.js +8 -134
- package/dist/print.d.ts +8 -0
- package/dist/print.js +33 -0
- package/dist/scaffold.d.ts +6 -7
- package/dist/scaffold.js +746 -419
- package/dist/templates/backend.d.ts +1 -1
- package/dist/templates/backend.js +117 -195
- package/dist/templates/extension.d.ts +1 -1
- package/dist/templates/extension.js +266 -68
- package/dist/templates/frontend.d.ts +1 -1
- package/dist/templates/frontend.js +86 -173
- package/dist/toolchain.d.ts +0 -3
- package/dist/toolchain.js +5 -16
- package/dist/util.d.ts +1 -0
- package/dist/util.js +65 -8
- package/package.json +12 -13
- package/dist/templates/default-web.d.ts +0 -2
- package/dist/templates/default-web.js +0 -1936
|
@@ -1,37 +1,57 @@
|
|
|
1
|
+
import { rmSync } from 'node:fs';
|
|
1
2
|
import path from 'node:path';
|
|
2
3
|
import { writeFile, exec } from '../util.js';
|
|
4
|
+
import { anhedralPrint } from '../print.js';
|
|
5
|
+
import { EXTENSION_DEPENDENCIES } from '../dependencies.js';
|
|
3
6
|
import { resolveToolchainChannel, resolveToolchain, toolPackageRef } from '../toolchain.js';
|
|
4
|
-
export async function scaffoldExtension(root, { projectName, displayName }) {
|
|
5
|
-
const
|
|
7
|
+
export async function scaffoldExtension(root, { projectName, displayName, skipInstall }) {
|
|
8
|
+
const appsRoot = path.join(root, 'apps');
|
|
9
|
+
const dir = path.join(appsRoot, 'extension');
|
|
6
10
|
const toolchain = resolveToolchain(resolveToolchainChannel(process.env.ANHEDRAL_TOOLCHAIN));
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
+
anhedralPrint.section('Chrome extension (WXT)');
|
|
12
|
+
anhedralPrint.step('Scaffolding WXT extension');
|
|
13
|
+
writeFile(path.join(appsRoot, '.gitkeep'), '');
|
|
14
|
+
exec(`pnpm dlx ${toolPackageRef('wxt', toolchain.wxt)} init extension -t react --pm pnpm`, appsRoot);
|
|
11
15
|
writePackageJson(dir, projectName);
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
anhedralPrint.done('WXT extension scaffolded');
|
|
17
|
+
anhedralPrint.step('Installing Clerk + React + Tailwind dependencies');
|
|
18
|
+
if (skipInstall) {
|
|
19
|
+
anhedralPrint.info('Skipping extension dependency install (--skip-install)');
|
|
20
|
+
anhedralPrint.info('Run after init: pnpm install');
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
exec('pnpm install --no-frozen-lockfile', dir);
|
|
24
|
+
}
|
|
25
|
+
anhedralPrint.done(skipInstall ? 'Extension dependency manifests written' : 'Extension dependencies installed');
|
|
26
|
+
cleanWxtStarterFiles(dir);
|
|
17
27
|
writeWxtConfig(dir, displayName);
|
|
18
28
|
writeTsConfig(dir);
|
|
19
29
|
writeEnvExample(dir);
|
|
30
|
+
writeEnvFile(dir);
|
|
20
31
|
writePostcssConfig(dir);
|
|
21
32
|
writeTailwindConfig(dir);
|
|
22
33
|
writeShadcnConfig(dir);
|
|
34
|
+
writeReadme(dir, displayName);
|
|
23
35
|
writeCnUtil(dir);
|
|
24
|
-
|
|
36
|
+
writeButtonComponent(dir);
|
|
25
37
|
writeAuthContext(dir);
|
|
26
38
|
writeApiClient(dir);
|
|
27
39
|
writeBackground(dir);
|
|
40
|
+
writeContentScript(dir);
|
|
28
41
|
writeSidepanelEntry(dir);
|
|
29
42
|
writeSidepanelHtml(dir, displayName);
|
|
30
43
|
writeSidepanelApp(dir);
|
|
31
44
|
writeStyles(dir);
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
45
|
+
anhedralPrint.done('Extension source files written');
|
|
46
|
+
}
|
|
47
|
+
function cleanWxtStarterFiles(dir) {
|
|
48
|
+
for (const relativePath of [
|
|
49
|
+
'entrypoints',
|
|
50
|
+
'assets/react.svg',
|
|
51
|
+
'public/wxt.svg',
|
|
52
|
+
]) {
|
|
53
|
+
rmSync(path.join(dir, relativePath), { recursive: true, force: true });
|
|
54
|
+
}
|
|
35
55
|
}
|
|
36
56
|
function writePackageJson(dir, projectName) {
|
|
37
57
|
writeFile(path.join(dir, 'package.json'), JSON.stringify({
|
|
@@ -49,6 +69,8 @@ function writePackageJson(dir, projectName) {
|
|
|
49
69
|
'zip:firefox': 'wxt zip -b firefox',
|
|
50
70
|
typecheck: 'tsc --noEmit',
|
|
51
71
|
},
|
|
72
|
+
dependencies: EXTENSION_DEPENDENCIES.dependencies,
|
|
73
|
+
devDependencies: EXTENSION_DEPENDENCIES.devDependencies,
|
|
52
74
|
}, null, 2) + '\n');
|
|
53
75
|
}
|
|
54
76
|
function writeWxtConfig(dir, displayName) {
|
|
@@ -64,11 +86,15 @@ export default defineConfig({
|
|
|
64
86
|
description: '${displayName} Chrome Extension',
|
|
65
87
|
version: '0.1.0',
|
|
66
88
|
...(crxPublicKey ? { key: crxPublicKey } : {}),
|
|
67
|
-
|
|
89
|
+
minimum_chrome_version: '114',
|
|
90
|
+
permissions: ['activeTab', 'cookies', 'storage', 'sidePanel'],
|
|
68
91
|
host_permissions: [],
|
|
69
92
|
action: {
|
|
70
93
|
default_title: 'Open ${displayName}',
|
|
71
94
|
},
|
|
95
|
+
side_panel: {
|
|
96
|
+
default_path: 'sidepanel.html',
|
|
97
|
+
},
|
|
72
98
|
};
|
|
73
99
|
},
|
|
74
100
|
modules: ['@wxt-dev/module-react'],
|
|
@@ -87,11 +113,11 @@ function writeTsConfig(dir) {
|
|
|
87
113
|
forceConsistentCasingInFileNames: true,
|
|
88
114
|
resolveJsonModule: true,
|
|
89
115
|
isolatedModules: true,
|
|
116
|
+
allowImportingTsExtensions: true,
|
|
90
117
|
noEmit: true,
|
|
91
118
|
jsx: 'react-jsx',
|
|
92
119
|
lib: ['DOM', 'DOM.Iterable', 'ESNext'],
|
|
93
120
|
types: ['wxt/browser', '@wxt-dev/module-react', 'chrome'],
|
|
94
|
-
baseUrl: '.',
|
|
95
121
|
paths: { '@/*': ['./src/*'] },
|
|
96
122
|
},
|
|
97
123
|
include: ['**/*.ts', '**/*.tsx', '.wxt/wxt.d.ts'],
|
|
@@ -111,12 +137,29 @@ VITE_WEBSITE_URL=http://localhost:8081
|
|
|
111
137
|
# Chrome Extension CRX public key (optional, for stable extension ID)
|
|
112
138
|
VITE_CRX_PUBLIC_KEY=
|
|
113
139
|
|
|
140
|
+
# RevenueCat Web Billing URL (optional, for subscription management)
|
|
141
|
+
VITE_RC_BILLING_URL=
|
|
142
|
+
`);
|
|
143
|
+
}
|
|
144
|
+
function writeEnvFile(dir) {
|
|
145
|
+
writeFile(path.join(dir, '.env'), `# Clerk Authentication
|
|
146
|
+
VITE_CLERK_PUBLISHABLE_KEY=
|
|
147
|
+
|
|
148
|
+
# Backend API URL
|
|
149
|
+
VITE_API_URL=http://localhost:8787
|
|
150
|
+
|
|
151
|
+
# Website URL (for sign-up and subscription links)
|
|
152
|
+
VITE_WEBSITE_URL=http://localhost:8081
|
|
153
|
+
|
|
154
|
+
# Chrome Extension CRX public key (optional, for stable extension ID)
|
|
155
|
+
VITE_CRX_PUBLIC_KEY=
|
|
156
|
+
|
|
114
157
|
# RevenueCat Web Billing URL (optional, for subscription management)
|
|
115
158
|
VITE_RC_BILLING_URL=
|
|
116
159
|
`);
|
|
117
160
|
}
|
|
118
161
|
function writePostcssConfig(dir) {
|
|
119
|
-
writeFile(path.join(dir, 'postcss.config.
|
|
162
|
+
writeFile(path.join(dir, 'postcss.config.cjs'), `module.exports = {
|
|
120
163
|
plugins: {
|
|
121
164
|
tailwindcss: {},
|
|
122
165
|
autoprefixer: {},
|
|
@@ -156,15 +199,115 @@ export function cn(...inputs: ClassValue[]) {
|
|
|
156
199
|
}
|
|
157
200
|
`);
|
|
158
201
|
}
|
|
202
|
+
function writeButtonComponent(dir) {
|
|
203
|
+
writeFile(path.join(dir, 'src/components/ui/button.tsx'), `import * as React from 'react';
|
|
204
|
+
import { cn } from '@/lib/utils';
|
|
205
|
+
|
|
206
|
+
type ButtonVariant = 'default' | 'outline';
|
|
207
|
+
|
|
208
|
+
export type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
|
|
209
|
+
variant?: ButtonVariant;
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const variantClasses: Record<ButtonVariant, string> = {
|
|
213
|
+
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
|
214
|
+
outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
218
|
+
({ className, variant = 'default', type = 'button', ...props }, ref) => {
|
|
219
|
+
return (
|
|
220
|
+
<button
|
|
221
|
+
ref={ref}
|
|
222
|
+
type={type}
|
|
223
|
+
className={cn(
|
|
224
|
+
'inline-flex h-9 items-center justify-center whitespace-nowrap rounded-md px-4 py-2 text-sm font-medium transition-colors disabled:pointer-events-none disabled:opacity-50',
|
|
225
|
+
variantClasses[variant],
|
|
226
|
+
className,
|
|
227
|
+
)}
|
|
228
|
+
{...props}
|
|
229
|
+
/>
|
|
230
|
+
);
|
|
231
|
+
},
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
Button.displayName = 'Button';
|
|
235
|
+
`);
|
|
236
|
+
}
|
|
159
237
|
function writeTailwindConfig(dir) {
|
|
160
|
-
writeFile(path.join(dir, 'tailwind.config.
|
|
238
|
+
writeFile(path.join(dir, 'tailwind.config.cjs'), `/** @type {import('tailwindcss').Config} */
|
|
161
239
|
module.exports = {
|
|
162
240
|
content: ['./src/**/*.{ts,tsx,html}'],
|
|
163
|
-
theme: {
|
|
241
|
+
theme: {
|
|
242
|
+
extend: {
|
|
243
|
+
colors: {
|
|
244
|
+
border: 'hsl(var(--border))',
|
|
245
|
+
input: 'hsl(var(--input))',
|
|
246
|
+
ring: 'hsl(var(--ring))',
|
|
247
|
+
background: 'hsl(var(--background))',
|
|
248
|
+
foreground: 'hsl(var(--foreground))',
|
|
249
|
+
primary: {
|
|
250
|
+
DEFAULT: 'hsl(var(--primary))',
|
|
251
|
+
foreground: 'hsl(var(--primary-foreground))',
|
|
252
|
+
},
|
|
253
|
+
secondary: {
|
|
254
|
+
DEFAULT: 'hsl(var(--secondary))',
|
|
255
|
+
foreground: 'hsl(var(--secondary-foreground))',
|
|
256
|
+
},
|
|
257
|
+
destructive: {
|
|
258
|
+
DEFAULT: 'hsl(var(--destructive))',
|
|
259
|
+
foreground: 'hsl(var(--destructive-foreground))',
|
|
260
|
+
},
|
|
261
|
+
muted: {
|
|
262
|
+
DEFAULT: 'hsl(var(--muted))',
|
|
263
|
+
foreground: 'hsl(var(--muted-foreground))',
|
|
264
|
+
},
|
|
265
|
+
accent: {
|
|
266
|
+
DEFAULT: 'hsl(var(--accent))',
|
|
267
|
+
foreground: 'hsl(var(--accent-foreground))',
|
|
268
|
+
},
|
|
269
|
+
popover: {
|
|
270
|
+
DEFAULT: 'hsl(var(--popover))',
|
|
271
|
+
foreground: 'hsl(var(--popover-foreground))',
|
|
272
|
+
},
|
|
273
|
+
card: {
|
|
274
|
+
DEFAULT: 'hsl(var(--card))',
|
|
275
|
+
foreground: 'hsl(var(--card-foreground))',
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
borderRadius: {
|
|
279
|
+
lg: 'var(--radius)',
|
|
280
|
+
md: 'calc(var(--radius) - 2px)',
|
|
281
|
+
sm: 'calc(var(--radius) - 4px)',
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
},
|
|
164
285
|
plugins: [],
|
|
165
286
|
};
|
|
166
287
|
`);
|
|
167
288
|
}
|
|
289
|
+
function writeReadme(dir, displayName) {
|
|
290
|
+
writeFile(path.join(dir, 'README.md'), `# ${displayName} Chrome Extension
|
|
291
|
+
|
|
292
|
+
WXT side-panel extension generated by anhedral.
|
|
293
|
+
|
|
294
|
+
## Development
|
|
295
|
+
|
|
296
|
+
\`\`\`bash
|
|
297
|
+
pnpm dev
|
|
298
|
+
pnpm build
|
|
299
|
+
pnpm zip
|
|
300
|
+
\`\`\`
|
|
301
|
+
|
|
302
|
+
Set \`VITE_CLERK_PUBLISHABLE_KEY\` and \`VITE_API_URL\` in \`.env\` before using auth-backed routes. Set \`VITE_CRX_PUBLIC_KEY\` only when you need a stable Chrome extension ID.
|
|
303
|
+
|
|
304
|
+
## Chrome
|
|
305
|
+
|
|
306
|
+
Run \`pnpm build\`, then load \`.output/chrome-mv3\` as an unpacked extension from \`chrome://extensions\`.
|
|
307
|
+
|
|
308
|
+
The extension uses Chrome's Side Panel API. The browser action opens \`sidepanel.html\`, \`wxt.config.ts\` declares the \`sidePanel\` permission and Chrome 114+ minimum version, the background script initializes Clerk, and the content script is ready for page-to-extension messages.
|
|
309
|
+
`);
|
|
310
|
+
}
|
|
168
311
|
function writeAuthContext(dir) {
|
|
169
312
|
writeFile(path.join(dir, 'src/contexts/auth-context.tsx'), `import * as React from 'react';
|
|
170
313
|
import { ClerkProvider, useAuth as useClerkAuth, useUser } from '@clerk/chrome-extension';
|
|
@@ -306,58 +449,25 @@ export { WEBSITE_URL };
|
|
|
306
449
|
`);
|
|
307
450
|
}
|
|
308
451
|
function writeApiClient(dir) {
|
|
309
|
-
writeFile(path.join(dir, 'src/lib/api.ts'), `
|
|
310
|
-
|
|
311
|
-
export type SubscriptionEntitlements = {
|
|
312
|
-
pro: boolean;
|
|
313
|
-
inTrial: boolean;
|
|
314
|
-
trialEndsAt?: string;
|
|
315
|
-
expiresAt?: string;
|
|
316
|
-
periodStart?: string;
|
|
317
|
-
periodEnd?: string;
|
|
318
|
-
method?: 'trialing' | 'redeemed' | 'paid' | null;
|
|
319
|
-
managementUrl?: string;
|
|
320
|
-
cancelAtPeriodEnd?: boolean;
|
|
321
|
-
};
|
|
452
|
+
writeFile(path.join(dir, 'src/lib/api.ts'), `import { ApiClient } from '@anhedral/api-client';
|
|
322
453
|
|
|
323
454
|
export class APIClient {
|
|
324
455
|
constructor(private getToken: () => Promise<string | null>) {}
|
|
325
456
|
|
|
326
|
-
private
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
'Content-Type': 'application/json',
|
|
333
|
-
'X-Client-Type': 'chrome-extension',
|
|
334
|
-
'X-Platform': 'chrome',
|
|
335
|
-
...(options.headers as Record<string, string>),
|
|
336
|
-
};
|
|
337
|
-
|
|
338
|
-
const response = await fetch(\`\${API_BASE_URL}\${endpoint}\`, { ...options, headers });
|
|
339
|
-
|
|
340
|
-
if (!response.ok) {
|
|
341
|
-
let error: { message?: string } = {};
|
|
342
|
-
try { error = await response.json(); } catch {}
|
|
343
|
-
throw new Error(error.message || \`Request failed (\${response.status})\`);
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
if (response.status === 204) return {} as T;
|
|
347
|
-
return response.json();
|
|
457
|
+
private client() {
|
|
458
|
+
return new ApiClient({
|
|
459
|
+
baseUrl: import.meta.env.VITE_API_URL || 'http://localhost:8787',
|
|
460
|
+
getToken: this.getToken,
|
|
461
|
+
platform: 'extension',
|
|
462
|
+
});
|
|
348
463
|
}
|
|
349
464
|
|
|
350
|
-
|
|
351
|
-
return this.
|
|
352
|
-
user: { id: string; email: string; subscriptionTier: string; subscriptionStatus: string };
|
|
353
|
-
}>('/auth/me');
|
|
465
|
+
getMe() {
|
|
466
|
+
return this.client().getMe();
|
|
354
467
|
}
|
|
355
468
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
? '/subscriptions/entitlements/me?refresh=true'
|
|
359
|
-
: '/subscriptions/entitlements/me';
|
|
360
|
-
return this.request<SubscriptionEntitlements>(path);
|
|
469
|
+
getSubscriptionEntitlements(options?: { refresh?: boolean }) {
|
|
470
|
+
return this.client().getSubscriptionEntitlements(options);
|
|
361
471
|
}
|
|
362
472
|
}
|
|
363
473
|
`);
|
|
@@ -365,20 +475,42 @@ export class APIClient {
|
|
|
365
475
|
function writeBackground(dir) {
|
|
366
476
|
writeFile(path.join(dir, 'src/entrypoints/background.ts'), `import { createClerkClient } from '@clerk/chrome-extension/background';
|
|
367
477
|
|
|
478
|
+
type ChromeWithSidePanel = typeof chrome & {
|
|
479
|
+
sidePanel: {
|
|
480
|
+
setPanelBehavior: (behavior: { openPanelOnActionClick: boolean }) => Promise<void>;
|
|
481
|
+
};
|
|
482
|
+
};
|
|
483
|
+
|
|
368
484
|
export default defineBackground(() => {
|
|
369
485
|
// Initialize Clerk in the background script for cookie-based auth
|
|
370
486
|
void createClerkClient({
|
|
371
487
|
publishableKey: import.meta.env.VITE_CLERK_PUBLISHABLE_KEY || '',
|
|
372
488
|
}).catch(() => {});
|
|
373
489
|
|
|
374
|
-
// Open the side panel when the extension icon is clicked
|
|
375
|
-
|
|
376
|
-
chrome.sidePanel
|
|
490
|
+
// Open the side panel when the extension icon is clicked.
|
|
491
|
+
(chrome as ChromeWithSidePanel).sidePanel
|
|
377
492
|
.setPanelBehavior({ openPanelOnActionClick: true })
|
|
378
493
|
.catch(() => {});
|
|
379
494
|
});
|
|
380
495
|
`);
|
|
381
496
|
}
|
|
497
|
+
function writeContentScript(dir) {
|
|
498
|
+
writeFile(path.join(dir, 'src/entrypoints/content.ts'), `export default defineContentScript({
|
|
499
|
+
matches: ['<all_urls>'],
|
|
500
|
+
main() {
|
|
501
|
+
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
|
502
|
+
if (message?.type !== 'ANHEDRAL_PAGE_SNAPSHOT') return false;
|
|
503
|
+
|
|
504
|
+
sendResponse({
|
|
505
|
+
title: document.title,
|
|
506
|
+
location: window.location.href,
|
|
507
|
+
});
|
|
508
|
+
return true;
|
|
509
|
+
});
|
|
510
|
+
},
|
|
511
|
+
});
|
|
512
|
+
`);
|
|
513
|
+
}
|
|
382
514
|
function writeSidepanelEntry(dir) {
|
|
383
515
|
writeFile(path.join(dir, 'src/entrypoints/sidepanel/main.tsx'), `import * as React from 'react';
|
|
384
516
|
import { createRoot } from 'react-dom/client';
|
|
@@ -418,9 +550,33 @@ function writeSidepanelApp(dir) {
|
|
|
418
550
|
writeFile(path.join(dir, 'src/entrypoints/sidepanel/app.tsx'), `import * as React from 'react';
|
|
419
551
|
import { useAuth } from '../../contexts/auth-context';
|
|
420
552
|
import { SignIn } from '@clerk/chrome-extension';
|
|
553
|
+
import { Button } from '../../components/ui/button';
|
|
554
|
+
|
|
555
|
+
type PageSnapshot = {
|
|
556
|
+
title: string;
|
|
557
|
+
location: string;
|
|
558
|
+
};
|
|
421
559
|
|
|
422
560
|
export function SidePanelApp() {
|
|
423
561
|
const { isSignedIn, isLoading, signOut, subscription } = useAuth();
|
|
562
|
+
const [page, setPage] = React.useState<PageSnapshot | null>(null);
|
|
563
|
+
const [pageError, setPageError] = React.useState<string | null>(null);
|
|
564
|
+
|
|
565
|
+
const readActivePage = React.useCallback(async () => {
|
|
566
|
+
setPageError(null);
|
|
567
|
+
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
|
568
|
+
if (!tab.id) {
|
|
569
|
+
setPageError('No active tab is available.');
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
try {
|
|
574
|
+
const response = await chrome.tabs.sendMessage(tab.id, { type: 'ANHEDRAL_PAGE_SNAPSHOT' });
|
|
575
|
+
setPage(response as PageSnapshot);
|
|
576
|
+
} catch {
|
|
577
|
+
setPageError('Refresh the active page, then try again.');
|
|
578
|
+
}
|
|
579
|
+
}, []);
|
|
424
580
|
|
|
425
581
|
if (isLoading) {
|
|
426
582
|
return <div style={{ padding: 24, textAlign: 'center' }}>Loading...</div>;
|
|
@@ -438,7 +594,15 @@ export function SidePanelApp() {
|
|
|
438
594
|
<div style={{ padding: 24 }}>
|
|
439
595
|
<h2>Welcome!</h2>
|
|
440
596
|
<p>Subscription: {subscription.status}</p>
|
|
441
|
-
<button onClick={
|
|
597
|
+
<Button type="button" onClick={() => void readActivePage()}>Read active page</Button>
|
|
598
|
+
{page ? (
|
|
599
|
+
<div style={{ marginTop: 16 }}>
|
|
600
|
+
<strong>{page.title || 'Untitled page'}</strong>
|
|
601
|
+
<p style={{ overflowWrap: 'anywhere' }}>{page.location}</p>
|
|
602
|
+
</div>
|
|
603
|
+
) : null}
|
|
604
|
+
{pageError ? <p style={{ color: 'hsl(var(--destructive))' }}>{pageError}</p> : null}
|
|
605
|
+
<Button type="button" variant="outline" onClick={signOut}>Sign Out</Button>
|
|
442
606
|
</div>
|
|
443
607
|
);
|
|
444
608
|
}
|
|
@@ -449,6 +613,40 @@ function writeStyles(dir) {
|
|
|
449
613
|
@tailwind components;
|
|
450
614
|
@tailwind utilities;
|
|
451
615
|
|
|
616
|
+
@layer base {
|
|
617
|
+
:root {
|
|
618
|
+
--background: 0 0% 100%;
|
|
619
|
+
--foreground: 0 0% 9%;
|
|
620
|
+
--card: 0 0% 100%;
|
|
621
|
+
--card-foreground: 0 0% 9%;
|
|
622
|
+
--popover: 0 0% 100%;
|
|
623
|
+
--popover-foreground: 0 0% 9%;
|
|
624
|
+
--primary: 0 0% 9%;
|
|
625
|
+
--primary-foreground: 0 0% 98%;
|
|
626
|
+
--secondary: 0 0% 96.1%;
|
|
627
|
+
--secondary-foreground: 0 0% 9%;
|
|
628
|
+
--muted: 0 0% 96.1%;
|
|
629
|
+
--muted-foreground: 0 0% 45.1%;
|
|
630
|
+
--accent: 0 0% 96.1%;
|
|
631
|
+
--accent-foreground: 0 0% 9%;
|
|
632
|
+
--destructive: 0 84.2% 60.2%;
|
|
633
|
+
--destructive-foreground: 0 0% 98%;
|
|
634
|
+
--border: 0 0% 89.8%;
|
|
635
|
+
--input: 0 0% 89.8%;
|
|
636
|
+
--ring: 0 0% 63.9%;
|
|
637
|
+
--radius: 0.5rem;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
* {
|
|
641
|
+
border-color: hsl(var(--border));
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
body {
|
|
645
|
+
background: hsl(var(--background));
|
|
646
|
+
color: hsl(var(--foreground));
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
452
650
|
body {
|
|
453
651
|
margin: 0;
|
|
454
652
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { ProjectOptions } from '../scaffold.js';
|
|
2
|
-
export declare function scaffoldFrontend(root: string, { projectName, displayName }: ProjectOptions): Promise<void>;
|
|
2
|
+
export declare function scaffoldFrontend(root: string, { projectName, displayName, skipInstall }: ProjectOptions): Promise<void>;
|