@nuraly/lumenjs 0.1.2 → 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 +76 -235
- package/dist/auth/config.d.ts +23 -0
- package/dist/auth/config.js +115 -0
- package/dist/auth/guard.d.ts +12 -0
- package/dist/auth/guard.js +28 -0
- package/dist/auth/index.d.ts +3 -0
- package/dist/auth/index.js +1 -0
- package/dist/auth/middleware.d.ts +23 -0
- package/dist/auth/middleware.js +89 -0
- package/dist/auth/native-auth.d.ts +73 -0
- package/dist/auth/native-auth.js +293 -0
- package/dist/auth/oidc-client.d.ts +17 -0
- package/dist/auth/oidc-client.js +123 -0
- package/dist/auth/providers/google.d.ts +23 -0
- package/dist/auth/providers/google.js +25 -0
- package/dist/auth/providers/index.d.ts +2 -0
- package/dist/auth/providers/index.js +1 -0
- package/dist/auth/routes/login.d.ts +8 -0
- package/dist/auth/routes/login.js +98 -0
- package/dist/auth/routes/logout.d.ts +4 -0
- package/dist/auth/routes/logout.js +79 -0
- package/dist/auth/routes/oidc-callback.d.ts +3 -0
- package/dist/auth/routes/oidc-callback.js +70 -0
- package/dist/auth/routes/password.d.ts +5 -0
- package/dist/auth/routes/password.js +149 -0
- package/dist/auth/routes/signup.d.ts +3 -0
- package/dist/auth/routes/signup.js +81 -0
- package/dist/auth/routes/token.d.ts +4 -0
- package/dist/auth/routes/token.js +70 -0
- package/dist/auth/routes/utils.d.ts +7 -0
- package/dist/auth/routes/utils.js +35 -0
- package/dist/auth/routes/verify.d.ts +3 -0
- package/dist/auth/routes/verify.js +26 -0
- package/dist/auth/routes.d.ts +8 -0
- package/dist/auth/routes.js +110 -0
- package/dist/auth/session.d.ts +8 -0
- package/dist/auth/session.js +54 -0
- package/dist/auth/token.d.ts +33 -0
- package/dist/auth/token.js +90 -0
- package/dist/auth/types.d.ts +156 -0
- package/dist/auth/types.js +2 -0
- package/dist/build/build-client.d.ts +15 -0
- package/dist/build/build-client.js +45 -0
- package/dist/build/build-prerender.d.ts +11 -0
- package/dist/build/build-prerender.js +159 -0
- package/dist/build/build-server.d.ts +17 -0
- package/dist/build/build-server.js +98 -0
- package/dist/build/build.js +52 -120
- package/dist/build/scan.d.ts +19 -0
- package/dist/build/scan.js +77 -6
- package/dist/build/serve-api.js +8 -2
- package/dist/build/serve-loaders.d.ts +4 -2
- package/dist/build/serve-loaders.js +128 -10
- package/dist/build/serve-ssr.js +38 -11
- package/dist/build/serve-static.js +3 -3
- package/dist/build/serve.js +229 -14
- package/dist/cli.js +37 -6
- package/dist/communication/encryption.d.ts +35 -0
- package/dist/communication/encryption.js +90 -0
- package/dist/communication/handlers/context.d.ts +27 -0
- package/dist/communication/handlers/context.js +1 -0
- package/dist/communication/handlers/conversation.d.ts +24 -0
- package/dist/communication/handlers/conversation.js +113 -0
- package/dist/communication/handlers/file-upload.d.ts +17 -0
- package/dist/communication/handlers/file-upload.js +62 -0
- package/dist/communication/handlers/messaging.d.ts +30 -0
- package/dist/communication/handlers/messaging.js +237 -0
- package/dist/communication/handlers/presence.d.ts +15 -0
- package/dist/communication/handlers/presence.js +76 -0
- package/dist/communication/handlers.d.ts +5 -0
- package/dist/communication/handlers.js +5 -0
- package/dist/communication/index.d.ts +9 -0
- package/dist/communication/index.js +7 -0
- package/dist/communication/link-preview.d.ts +18 -0
- package/dist/communication/link-preview.js +115 -0
- package/dist/communication/schema.d.ts +10 -0
- package/dist/communication/schema.js +101 -0
- package/dist/communication/server.d.ts +86 -0
- package/dist/communication/server.js +212 -0
- package/dist/communication/signaling.d.ts +43 -0
- package/dist/communication/signaling.js +271 -0
- package/dist/communication/store.d.ts +71 -0
- package/dist/communication/store.js +289 -0
- package/dist/communication/types.d.ts +454 -0
- package/dist/communication/types.js +1 -0
- package/dist/create.d.ts +1 -0
- package/dist/create.js +55 -0
- package/dist/db/auto-migrate.d.ts +3 -0
- package/dist/db/auto-migrate.js +100 -0
- package/dist/db/client.d.ts +3 -0
- package/dist/db/client.js +18 -0
- package/dist/db/context.d.ts +2 -0
- package/dist/db/context.js +9 -0
- package/dist/db/index.d.ts +23 -0
- package/dist/db/index.js +258 -0
- package/dist/db/seed.d.ts +12 -0
- package/dist/db/seed.js +88 -0
- package/dist/db/table.d.ts +10 -0
- package/dist/db/table.js +12 -0
- package/dist/dev-server/config.d.ts +14 -0
- package/dist/dev-server/config.js +26 -9
- package/dist/dev-server/index-html.d.ts +3 -0
- package/dist/dev-server/index-html.js +18 -6
- package/dist/dev-server/nuralyui-aliases.d.ts +0 -4
- package/dist/dev-server/nuralyui-aliases.js +115 -94
- package/dist/dev-server/plugins/vite-plugin-api-routes.js +29 -5
- package/dist/dev-server/plugins/vite-plugin-auth.d.ts +6 -0
- package/dist/dev-server/plugins/vite-plugin-auth.js +223 -0
- package/dist/dev-server/plugins/vite-plugin-auto-define.d.ts +16 -0
- package/dist/dev-server/plugins/vite-plugin-auto-define.js +111 -0
- package/dist/dev-server/plugins/vite-plugin-communication.d.ts +6 -0
- package/dist/dev-server/plugins/vite-plugin-communication.js +205 -0
- package/dist/dev-server/plugins/vite-plugin-editor-api.d.ts +6 -0
- package/dist/dev-server/plugins/vite-plugin-editor-api.js +318 -0
- package/dist/dev-server/plugins/vite-plugin-i18n.js +69 -2
- package/dist/dev-server/plugins/vite-plugin-lit-dedup.d.ts +6 -0
- package/dist/dev-server/plugins/vite-plugin-lit-dedup.js +78 -34
- package/dist/dev-server/plugins/vite-plugin-lit-hmr.js +44 -2
- package/dist/dev-server/plugins/vite-plugin-llms.d.ts +2 -0
- package/dist/dev-server/plugins/vite-plugin-llms.js +92 -0
- package/dist/dev-server/plugins/vite-plugin-loaders.d.ts +0 -1
- package/dist/dev-server/plugins/vite-plugin-loaders.js +311 -42
- package/dist/dev-server/plugins/vite-plugin-routes.js +18 -6
- package/dist/dev-server/plugins/vite-plugin-socketio.d.ts +2 -0
- package/dist/dev-server/plugins/vite-plugin-socketio.js +51 -0
- package/dist/dev-server/plugins/vite-plugin-source-annotator.d.ts +2 -0
- package/dist/dev-server/plugins/vite-plugin-source-annotator.js +26 -3
- package/dist/dev-server/plugins/vite-plugin-storage.d.ts +10 -0
- package/dist/dev-server/plugins/vite-plugin-storage.js +126 -0
- package/dist/dev-server/plugins/vite-plugin-virtual-modules.js +111 -2
- package/dist/dev-server/server.js +128 -12
- package/dist/dev-server/ssr-render.d.ts +2 -1
- package/dist/dev-server/ssr-render.js +107 -48
- package/dist/editor/ai/backend.d.ts +20 -0
- package/dist/editor/ai/backend.js +104 -0
- package/dist/editor/ai/claude-code-client.d.ts +20 -0
- package/dist/editor/ai/claude-code-client.js +145 -0
- package/dist/editor/ai/opencode-client.d.ts +14 -0
- package/dist/editor/ai/opencode-client.js +125 -0
- package/dist/editor/ai/snapshot-store.d.ts +22 -0
- package/dist/editor/ai/snapshot-store.js +35 -0
- package/dist/editor/ai/types.d.ts +30 -0
- package/dist/editor/ai/types.js +136 -0
- package/dist/editor/ai-chat-panel.d.ts +13 -0
- package/dist/editor/ai-chat-panel.js +587 -0
- package/dist/editor/ai-markdown.d.ts +10 -0
- package/dist/editor/ai-markdown.js +70 -0
- package/dist/editor/ai-project-panel.d.ts +11 -0
- package/dist/editor/ai-project-panel.js +332 -0
- package/dist/editor/ast-modification.d.ts +11 -0
- package/dist/editor/ast-modification.js +1 -0
- package/dist/editor/ast-service.d.ts +30 -0
- package/dist/editor/ast-service.js +180 -0
- package/dist/editor/css-rules.d.ts +54 -0
- package/dist/editor/css-rules.js +423 -0
- package/dist/editor/editor-api-client.d.ts +51 -0
- package/dist/editor/editor-api-client.js +162 -0
- package/dist/editor/editor-bridge.d.ts +1 -0
- package/dist/editor/editor-bridge.js +17 -8
- package/dist/editor/editor-toolbar.d.ts +14 -0
- package/dist/editor/editor-toolbar.js +115 -0
- package/dist/editor/file-editor.d.ts +9 -0
- package/dist/editor/file-editor.js +236 -0
- package/dist/editor/file-service.d.ts +16 -0
- package/dist/editor/file-service.js +52 -0
- package/dist/editor/i18n-key-gen.d.ts +1 -0
- package/dist/editor/i18n-key-gen.js +7 -0
- package/dist/editor/inline-text-edit.d.ts +5 -0
- package/dist/editor/inline-text-edit.js +173 -92
- package/dist/editor/overlay-events.d.ts +5 -0
- package/dist/editor/overlay-events.js +364 -0
- package/dist/editor/overlay-hmr.d.ts +2 -0
- package/dist/editor/overlay-hmr.js +75 -0
- package/dist/editor/overlay-selection.d.ts +29 -0
- package/dist/editor/overlay-selection.js +148 -0
- package/dist/editor/overlay-utils.d.ts +12 -0
- package/dist/editor/overlay-utils.js +59 -0
- package/dist/editor/properties-panel-persist.d.ts +14 -0
- package/dist/editor/properties-panel-persist.js +70 -0
- package/dist/editor/properties-panel-rows.d.ts +10 -0
- package/dist/editor/properties-panel-rows.js +349 -0
- package/dist/editor/properties-panel-styles.d.ts +4 -0
- package/dist/editor/properties-panel-styles.js +174 -0
- package/dist/editor/properties-panel.d.ts +4 -0
- package/dist/editor/properties-panel.js +148 -0
- package/dist/editor/property-registry.d.ts +16 -0
- package/dist/editor/property-registry.js +303 -0
- package/dist/editor/standalone-file-panel.d.ts +0 -0
- package/dist/editor/standalone-file-panel.js +1 -0
- package/dist/editor/standalone-overlay-dom.d.ts +0 -0
- package/dist/editor/standalone-overlay-dom.js +1 -0
- package/dist/editor/standalone-overlay-styles.d.ts +0 -0
- package/dist/editor/standalone-overlay-styles.js +1 -0
- package/dist/editor/standalone-overlay.d.ts +1 -0
- package/dist/editor/standalone-overlay.js +76 -0
- package/dist/editor/syntax-highlighter.d.ts +4 -0
- package/dist/editor/syntax-highlighter.js +81 -0
- package/dist/editor/text-toolbar.d.ts +11 -0
- package/dist/editor/text-toolbar.js +327 -0
- package/dist/editor/toolbar-styles.d.ts +4 -0
- package/dist/editor/toolbar-styles.js +198 -0
- package/dist/email/index.d.ts +32 -0
- package/dist/email/index.js +154 -0
- package/dist/email/providers/resend.d.ts +2 -0
- package/dist/email/providers/resend.js +24 -0
- package/dist/email/providers/sendgrid.d.ts +2 -0
- package/dist/email/providers/sendgrid.js +31 -0
- package/dist/email/providers/smtp.d.ts +13 -0
- package/dist/email/providers/smtp.js +125 -0
- package/dist/email/template-engine.d.ts +18 -0
- package/dist/email/template-engine.js +116 -0
- package/dist/email/templates/base.d.ts +9 -0
- package/dist/email/templates/base.js +65 -0
- package/dist/email/templates/password-reset.d.ts +5 -0
- package/dist/email/templates/password-reset.js +15 -0
- package/dist/email/templates/verify-email.d.ts +5 -0
- package/dist/email/templates/verify-email.js +15 -0
- package/dist/email/templates/welcome.d.ts +5 -0
- package/dist/email/templates/welcome.js +13 -0
- package/dist/email/types.d.ts +49 -0
- package/dist/email/types.js +1 -0
- package/dist/llms/generate.d.ts +46 -0
- package/dist/llms/generate.js +185 -0
- package/dist/permissions/guard.d.ts +28 -0
- package/dist/permissions/guard.js +30 -0
- package/dist/permissions/index.d.ts +6 -0
- package/dist/permissions/index.js +3 -0
- package/dist/permissions/service.d.ts +80 -0
- package/dist/permissions/service.js +210 -0
- package/dist/permissions/tables.d.ts +5 -0
- package/dist/permissions/tables.js +68 -0
- package/dist/permissions/types.d.ts +33 -0
- package/dist/permissions/types.js +1 -0
- package/dist/runtime/app-shell.js +163 -0
- package/dist/runtime/auth.d.ts +10 -0
- package/dist/runtime/auth.js +30 -0
- package/dist/runtime/communication.d.ts +137 -0
- package/dist/runtime/communication.js +228 -0
- package/dist/runtime/error-boundary.d.ts +23 -0
- package/dist/runtime/error-boundary.js +120 -0
- package/dist/runtime/i18n.d.ts +6 -1
- package/dist/runtime/i18n.js +42 -21
- package/dist/runtime/router-data.d.ts +5 -0
- package/dist/runtime/router-data.js +121 -16
- package/dist/runtime/router-hydration.js +25 -0
- package/dist/runtime/router.d.ts +21 -1
- package/dist/runtime/router.js +221 -39
- package/dist/runtime/socket-client.d.ts +2 -0
- package/dist/runtime/socket-client.js +30 -0
- package/dist/runtime/webrtc.d.ts +47 -0
- package/dist/runtime/webrtc.js +178 -0
- package/dist/shared/graceful-shutdown.d.ts +8 -0
- package/dist/shared/graceful-shutdown.js +36 -0
- package/dist/shared/health.d.ts +8 -0
- package/dist/shared/health.js +25 -0
- package/dist/shared/llms-txt.d.ts +31 -0
- package/dist/shared/llms-txt.js +85 -0
- package/dist/shared/logger.d.ts +32 -0
- package/dist/shared/logger.js +93 -0
- package/dist/shared/meta.d.ts +27 -0
- package/dist/shared/meta.js +71 -0
- package/dist/shared/middleware-runner.d.ts +9 -0
- package/dist/shared/middleware-runner.js +29 -0
- package/dist/shared/rate-limit.d.ts +18 -0
- package/dist/shared/rate-limit.js +71 -0
- package/dist/shared/request-id.d.ts +5 -0
- package/dist/shared/request-id.js +18 -0
- package/dist/shared/route-matching.js +16 -1
- package/dist/shared/security-headers.d.ts +18 -0
- package/dist/shared/security-headers.js +38 -0
- package/dist/shared/socket-io-setup.d.ts +11 -0
- package/dist/shared/socket-io-setup.js +51 -0
- package/dist/shared/types.d.ts +16 -0
- package/dist/shared/utils.d.ts +37 -7
- package/dist/shared/utils.js +175 -26
- package/dist/storage/adapters/local.d.ts +44 -0
- package/dist/storage/adapters/local.js +85 -0
- package/dist/storage/adapters/s3.d.ts +32 -0
- package/dist/storage/adapters/s3.js +116 -0
- package/dist/storage/adapters/types.d.ts +53 -0
- package/dist/storage/adapters/types.js +1 -0
- package/dist/storage/index.d.ts +76 -0
- package/dist/storage/index.js +83 -0
- package/package.json +20 -1
- package/templates/blog/api/posts.ts +6 -0
- package/templates/blog/data/migrations/001_init.sql +13 -0
- package/templates/blog/lumenjs.config.ts +3 -0
- package/templates/blog/package.json +14 -0
- package/templates/blog/pages/_layout.ts +25 -0
- package/templates/blog/pages/index.ts +65 -0
- package/templates/blog/pages/posts/[slug].ts +60 -0
- package/templates/blog/pages/tag/[tag].ts +44 -0
- package/templates/dashboard/api/stats.ts +10 -0
- package/templates/dashboard/data/migrations/001_init.sql +13 -0
- package/templates/dashboard/lumenjs.config.ts +3 -0
- package/templates/dashboard/package.json +14 -0
- package/templates/dashboard/pages/_layout.ts +25 -0
- package/templates/dashboard/pages/index.ts +72 -0
- package/templates/dashboard/pages/settings/index.ts +29 -0
- package/templates/default/lumenjs.config.ts +3 -0
- package/templates/default/package.json +14 -0
- package/templates/default/pages/index.ts +24 -0
- package/templates/social/api/posts/[id].ts +14 -0
- package/templates/social/api/posts.ts +11 -0
- package/templates/social/api/profile/[username].ts +10 -0
- package/templates/social/api/upload.ts +19 -0
- package/templates/social/data/migrations/001_init.sql +78 -0
- package/templates/social/data/migrations/002_add_image_url.sql +1 -0
- package/templates/social/data/migrations/003_auth.sql +7 -0
- package/templates/social/docs/architecture.md +76 -0
- package/templates/social/docs/components.md +100 -0
- package/templates/social/docs/data.md +89 -0
- package/templates/social/docs/pages.md +96 -0
- package/templates/social/docs/theming.md +52 -0
- package/templates/social/lib/media.ts +130 -0
- package/templates/social/lumenjs.auth.ts +21 -0
- package/templates/social/lumenjs.config.ts +3 -0
- package/templates/social/package.json +5 -0
- package/templates/social/pages/_layout.ts +239 -0
- package/templates/social/pages/apps/[id].ts +173 -0
- package/templates/social/pages/apps/index.ts +116 -0
- package/templates/social/pages/auth/login.ts +92 -0
- package/templates/social/pages/bookmarks.ts +57 -0
- package/templates/social/pages/explore.ts +73 -0
- package/templates/social/pages/index.ts +351 -0
- package/templates/social/pages/messages.ts +298 -0
- package/templates/social/pages/new.ts +77 -0
- package/templates/social/pages/notifications.ts +73 -0
- package/templates/social/pages/post/[id].ts +124 -0
- package/templates/social/pages/profile/[username].ts +100 -0
- package/templates/social/pages/settings/accessibility.ts +153 -0
- package/templates/social/pages/settings/account.ts +260 -0
- package/templates/social/pages/settings/help.ts +141 -0
- package/templates/social/pages/settings/language.ts +103 -0
- package/templates/social/pages/settings/privacy.ts +183 -0
- package/templates/social/pages/settings/security.ts +133 -0
- package/templates/social/pages/settings.ts +185 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export interface StoredFile {
|
|
2
|
+
/** Storage key (path within bucket / upload-dir) */
|
|
3
|
+
key: string;
|
|
4
|
+
/** Publicly accessible URL (for public-read files) */
|
|
5
|
+
url: string;
|
|
6
|
+
size: number;
|
|
7
|
+
mimeType: string;
|
|
8
|
+
fileName: string;
|
|
9
|
+
}
|
|
10
|
+
export interface PutOptions {
|
|
11
|
+
/** Custom storage key. Auto-generated UUID if omitted. */
|
|
12
|
+
key?: string;
|
|
13
|
+
mimeType?: string;
|
|
14
|
+
fileName?: string;
|
|
15
|
+
/** Access control. Default: 'public-read' */
|
|
16
|
+
acl?: 'public-read' | 'private';
|
|
17
|
+
}
|
|
18
|
+
export interface PresignPutOptions {
|
|
19
|
+
/** Expected MIME type (enforced in local adapter; advisory on S3 via Content-Type condition) */
|
|
20
|
+
mimeType?: string;
|
|
21
|
+
/** URL expiry in seconds. Default: 3600 */
|
|
22
|
+
expiresIn?: number;
|
|
23
|
+
/** Maximum file size in bytes */
|
|
24
|
+
maxSize?: number;
|
|
25
|
+
}
|
|
26
|
+
export interface PresignGetOptions {
|
|
27
|
+
/** URL expiry in seconds. Default: 3600 */
|
|
28
|
+
expiresIn?: number;
|
|
29
|
+
}
|
|
30
|
+
export interface PresignedUpload {
|
|
31
|
+
/** Presigned URL the client should PUT the encrypted file to */
|
|
32
|
+
uploadUrl: string;
|
|
33
|
+
/** Storage key to reference the file after upload */
|
|
34
|
+
key: string;
|
|
35
|
+
/** ISO 8601 expiry timestamp */
|
|
36
|
+
expiresAt: string;
|
|
37
|
+
}
|
|
38
|
+
/** Storage adapter interface — implement for any backend (S3, R2, MinIO, local disk, etc.) */
|
|
39
|
+
export interface StorageAdapter {
|
|
40
|
+
/** Upload a file buffer server-side. Returns stored file metadata with URL. */
|
|
41
|
+
put(data: Buffer, options?: PutOptions): Promise<StoredFile>;
|
|
42
|
+
/** Delete a file by its storage key. */
|
|
43
|
+
delete(key: string): Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* Generate a presigned PUT URL for direct client-to-storage upload.
|
|
46
|
+
* Used for E2E-encrypted chat attachments — the server never sees the plaintext file.
|
|
47
|
+
*/
|
|
48
|
+
presignPut(key: string, options?: PresignPutOptions): Promise<PresignedUpload>;
|
|
49
|
+
/** Generate a presigned GET URL for temporarily accessing a private file. */
|
|
50
|
+
presignGet(key: string, options?: PresignGetOptions): Promise<string>;
|
|
51
|
+
/** Return the permanent public URL for a public-read file. */
|
|
52
|
+
publicUrl(key: string): string;
|
|
53
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { StorageAdapter } from './adapters/types.js';
|
|
2
|
+
export type { StorageAdapter, StoredFile, PutOptions, PresignPutOptions, PresignGetOptions, PresignedUpload } from './adapters/types.js';
|
|
3
|
+
export { LocalStorageAdapter } from './adapters/local.js';
|
|
4
|
+
export { S3StorageAdapter } from './adapters/s3.js';
|
|
5
|
+
export interface LocalStorageConfig {
|
|
6
|
+
provider: 'local';
|
|
7
|
+
/** Directory to write uploaded files. Default: './uploads' */
|
|
8
|
+
uploadDir?: string;
|
|
9
|
+
/** URL path prefix for serving files. Default: '/uploads' */
|
|
10
|
+
publicPath?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface S3StorageConfig {
|
|
13
|
+
provider: 's3';
|
|
14
|
+
bucket: string;
|
|
15
|
+
region: string;
|
|
16
|
+
/** Read from env var LUMENJS_S3_ACCESS_KEY if omitted */
|
|
17
|
+
accessKeyId?: string;
|
|
18
|
+
/** Read from env var LUMENJS_S3_SECRET_KEY if omitted */
|
|
19
|
+
secretAccessKey?: string;
|
|
20
|
+
/** Custom endpoint for MinIO / Cloudflare R2 / other S3-compatible APIs */
|
|
21
|
+
endpoint?: string;
|
|
22
|
+
/** Public CDN base URL. Defaults to https://{bucket}.s3.{region}.amazonaws.com */
|
|
23
|
+
publicBaseUrl?: string;
|
|
24
|
+
}
|
|
25
|
+
export type StorageConfig = LocalStorageConfig | S3StorageConfig;
|
|
26
|
+
/**
|
|
27
|
+
* Create a storage adapter from a config object.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```ts
|
|
31
|
+
* // Local dev (default)
|
|
32
|
+
* const storage = createStorage({ provider: 'local', uploadDir: './uploads' });
|
|
33
|
+
*
|
|
34
|
+
* // AWS S3
|
|
35
|
+
* const storage = createStorage({
|
|
36
|
+
* provider: 's3',
|
|
37
|
+
* bucket: 'my-bucket',
|
|
38
|
+
* region: 'us-east-1',
|
|
39
|
+
* accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
|
|
40
|
+
* secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
|
|
41
|
+
* });
|
|
42
|
+
*
|
|
43
|
+
* // MinIO / Cloudflare R2
|
|
44
|
+
* const storage = createStorage({
|
|
45
|
+
* provider: 's3',
|
|
46
|
+
* endpoint: 'https://minio.example.com',
|
|
47
|
+
* bucket: 'my-bucket',
|
|
48
|
+
* region: 'us-east-1',
|
|
49
|
+
* accessKeyId: process.env.MINIO_KEY!,
|
|
50
|
+
* secretAccessKey: process.env.MINIO_SECRET!,
|
|
51
|
+
* });
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
export declare function createStorage(config: StorageConfig): StorageAdapter;
|
|
55
|
+
/**
|
|
56
|
+
* Get the global storage adapter (set by the LumenJS runtime or your app).
|
|
57
|
+
* Returns null if storage has not been configured.
|
|
58
|
+
*
|
|
59
|
+
* Available as `req.storage` in API route handlers.
|
|
60
|
+
*/
|
|
61
|
+
export declare function useStorage(): StorageAdapter | null;
|
|
62
|
+
/**
|
|
63
|
+
* Set the global storage adapter.
|
|
64
|
+
* Call this in a `_middleware.ts` or app startup to configure storage.
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* ```ts
|
|
68
|
+
* // pages/_middleware.ts
|
|
69
|
+
* import { setStorage, createStorage } from '@nuraly/lumenjs/dist/storage/index.js';
|
|
70
|
+
*
|
|
71
|
+
* setStorage(createStorage({ provider: 'local' }));
|
|
72
|
+
* ```
|
|
73
|
+
*
|
|
74
|
+
* @internal also called by vite-plugin-storage in dev mode
|
|
75
|
+
*/
|
|
76
|
+
export declare function setStorage(adapter: StorageAdapter | null): void;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { LocalStorageAdapter } from './adapters/local.js';
|
|
2
|
+
import { S3StorageAdapter } from './adapters/s3.js';
|
|
3
|
+
export { LocalStorageAdapter } from './adapters/local.js';
|
|
4
|
+
export { S3StorageAdapter } from './adapters/s3.js';
|
|
5
|
+
// ── Factory ───────────────────────────────────────────────────────
|
|
6
|
+
/**
|
|
7
|
+
* Create a storage adapter from a config object.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* // Local dev (default)
|
|
12
|
+
* const storage = createStorage({ provider: 'local', uploadDir: './uploads' });
|
|
13
|
+
*
|
|
14
|
+
* // AWS S3
|
|
15
|
+
* const storage = createStorage({
|
|
16
|
+
* provider: 's3',
|
|
17
|
+
* bucket: 'my-bucket',
|
|
18
|
+
* region: 'us-east-1',
|
|
19
|
+
* accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
|
|
20
|
+
* secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
|
|
21
|
+
* });
|
|
22
|
+
*
|
|
23
|
+
* // MinIO / Cloudflare R2
|
|
24
|
+
* const storage = createStorage({
|
|
25
|
+
* provider: 's3',
|
|
26
|
+
* endpoint: 'https://minio.example.com',
|
|
27
|
+
* bucket: 'my-bucket',
|
|
28
|
+
* region: 'us-east-1',
|
|
29
|
+
* accessKeyId: process.env.MINIO_KEY!,
|
|
30
|
+
* secretAccessKey: process.env.MINIO_SECRET!,
|
|
31
|
+
* });
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export function createStorage(config) {
|
|
35
|
+
if (config.provider === 's3') {
|
|
36
|
+
const accessKeyId = config.accessKeyId ?? process.env.LUMENJS_S3_ACCESS_KEY ?? '';
|
|
37
|
+
const secretAccessKey = config.secretAccessKey ?? process.env.LUMENJS_S3_SECRET_KEY ?? '';
|
|
38
|
+
if (!accessKeyId || !secretAccessKey) {
|
|
39
|
+
throw new Error('[LumenJS:Storage] S3 credentials are required. ' +
|
|
40
|
+
'Set accessKeyId/secretAccessKey in config, or set LUMENJS_S3_ACCESS_KEY / LUMENJS_S3_SECRET_KEY env vars.');
|
|
41
|
+
}
|
|
42
|
+
return new S3StorageAdapter({
|
|
43
|
+
bucket: config.bucket,
|
|
44
|
+
region: config.region,
|
|
45
|
+
accessKeyId,
|
|
46
|
+
secretAccessKey,
|
|
47
|
+
endpoint: config.endpoint,
|
|
48
|
+
publicBaseUrl: config.publicBaseUrl,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
return new LocalStorageAdapter({
|
|
52
|
+
uploadDir: config.uploadDir ?? './uploads',
|
|
53
|
+
publicPath: config.publicPath ?? '/uploads',
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
// ── Singleton ─────────────────────────────────────────────────────
|
|
57
|
+
let _storage = null;
|
|
58
|
+
/**
|
|
59
|
+
* Get the global storage adapter (set by the LumenJS runtime or your app).
|
|
60
|
+
* Returns null if storage has not been configured.
|
|
61
|
+
*
|
|
62
|
+
* Available as `req.storage` in API route handlers.
|
|
63
|
+
*/
|
|
64
|
+
export function useStorage() {
|
|
65
|
+
return _storage;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Set the global storage adapter.
|
|
69
|
+
* Call this in a `_middleware.ts` or app startup to configure storage.
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```ts
|
|
73
|
+
* // pages/_middleware.ts
|
|
74
|
+
* import { setStorage, createStorage } from '@nuraly/lumenjs/dist/storage/index.js';
|
|
75
|
+
*
|
|
76
|
+
* setStorage(createStorage({ provider: 'local' }));
|
|
77
|
+
* ```
|
|
78
|
+
*
|
|
79
|
+
* @internal also called by vite-plugin-storage in dev mode
|
|
80
|
+
*/
|
|
81
|
+
export function setStorage(adapter) {
|
|
82
|
+
_storage = adapter;
|
|
83
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nuraly/lumenjs",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Full-stack Lit web component framework with file-based routing, server loaders, SSR, and API routes",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/cli.js",
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"dist",
|
|
12
|
+
"templates",
|
|
12
13
|
"README.md"
|
|
13
14
|
],
|
|
14
15
|
"scripts": {
|
|
@@ -42,11 +43,29 @@
|
|
|
42
43
|
"license": "MIT",
|
|
43
44
|
"dependencies": {
|
|
44
45
|
"@lit-labs/ssr": "^3.2.0",
|
|
46
|
+
"better-sqlite3": "^12.8.0",
|
|
47
|
+
"codejar": "^4.2.0",
|
|
45
48
|
"glob": "^10.3.0",
|
|
46
49
|
"lit": "^3.1.0",
|
|
50
|
+
"pg": "^8.20.0",
|
|
51
|
+
"socket.io": "^4.8.3",
|
|
52
|
+
"socket.io-client": "^4.8.3",
|
|
47
53
|
"vite": "^5.4.0"
|
|
48
54
|
},
|
|
55
|
+
"peerDependencies": {
|
|
56
|
+
"@aws-sdk/client-s3": "^3.0.0",
|
|
57
|
+
"@aws-sdk/s3-request-presigner": "^3.0.0"
|
|
58
|
+
},
|
|
59
|
+
"peerDependenciesMeta": {
|
|
60
|
+
"@aws-sdk/client-s3": {
|
|
61
|
+
"optional": true
|
|
62
|
+
},
|
|
63
|
+
"@aws-sdk/s3-request-presigner": {
|
|
64
|
+
"optional": true
|
|
65
|
+
}
|
|
66
|
+
},
|
|
49
67
|
"devDependencies": {
|
|
68
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
50
69
|
"@types/node": "^20.14.2",
|
|
51
70
|
"@vitest/coverage-v8": "^4.0.18",
|
|
52
71
|
"typescript": "^5.4.5",
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS posts (
|
|
2
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
3
|
+
title TEXT NOT NULL,
|
|
4
|
+
slug TEXT NOT NULL UNIQUE,
|
|
5
|
+
content TEXT NOT NULL,
|
|
6
|
+
date TEXT NOT NULL DEFAULT (date('now')),
|
|
7
|
+
tags TEXT NOT NULL DEFAULT '[]'
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
INSERT INTO posts (title, slug, content, date, tags) VALUES
|
|
11
|
+
('Hello World', 'hello-world', 'Welcome to your new LumenJS blog! This post was loaded from SQLite.', '2025-01-15', '["introduction","lumenjs"]'),
|
|
12
|
+
('Getting Started with LumenJS', 'getting-started', 'LumenJS makes it easy to build full-stack web apps with Lit web components and file-based routing.', '2025-01-20', '["tutorial","lumenjs","web-components"]'),
|
|
13
|
+
('SQLite Persistence', 'sqlite-persistence', 'LumenJS includes built-in SQLite support via better-sqlite3. Just use useDb() in your loaders and API routes.', '2025-01-25', '["database","lumenjs"]');
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { LitElement, html, css } from 'lit';
|
|
2
|
+
|
|
3
|
+
export class LayoutRoot extends LitElement {
|
|
4
|
+
static styles = css`
|
|
5
|
+
:host { display: block; min-height: 100vh; font-family: system-ui; color: #1e293b; }
|
|
6
|
+
header { max-width: 720px; margin: 0 auto; padding: 1.5rem 1rem; display: flex; justify-content: space-between; align-items: center; }
|
|
7
|
+
header a { color: #7c3aed; text-decoration: none; font-weight: 600; font-size: 1.125rem; }
|
|
8
|
+
nav a { color: #64748b; text-decoration: none; margin-left: 1.5rem; }
|
|
9
|
+
nav a:hover { color: #7c3aed; }
|
|
10
|
+
main { max-width: 720px; margin: 0 auto; padding: 0 1rem 3rem; }
|
|
11
|
+
`;
|
|
12
|
+
|
|
13
|
+
render() {
|
|
14
|
+
return html`
|
|
15
|
+
<header>
|
|
16
|
+
<a href="/">{{PROJECT_NAME}}</a>
|
|
17
|
+
<nav>
|
|
18
|
+
<a href="/">Home</a>
|
|
19
|
+
<a href="/posts">Posts</a>
|
|
20
|
+
</nav>
|
|
21
|
+
</header>
|
|
22
|
+
<main><slot></slot></main>
|
|
23
|
+
`;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { LitElement, html, css } from 'lit';
|
|
2
|
+
|
|
3
|
+
function estimateReadingTime(text: string): number {
|
|
4
|
+
const wordsPerMinute = 200;
|
|
5
|
+
const words = text.trim().split(/\s+/).length;
|
|
6
|
+
return Math.max(1, Math.ceil(words / wordsPerMinute));
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const POSTS = [
|
|
10
|
+
{
|
|
11
|
+
slug: 'hello-world',
|
|
12
|
+
title: 'Hello World',
|
|
13
|
+
date: '2025-01-15',
|
|
14
|
+
excerpt: 'Welcome to my blog built with LumenJS.',
|
|
15
|
+
content: 'Welcome to my blog! This is a sample post built with LumenJS. Each post is a dynamic route using the [slug] parameter.',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
slug: 'getting-started',
|
|
19
|
+
title: 'Getting Started with LumenJS',
|
|
20
|
+
date: '2025-01-20',
|
|
21
|
+
excerpt: 'Learn how to build web apps with Lit components and file-based routing.',
|
|
22
|
+
content: 'LumenJS uses file-based routing with Lit web components. Create a file in pages/ and it becomes a route. Add a loader() function for server-side data fetching.',
|
|
23
|
+
},
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
export async function loader() {
|
|
27
|
+
return {
|
|
28
|
+
posts: POSTS.map(({ content, ...post }) => ({
|
|
29
|
+
...post,
|
|
30
|
+
readingTime: estimateReadingTime(content),
|
|
31
|
+
})),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class PageIndex extends LitElement {
|
|
36
|
+
static properties = { loaderData: { type: Object } };
|
|
37
|
+
loaderData: any = {};
|
|
38
|
+
|
|
39
|
+
static styles = css`
|
|
40
|
+
:host { display: block; }
|
|
41
|
+
h1 { font-size: 2rem; margin-bottom: 0.5rem; }
|
|
42
|
+
.subtitle { color: #64748b; margin-bottom: 2rem; }
|
|
43
|
+
.post { border-bottom: 1px solid #e2e8f0; padding: 1.5rem 0; }
|
|
44
|
+
.post:last-child { border-bottom: none; }
|
|
45
|
+
.post a { color: #1e293b; text-decoration: none; font-size: 1.25rem; font-weight: 600; }
|
|
46
|
+
.post a:hover { color: #7c3aed; }
|
|
47
|
+
.meta { color: #94a3b8; font-size: 0.875rem; margin-top: 0.25rem; }
|
|
48
|
+
.excerpt { color: #64748b; margin-top: 0.5rem; line-height: 1.5; }
|
|
49
|
+
`;
|
|
50
|
+
|
|
51
|
+
render() {
|
|
52
|
+
const posts = this.loaderData.posts || [];
|
|
53
|
+
return html`
|
|
54
|
+
<h1>Blog</h1>
|
|
55
|
+
<p class="subtitle">Thoughts and tutorials</p>
|
|
56
|
+
${posts.map((p: any) => html`
|
|
57
|
+
<div class="post">
|
|
58
|
+
<a href="/posts/${p.slug}">${p.title}</a>
|
|
59
|
+
<div class="meta">${p.date} · ${p.readingTime} min read</div>
|
|
60
|
+
<p class="excerpt">${p.excerpt}</p>
|
|
61
|
+
</div>
|
|
62
|
+
`)}
|
|
63
|
+
`;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { LitElement, html, css } from 'lit';
|
|
2
|
+
|
|
3
|
+
function estimateReadingTime(text: string): number {
|
|
4
|
+
const wordsPerMinute = 200;
|
|
5
|
+
const words = text.trim().split(/\s+/).length;
|
|
6
|
+
return Math.max(1, Math.ceil(words / wordsPerMinute));
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const POSTS: Record<string, { title: string; date: string; content: string }> = {
|
|
10
|
+
'hello-world': {
|
|
11
|
+
title: 'Hello World',
|
|
12
|
+
date: '2025-01-15',
|
|
13
|
+
content: 'Welcome to my blog! This is a sample post built with LumenJS. Each post is a dynamic route using the [slug] parameter.',
|
|
14
|
+
},
|
|
15
|
+
'getting-started': {
|
|
16
|
+
title: 'Getting Started with LumenJS',
|
|
17
|
+
date: '2025-01-20',
|
|
18
|
+
content: 'LumenJS uses file-based routing with Lit web components. Create a file in pages/ and it becomes a route. Add a loader() function for server-side data fetching.',
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export async function loader({ params }: { params: { slug: string } }) {
|
|
23
|
+
const post = POSTS[params.slug];
|
|
24
|
+
if (!post) return { notFound: true };
|
|
25
|
+
return {
|
|
26
|
+
...post,
|
|
27
|
+
readingTime: estimateReadingTime(post.content),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class PagePost extends LitElement {
|
|
32
|
+
static properties = { loaderData: { type: Object }, slug: { type: String } };
|
|
33
|
+
loaderData: any = {};
|
|
34
|
+
slug = '';
|
|
35
|
+
|
|
36
|
+
static styles = css`
|
|
37
|
+
:host { display: block; }
|
|
38
|
+
.back { color: #7c3aed; text-decoration: none; font-size: 0.875rem; }
|
|
39
|
+
.back:hover { text-decoration: underline; }
|
|
40
|
+
h1 { font-size: 2rem; margin: 1rem 0 0.25rem; }
|
|
41
|
+
.date { color: #94a3b8; font-size: 0.875rem; margin-bottom: 1.5rem; }
|
|
42
|
+
.content { line-height: 1.8; color: #334155; }
|
|
43
|
+
.not-found { color: #64748b; }
|
|
44
|
+
`;
|
|
45
|
+
|
|
46
|
+
render() {
|
|
47
|
+
if (this.loaderData.notFound) {
|
|
48
|
+
return html`
|
|
49
|
+
<a class="back" href="/posts">← Back to posts</a>
|
|
50
|
+
<p class="not-found">Post not found.</p>
|
|
51
|
+
`;
|
|
52
|
+
}
|
|
53
|
+
return html`
|
|
54
|
+
<a class="back" href="/">← Back to posts</a>
|
|
55
|
+
<h1>${this.loaderData.title}</h1>
|
|
56
|
+
<div class="date">${this.loaderData.date} · ${this.loaderData.readingTime} min read</div>
|
|
57
|
+
<p class="content">${this.loaderData.content}</p>
|
|
58
|
+
`;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { LitElement, html, css } from 'lit';
|
|
2
|
+
|
|
3
|
+
const ALL_POSTS = [
|
|
4
|
+
{ slug: 'hello-world', title: 'Hello World', date: '2025-01-15', excerpt: 'Welcome to my blog built with LumenJS.', tags: ['introduction', 'lumenjs'] },
|
|
5
|
+
{ slug: 'getting-started', title: 'Getting Started with LumenJS', date: '2025-01-20', excerpt: 'Learn how to build web apps with Lit components and file-based routing.', tags: ['tutorial', 'lumenjs', 'web-components'] },
|
|
6
|
+
];
|
|
7
|
+
|
|
8
|
+
export async function loader({ params }: { params: { tag: string } }) {
|
|
9
|
+
const posts = ALL_POSTS.filter(p => p.tags.includes(params.tag));
|
|
10
|
+
return { tag: params.tag, posts };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class PageTag extends LitElement {
|
|
14
|
+
static properties = { loaderData: { type: Object } };
|
|
15
|
+
loaderData: any = {};
|
|
16
|
+
|
|
17
|
+
static styles = css`
|
|
18
|
+
:host { display: block; }
|
|
19
|
+
h1 { font-size: 2rem; margin-bottom: 0.5rem; }
|
|
20
|
+
.subtitle { color: #64748b; margin-bottom: 2rem; }
|
|
21
|
+
.post { border-bottom: 1px solid #e2e8f0; padding: 1.5rem 0; }
|
|
22
|
+
.post:last-child { border-bottom: none; }
|
|
23
|
+
.post a { color: #1e293b; text-decoration: none; font-size: 1.25rem; font-weight: 600; }
|
|
24
|
+
.post a:hover { color: #7c3aed; }
|
|
25
|
+
.meta { color: #94a3b8; font-size: 0.875rem; margin-top: 0.25rem; }
|
|
26
|
+
.back { color: #7c3aed; text-decoration: none; font-size: 0.875rem; }
|
|
27
|
+
.back:hover { text-decoration: underline; }
|
|
28
|
+
`;
|
|
29
|
+
|
|
30
|
+
render() {
|
|
31
|
+
const { tag, posts } = this.loaderData;
|
|
32
|
+
return html`
|
|
33
|
+
<a class="back" href="/">← All posts</a>
|
|
34
|
+
<h1>Tagged: ${tag}</h1>
|
|
35
|
+
<p class="subtitle">${posts?.length || 0} post${posts?.length !== 1 ? 's' : ''}</p>
|
|
36
|
+
${(posts || []).map((p: any) => html`
|
|
37
|
+
<div class="post">
|
|
38
|
+
<a href="/posts/${p.slug}">${p.title}</a>
|
|
39
|
+
<div class="meta">${p.date}</div>
|
|
40
|
+
</div>
|
|
41
|
+
`)}
|
|
42
|
+
`;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS stats (
|
|
2
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
3
|
+
label TEXT NOT NULL UNIQUE,
|
|
4
|
+
value REAL NOT NULL,
|
|
5
|
+
unit TEXT NOT NULL DEFAULT '',
|
|
6
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
7
|
+
);
|
|
8
|
+
|
|
9
|
+
INSERT INTO stats (label, value, unit) VALUES
|
|
10
|
+
('Total Users', 1284, 'users'),
|
|
11
|
+
('Revenue', 42500, 'USD'),
|
|
12
|
+
('Active Sessions', 89, 'sessions'),
|
|
13
|
+
('Uptime', 99.97, '%');
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { LitElement, html, css } from 'lit';
|
|
2
|
+
|
|
3
|
+
export class LayoutRoot extends LitElement {
|
|
4
|
+
static styles = css`
|
|
5
|
+
:host { display: flex; min-height: 100vh; font-family: system-ui; color: #1e293b; }
|
|
6
|
+
aside { width: 240px; background: #0f172a; color: #cbd5e1; padding: 1.5rem 0; flex-shrink: 0; }
|
|
7
|
+
.logo { padding: 0 1.25rem 1.5rem; font-weight: 700; font-size: 1.125rem; color: #fff; }
|
|
8
|
+
nav a { display: block; padding: 0.625rem 1.25rem; color: #94a3b8; text-decoration: none; font-size: 0.875rem; }
|
|
9
|
+
nav a:hover { background: #1e293b; color: #fff; }
|
|
10
|
+
main { flex: 1; background: #f8fafc; padding: 2rem; }
|
|
11
|
+
`;
|
|
12
|
+
|
|
13
|
+
render() {
|
|
14
|
+
return html`
|
|
15
|
+
<aside>
|
|
16
|
+
<div class="logo">{{PROJECT_NAME}}</div>
|
|
17
|
+
<nav>
|
|
18
|
+
<a href="/">Overview</a>
|
|
19
|
+
<a href="/settings">Settings</a>
|
|
20
|
+
</nav>
|
|
21
|
+
</aside>
|
|
22
|
+
<main><slot></slot></main>
|
|
23
|
+
`;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { LitElement, html, css } from 'lit';
|
|
2
|
+
|
|
3
|
+
export async function loader() {
|
|
4
|
+
return {
|
|
5
|
+
stats: [
|
|
6
|
+
{ label: 'Users', value: '1,234' },
|
|
7
|
+
{ label: 'Revenue', value: '$12,345' },
|
|
8
|
+
{ label: 'Orders', value: '567' },
|
|
9
|
+
{ label: 'Conversion', value: '3.2%' },
|
|
10
|
+
],
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let tick = 0;
|
|
15
|
+
|
|
16
|
+
export function subscribe({ push }: { push: (data: any) => void }) {
|
|
17
|
+
const interval = setInterval(() => {
|
|
18
|
+
tick++;
|
|
19
|
+
push({
|
|
20
|
+
stats: [
|
|
21
|
+
{ label: 'Users', value: (1234 + tick * 3).toLocaleString() },
|
|
22
|
+
{ label: 'Revenue', value: '$' + (12345 + tick * 47).toLocaleString() },
|
|
23
|
+
{ label: 'Orders', value: (567 + tick).toLocaleString() },
|
|
24
|
+
{ label: 'Conversion', value: (3.2 + Math.sin(tick / 5) * 0.5).toFixed(1) + '%' },
|
|
25
|
+
],
|
|
26
|
+
updatedAt: new Date().toISOString(),
|
|
27
|
+
});
|
|
28
|
+
}, 2000);
|
|
29
|
+
return () => clearInterval(interval);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class PageIndex extends LitElement {
|
|
33
|
+
static properties = {
|
|
34
|
+
loaderData: { type: Object },
|
|
35
|
+
liveData: { type: Object },
|
|
36
|
+
};
|
|
37
|
+
loaderData: any = {};
|
|
38
|
+
liveData: any = null;
|
|
39
|
+
|
|
40
|
+
static styles = css`
|
|
41
|
+
:host { display: block; }
|
|
42
|
+
h1 { font-size: 1.5rem; margin-bottom: 1.5rem; }
|
|
43
|
+
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 1rem; }
|
|
44
|
+
.card { background: #fff; border-radius: 8px; padding: 1.5rem; box-shadow: 0 1px 3px rgba(0,0,0,0.08); transition: box-shadow 0.2s; }
|
|
45
|
+
.card.live { box-shadow: 0 0 0 2px rgba(124,58,237,0.3), 0 1px 3px rgba(0,0,0,0.08); }
|
|
46
|
+
.card-label { font-size: 0.875rem; color: #64748b; }
|
|
47
|
+
.card-value { font-size: 1.75rem; font-weight: 700; margin-top: 0.25rem; }
|
|
48
|
+
.status { font-size: 0.75rem; color: #94a3b8; margin-top: 1.5rem; }
|
|
49
|
+
.status .dot { display: inline-block; width: 6px; height: 6px; border-radius: 50%; background: #22c55e; margin-right: 0.375rem; vertical-align: middle; }
|
|
50
|
+
`;
|
|
51
|
+
|
|
52
|
+
render() {
|
|
53
|
+
const stats = this.liveData?.stats || this.loaderData.stats || [];
|
|
54
|
+
const isLive = !!this.liveData;
|
|
55
|
+
return html`
|
|
56
|
+
<h1>Overview</h1>
|
|
57
|
+
<div class="grid">
|
|
58
|
+
${stats.map((s: any) => html`
|
|
59
|
+
<div class="card ${isLive ? 'live' : ''}">
|
|
60
|
+
<div class="card-label">${s.label}</div>
|
|
61
|
+
<div class="card-value">${s.value}</div>
|
|
62
|
+
</div>
|
|
63
|
+
`)}
|
|
64
|
+
</div>
|
|
65
|
+
${isLive ? html`
|
|
66
|
+
<div class="status">
|
|
67
|
+
<span class="dot"></span>Live — updated ${this.liveData.updatedAt ? new Date(this.liveData.updatedAt).toLocaleTimeString() : ''}
|
|
68
|
+
</div>
|
|
69
|
+
` : ''}
|
|
70
|
+
`;
|
|
71
|
+
}
|
|
72
|
+
}
|