@nuraly/lumenjs 0.1.4 → 0.2.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.
- package/dist/auth/native-auth.d.ts +9 -0
- package/dist/auth/native-auth.js +49 -2
- package/dist/auth/routes/login.js +24 -1
- package/dist/auth/routes/totp.d.ts +22 -0
- package/dist/auth/routes/totp.js +232 -0
- package/dist/auth/routes.js +14 -0
- package/dist/auth/token.js +2 -2
- package/dist/build/build-server.d.ts +2 -1
- package/dist/build/build-server.js +10 -1
- package/dist/build/build.js +13 -4
- package/dist/build/scan.d.ts +1 -0
- package/dist/build/scan.js +2 -1
- package/dist/build/serve.js +131 -11
- package/dist/dev-server/config.js +18 -1
- package/dist/dev-server/index-html.d.ts +1 -0
- package/dist/dev-server/index-html.js +4 -1
- package/dist/dev-server/plugins/vite-plugin-routes.js +3 -2
- package/dist/dev-server/plugins/vite-plugin-virtual-modules.js +34 -6
- package/dist/dev-server/server.js +146 -88
- package/dist/dev-server/ssr-render.js +10 -2
- package/dist/editor/ai/backend.js +11 -2
- package/dist/editor/ai/deepseek-client.d.ts +7 -0
- package/dist/editor/ai/deepseek-client.js +113 -0
- package/dist/editor/ai/opencode-client.d.ts +1 -1
- package/dist/editor/ai/opencode-client.js +21 -47
- package/dist/editor/ai-chat-panel.js +27 -1
- package/dist/editor/editor-bridge.js +2 -1
- package/dist/editor/overlay-hmr.js +2 -1
- package/dist/runtime/app-shell.d.ts +1 -1
- package/dist/runtime/app-shell.js +1 -0
- package/dist/runtime/island.d.ts +16 -0
- package/dist/runtime/island.js +80 -0
- package/dist/runtime/router-hydration.js +9 -2
- package/dist/runtime/router.d.ts +3 -1
- package/dist/runtime/router.js +49 -1
- package/dist/runtime/webrtc.d.ts +44 -0
- package/dist/runtime/webrtc.js +263 -13
- package/dist/shared/dom-shims.js +4 -2
- package/dist/shared/types.d.ts +1 -0
- package/dist/storage/adapters/s3.js +6 -3
- package/package.json +33 -7
- package/templates/social/api/posts/[id].ts +0 -14
- package/templates/social/api/posts.ts +0 -11
- package/templates/social/api/profile/[username].ts +0 -10
- package/templates/social/api/upload.ts +0 -19
- package/templates/social/data/migrations/001_init.sql +0 -78
- package/templates/social/data/migrations/002_add_image_url.sql +0 -1
- package/templates/social/data/migrations/003_auth.sql +0 -7
- package/templates/social/docs/architecture.md +0 -76
- package/templates/social/docs/components.md +0 -100
- package/templates/social/docs/data.md +0 -89
- package/templates/social/docs/pages.md +0 -96
- package/templates/social/docs/theming.md +0 -52
- package/templates/social/lib/media.ts +0 -130
- package/templates/social/lumenjs.auth.ts +0 -21
- package/templates/social/lumenjs.config.ts +0 -3
- package/templates/social/package.json +0 -5
- package/templates/social/pages/_layout.ts +0 -239
- package/templates/social/pages/apps/[id].ts +0 -173
- package/templates/social/pages/apps/index.ts +0 -116
- package/templates/social/pages/auth/login.ts +0 -92
- package/templates/social/pages/bookmarks.ts +0 -57
- package/templates/social/pages/explore.ts +0 -73
- package/templates/social/pages/index.ts +0 -351
- package/templates/social/pages/messages.ts +0 -298
- package/templates/social/pages/new.ts +0 -77
- package/templates/social/pages/notifications.ts +0 -73
- package/templates/social/pages/post/[id].ts +0 -124
- package/templates/social/pages/profile/[username].ts +0 -100
- package/templates/social/pages/settings/accessibility.ts +0 -153
- package/templates/social/pages/settings/account.ts +0 -260
- package/templates/social/pages/settings/help.ts +0 -141
- package/templates/social/pages/settings/language.ts +0 -103
- package/templates/social/pages/settings/privacy.ts +0 -183
- package/templates/social/pages/settings/security.ts +0 -133
- package/templates/social/pages/settings.ts +0 -185
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
import os from 'os';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import fs from 'fs';
|
|
4
|
-
import crypto from 'crypto';
|
|
5
|
-
|
|
6
|
-
export interface CompressOptions {
|
|
7
|
-
/** Max width in pixels for images. Default: 1920 */
|
|
8
|
-
maxWidth?: number;
|
|
9
|
-
/** JPEG quality 1–100. Default: 82 */
|
|
10
|
-
quality?: number;
|
|
11
|
-
/** Audio bitrate. Default: '128k' */
|
|
12
|
-
audioBitrate?: string;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface CompressResult {
|
|
16
|
-
data: Buffer;
|
|
17
|
-
mimeType: string;
|
|
18
|
-
/** File extension including dot, e.g. '.jpg' or '.aac' */
|
|
19
|
-
ext: string;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// ── Image ────────────────────────────────────────────────────────────────────
|
|
23
|
-
|
|
24
|
-
async function compressImage(data: Buffer, mimeType: string, opts: CompressOptions): Promise<CompressResult> {
|
|
25
|
-
// GIF: skip — compressing would break animation
|
|
26
|
-
if (mimeType === 'image/gif') return { data, mimeType, ext: '.gif' };
|
|
27
|
-
|
|
28
|
-
let sharp: any;
|
|
29
|
-
try {
|
|
30
|
-
const mod = await import('sharp' as string);
|
|
31
|
-
sharp = mod.default ?? mod;
|
|
32
|
-
} catch {
|
|
33
|
-
throw new Error(
|
|
34
|
-
'[LumenJS:Media] sharp is required for image compression. ' +
|
|
35
|
-
'Install with: npm install sharp',
|
|
36
|
-
);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const compressed = await sharp(data)
|
|
40
|
-
.resize({ width: opts.maxWidth ?? 1920, withoutEnlargement: true })
|
|
41
|
-
.jpeg({ quality: opts.quality ?? 82 })
|
|
42
|
-
.toBuffer();
|
|
43
|
-
|
|
44
|
-
return { data: compressed, mimeType: 'image/jpeg', ext: '.jpg' };
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// ── Audio ────────────────────────────────────────────────────────────────────
|
|
48
|
-
|
|
49
|
-
const AUDIO_INPUT_EXT: Record<string, string> = {
|
|
50
|
-
'audio/mpeg': '.mp3',
|
|
51
|
-
'audio/mp3': '.mp3',
|
|
52
|
-
'audio/mp4': '.m4a',
|
|
53
|
-
'audio/aac': '.aac',
|
|
54
|
-
'audio/ogg': '.ogg',
|
|
55
|
-
'audio/wav': '.wav',
|
|
56
|
-
'audio/webm': '.webm',
|
|
57
|
-
'audio/flac': '.flac',
|
|
58
|
-
'audio/x-flac': '.flac',
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
async function compressAudio(data: Buffer, mimeType: string, opts: CompressOptions): Promise<CompressResult> {
|
|
62
|
-
// Already AAC at target bitrate — skip re-encoding
|
|
63
|
-
if (mimeType === 'audio/aac') return { data, mimeType, ext: '.aac' };
|
|
64
|
-
|
|
65
|
-
let ffmpeg: any;
|
|
66
|
-
try {
|
|
67
|
-
const [ffmpegMod, installerMod] = await Promise.all([
|
|
68
|
-
import('fluent-ffmpeg' as string),
|
|
69
|
-
import('@ffmpeg-installer/ffmpeg' as string),
|
|
70
|
-
]);
|
|
71
|
-
ffmpeg = ffmpegMod.default ?? ffmpegMod;
|
|
72
|
-
ffmpeg.setFfmpegPath((installerMod.default ?? installerMod).path);
|
|
73
|
-
} catch {
|
|
74
|
-
throw new Error(
|
|
75
|
-
'[LumenJS:Media] fluent-ffmpeg and @ffmpeg-installer/ffmpeg are required for audio compression. ' +
|
|
76
|
-
'Install with: npm install fluent-ffmpeg @ffmpeg-installer/ffmpeg',
|
|
77
|
-
);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const inExt = AUDIO_INPUT_EXT[mimeType] ?? '.bin';
|
|
81
|
-
const tmpIn = path.join(os.tmpdir(), `nk-audio-in-${crypto.randomUUID()}${inExt}`);
|
|
82
|
-
const tmpOut = path.join(os.tmpdir(), `nk-audio-out-${crypto.randomUUID()}.aac`);
|
|
83
|
-
|
|
84
|
-
try {
|
|
85
|
-
fs.writeFileSync(tmpIn, data);
|
|
86
|
-
await new Promise<void>((resolve, reject) => {
|
|
87
|
-
ffmpeg(tmpIn)
|
|
88
|
-
.noVideo()
|
|
89
|
-
.audioCodec('aac')
|
|
90
|
-
.audioBitrate(opts.audioBitrate ?? '128k')
|
|
91
|
-
.on('error', reject)
|
|
92
|
-
.on('end', resolve)
|
|
93
|
-
.save(tmpOut);
|
|
94
|
-
});
|
|
95
|
-
return { data: fs.readFileSync(tmpOut), mimeType: 'audio/aac', ext: '.aac' };
|
|
96
|
-
} finally {
|
|
97
|
-
fs.rmSync(tmpIn, { force: true });
|
|
98
|
-
fs.rmSync(tmpOut, { force: true });
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// ── Public API ───────────────────────────────────────────────────────────────
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Compress a media buffer before storing.
|
|
106
|
-
*
|
|
107
|
-
* - Images (JPEG, PNG, WebP) → JPEG, max 1920px wide, quality 82
|
|
108
|
-
* - GIF → pass-through (preserves animation)
|
|
109
|
-
* - Audio → AAC 128k via ffmpeg
|
|
110
|
-
* - Everything else (video, PDF, binary) → pass-through
|
|
111
|
-
*
|
|
112
|
-
* Uses lazy dynamic imports so sharp/ffmpeg are only required if you
|
|
113
|
-
* actually upload that media type.
|
|
114
|
-
*
|
|
115
|
-
* @example
|
|
116
|
-
* const { data, mimeType, ext } = await compress(file.data, file.contentType);
|
|
117
|
-
* const key = `uploads/${crypto.randomUUID()}${ext}`;
|
|
118
|
-
* const stored = await req.storage.put(data, { key, mimeType });
|
|
119
|
-
*/
|
|
120
|
-
export async function compress(
|
|
121
|
-
data: Buffer,
|
|
122
|
-
mimeType: string,
|
|
123
|
-
options: CompressOptions = {},
|
|
124
|
-
): Promise<CompressResult> {
|
|
125
|
-
if (mimeType.startsWith('image/')) return compressImage(data, mimeType, options);
|
|
126
|
-
if (mimeType.startsWith('audio/')) return compressAudio(data, mimeType, options);
|
|
127
|
-
// video, PDF, binary — store as-is
|
|
128
|
-
const ext = path.extname(mimeType.split('/')[1] ?? '') || '';
|
|
129
|
-
return { data, mimeType, ext: ext ? `.${ext}` : '' };
|
|
130
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { googleProvider } from '@nuraly/lumenjs/dist/auth/index.js';
|
|
2
|
-
|
|
3
|
-
export default {
|
|
4
|
-
providers: [
|
|
5
|
-
googleProvider({
|
|
6
|
-
clientId: process.env.GOOGLE_CLIENT_ID!,
|
|
7
|
-
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
|
|
8
|
-
}),
|
|
9
|
-
],
|
|
10
|
-
session: {
|
|
11
|
-
secret: process.env.SESSION_SECRET ?? 'dev-secret-change-in-production',
|
|
12
|
-
cookieName: 'social-session',
|
|
13
|
-
maxAge: 60 * 60 * 24 * 7, // 7 days
|
|
14
|
-
secure: process.env.NODE_ENV === 'production',
|
|
15
|
-
},
|
|
16
|
-
routes: {
|
|
17
|
-
loginPage: '/auth/login',
|
|
18
|
-
postLogin: '/',
|
|
19
|
-
postLogout: '/auth/login',
|
|
20
|
-
},
|
|
21
|
-
};
|
|
@@ -1,239 +0,0 @@
|
|
|
1
|
-
import { LitElement, html, css } from 'lit';
|
|
2
|
-
|
|
3
|
-
const svg = {
|
|
4
|
-
home: html`<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 9l9-7 9 7v11a2 2 0 01-2 2H5a2 2 0 01-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>`,
|
|
5
|
-
explore: html`<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>`,
|
|
6
|
-
bell: html`<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M18 8A6 6 0 006 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 01-3.46 0"/></svg>`,
|
|
7
|
-
message: html`<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg>`,
|
|
8
|
-
user: html`<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>`,
|
|
9
|
-
search: html`<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>`,
|
|
10
|
-
post: html`<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 013 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>`,
|
|
11
|
-
bookmark: html`<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M19 21l-7-5-7 5V5a2 2 0 012-2h10a2 2 0 012 2z"/></svg>`,
|
|
12
|
-
plus: html`<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>`,
|
|
13
|
-
settings: html`<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06A1.65 1.65 0 0019.4 9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z"/></svg>`,
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
const THEME_LIGHT = {
|
|
17
|
-
'--bg': '#fff', '--bg-secondary': '#f7f9f9', '--bg-hover': '#fafafa',
|
|
18
|
-
'--border': '#eff3f4', '--border-light': '#f0f0f0',
|
|
19
|
-
'--text': '#0f1419', '--text-secondary': '#536471', '--text-tertiary': '#a3a3a3',
|
|
20
|
-
'--accent': '#7c3aed', '--accent-hover': '#6d28d9',
|
|
21
|
-
'--input-bg': '#eff3f4', '--card-bg': '#f7f9f9',
|
|
22
|
-
'--overlay': 'rgba(0,0,0,0.4)', '--backdrop': 'rgba(255,255,255,0.85)',
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
const THEME_DARK = {
|
|
26
|
-
'--bg': '#15202b', '--bg-secondary': '#1e2d3d', '--bg-hover': '#1c2b3a',
|
|
27
|
-
'--border': '#38444d', '--border-light': '#2f3b44',
|
|
28
|
-
'--text': '#e7e9ea', '--text-secondary': '#8b98a5', '--text-tertiary': '#6e767d',
|
|
29
|
-
'--accent': '#8b5cf6', '--accent-hover': '#7c3aed',
|
|
30
|
-
'--input-bg': '#253341', '--card-bg': '#1e2d3d',
|
|
31
|
-
'--overlay': 'rgba(0,0,0,0.7)', '--backdrop': 'rgba(21,32,43,0.85)',
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
function applyTheme(dark: boolean) {
|
|
35
|
-
const theme = dark ? THEME_DARK : THEME_LIGHT;
|
|
36
|
-
const root = document.documentElement;
|
|
37
|
-
for (const [k, v] of Object.entries(theme)) root.style.setProperty(k, v);
|
|
38
|
-
root.style.setProperty('color-scheme', dark ? 'dark' : 'light');
|
|
39
|
-
document.body.style.background = theme['--bg'];
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Apply saved theme immediately (before render)
|
|
43
|
-
applyTheme(typeof localStorage !== 'undefined' && localStorage.getItem('theme') === 'dark');
|
|
44
|
-
|
|
45
|
-
// Expose toggle for settings page
|
|
46
|
-
(window as any).__nk_toggle_theme = (dark: boolean) => {
|
|
47
|
-
localStorage.setItem('theme', dark ? 'dark' : 'light');
|
|
48
|
-
applyTheme(dark);
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
export class LayoutRoot extends LitElement {
|
|
52
|
-
|
|
53
|
-
static styles = css`
|
|
54
|
-
:host { display: block; min-height: 100vh; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; color: var(--text); background: var(--bg); font-size: 15px; touch-action: pan-x pan-y; -webkit-text-size-adjust: 100%; }
|
|
55
|
-
|
|
56
|
-
.shell { display: flex; max-width: 1000px; margin: 0 auto; min-height: 100vh; }
|
|
57
|
-
|
|
58
|
-
.sidebar { width: 68px; flex-shrink: 0; position: sticky; top: 0; height: 100vh; border-right: 1px solid var(--border); display: flex; flex-direction: column; align-items: center; padding: 12px 0; }
|
|
59
|
-
.sidebar .logo { font-weight: 900; font-size: 22px; color: var(--text); text-decoration: none; margin-bottom: 20px; display: flex; align-items: center; justify-content: center; width: 42px; height: 42px; }
|
|
60
|
-
.sidebar a, .sidebar button { width: 42px; height: 42px; border-radius: 50%; display: flex; align-items: center; justify-content: center; color: var(--text-secondary); text-decoration: none; border: none; background: none; cursor: pointer; margin: 2px 0; position: relative; }
|
|
61
|
-
.sidebar a:hover, .sidebar button:hover { background: var(--input-bg); color: var(--text); }
|
|
62
|
-
.sidebar a[title]:hover::after { content: attr(title); position: absolute; left: 54px; top: 50%; transform: translateY(-50%); background: var(--text); color: var(--bg); font-size: 12px; font-weight: 600; padding: 4px 10px; border-radius: 6px; white-space: nowrap; pointer-events: none; z-index: 100; }
|
|
63
|
-
.sidebar .avatar { width: 34px; height: 34px; border-radius: 50%; background: #0f1419; color: #fff; display: flex; align-items: center; justify-content: center; font-size: 13px; font-weight: 600; margin-top: auto; margin-bottom: 8px; }
|
|
64
|
-
.sidebar .post-btn { width: 42px; height: 42px; border-radius: 50%; background: var(--accent); color: #fff; border: none; cursor: pointer; display: flex; align-items: center; justify-content: center; margin-top: 12px; }
|
|
65
|
-
.sidebar .post-btn:hover { background: var(--accent-hover); }
|
|
66
|
-
|
|
67
|
-
.main { flex: 1; min-width: 0; border-right: 1px solid var(--border); }
|
|
68
|
-
|
|
69
|
-
.right { width: 220px; flex-shrink: 0; padding: 10px 12px; position: sticky; top: 0; height: 100vh; overflow-y: auto; font-size: 13px; display: none; }
|
|
70
|
-
.shell.show-right .right { display: block; }
|
|
71
|
-
.search-wrap { position: relative; margin-bottom: 16px; }
|
|
72
|
-
.search-icon { position: absolute; left: 12px; top: 50%; transform: translateY(-50%); color: var(--text-secondary); display: flex; }
|
|
73
|
-
.search { width: 100%; padding: 10px 12px 10px 36px; border: 1px solid var(--border); border-radius: 9999px; font-size: 14px; outline: none; background: var(--input-bg); }
|
|
74
|
-
.search:focus { border-color: var(--accent); background: var(--bg); box-shadow: 0 0 0 1px #7c3aed; }
|
|
75
|
-
.right-card { background: var(--bg-secondary); border-radius: 16px; margin-bottom: 16px; overflow: hidden; }
|
|
76
|
-
.right-card h3 { font-size: 15px; font-weight: 800; padding: 10px 12px; margin: 0; }
|
|
77
|
-
.trend { padding: 8px 12px; cursor: pointer; }
|
|
78
|
-
.trend:hover { background: var(--input-bg); }
|
|
79
|
-
.trend-label { font-size: 12px; color: var(--text-secondary); }
|
|
80
|
-
.trend-name { font-size: 15px; font-weight: 700; margin-top: 1px; }
|
|
81
|
-
.trend-count { font-size: 12px; color: var(--text-secondary); margin-top: 1px; }
|
|
82
|
-
.who-item { display: flex; align-items: center; gap: 8px; padding: 8px 12px; }
|
|
83
|
-
.who-item:hover { background: var(--input-bg); }
|
|
84
|
-
.who-avatar { width: 36px; height: 36px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 13px; font-weight: 600; color: #fff; flex-shrink: 0; }
|
|
85
|
-
.who-info { flex: 1; min-width: 0; }
|
|
86
|
-
.who-name { font-size: 14px; font-weight: 700; }
|
|
87
|
-
.who-handle { font-size: 13px; color: var(--text-secondary); }
|
|
88
|
-
.follow-btn { padding: 5px 14px; border-radius: 9999px; background: #0f1419; color: #fff; border: none; font-size: 13px; font-weight: 700; cursor: pointer; }
|
|
89
|
-
.follow-btn:hover { opacity: 0.85; }
|
|
90
|
-
.show-more { display: block; padding: 10px 12px; color: var(--accent); font-size: 13px; text-decoration: none; }
|
|
91
|
-
.show-more:hover { background: var(--input-bg); }
|
|
92
|
-
|
|
93
|
-
/* Weather widget */
|
|
94
|
-
.widget-weather { padding: 16px; }
|
|
95
|
-
.w-header { display: flex; justify-content: space-between; align-items: baseline; }
|
|
96
|
-
.w-city { font-size: 15px; font-weight: 700; }
|
|
97
|
-
.w-temp { font-size: 28px; font-weight: 800; color: var(--text); }
|
|
98
|
-
.w-desc { font-size: 13px; color: var(--text-secondary); margin-top: 2px; }
|
|
99
|
-
.w-forecast { display: flex; gap: 0; margin-top: 12px; }
|
|
100
|
-
.w-day { flex: 1; text-align: center; padding: 6px 0; }
|
|
101
|
-
.w-label { display: block; font-size: 12px; color: var(--text-secondary); }
|
|
102
|
-
.w-val { display: block; font-size: 14px; font-weight: 600; margin-top: 2px; }
|
|
103
|
-
|
|
104
|
-
/* Calendar widget */
|
|
105
|
-
.widget-calendar h3 { font-size: 15px; font-weight: 700; }
|
|
106
|
-
.cal-event { display: flex; gap: 10px; padding: 8px 16px; font-size: 13px; }
|
|
107
|
-
.cal-time { color: var(--text-secondary); font-weight: 600; width: 40px; flex-shrink: 0; }
|
|
108
|
-
.cal-name { color: var(--text); }
|
|
109
|
-
|
|
110
|
-
/* Quick apps widget */
|
|
111
|
-
.widget-apps h3 { font-size: 15px; font-weight: 700; }
|
|
112
|
-
.qapps-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 4px; padding: 4px 16px 8px; }
|
|
113
|
-
.qapp { display: flex; flex-direction: column; align-items: center; gap: 4px; padding: 10px 4px; border-radius: 8px; color: var(--text-secondary); text-decoration: none; font-size: 11px; }
|
|
114
|
-
.qapp:hover { background: var(--input-bg); color: var(--text); }
|
|
115
|
-
.show-more:hover { background: var(--input-bg); }
|
|
116
|
-
|
|
117
|
-
/* Mobile top bar + bottom nav */
|
|
118
|
-
.mobile-top { display: none; }
|
|
119
|
-
.mobile-bar { display: none; }
|
|
120
|
-
@media (max-width: 860px) { .right { display: none; } }
|
|
121
|
-
@media (max-width: 640px) {
|
|
122
|
-
.sidebar { display: none; }
|
|
123
|
-
.mobile-top { display: flex; position: sticky; top: 0; z-index: 50; background: var(--bg); border-bottom: 1px solid var(--border); padding: 8px 16px; align-items: center; gap: 12px; }
|
|
124
|
-
.mobile-top .m-avatar { width: 28px; height: 28px; border-radius: 50%; object-fit: cover; }
|
|
125
|
-
.mobile-top .m-title { font-weight: 800; font-size: 18px; color: var(--text); }
|
|
126
|
-
.mobile-top .m-spacer { flex: 1; }
|
|
127
|
-
.mobile-top .m-icon { color: var(--text-secondary); display: flex; align-items: center; justify-content: center; width: 36px; height: 36px; border-radius: 50%; text-decoration: none; }
|
|
128
|
-
.mobile-top .m-icon:hover { background: var(--input-bg); color: var(--text); }
|
|
129
|
-
.mobile-bar { display: flex; position: fixed; bottom: 0; left: 0; right: 0; z-index: 50; background: var(--bg); border-top: 1px solid var(--border); height: 50px; align-items: center; justify-content: space-around; }
|
|
130
|
-
.mobile-bar a { color: var(--text-secondary); display: flex; align-items: center; justify-content: center; width: 44px; height: 44px; border-radius: 50%; text-decoration: none; }
|
|
131
|
-
.mobile-bar a:hover { background: var(--input-bg); color: var(--text); }
|
|
132
|
-
.mobile-bar .post-fab { width: 42px; height: 42px; background: var(--accent); color: #fff; border-radius: 50%; }
|
|
133
|
-
.mobile-bar .post-fab:hover { background: var(--accent-hover); }
|
|
134
|
-
.mobile-bar .more-btn { width: 44px; height: 44px; border: none; background: none; color: var(--text-secondary); cursor: pointer; display: flex; align-items: center; justify-content: center; border-radius: 50%; }
|
|
135
|
-
.mobile-bar .more-btn:hover { background: var(--input-bg); color: var(--text); }
|
|
136
|
-
|
|
137
|
-
/* More menu overlay */
|
|
138
|
-
.more-menu { display: none; }
|
|
139
|
-
.more-menu.open { display: flex; position: fixed; inset: 0; z-index: 100; background: var(--overlay); align-items: flex-end; justify-content: center; }
|
|
140
|
-
.more-sheet { background: var(--bg); border-radius: 16px 16px 0 0; width: 100%; max-width: 480px; padding: 8px 0 20px; }
|
|
141
|
-
.more-handle { width: 36px; height: 4px; border-radius: 2px; background: #d0d0d0; margin: 0 auto 12px; }
|
|
142
|
-
.more-item { display: flex; align-items: center; gap: 14px; padding: 14px 24px; color: var(--text); text-decoration: none; font-size: 16px; font-weight: 500; }
|
|
143
|
-
.more-item:hover { background: var(--bg-secondary); }
|
|
144
|
-
.more-item svg { color: var(--text-secondary); }
|
|
145
|
-
.main { padding-bottom: 56px; }
|
|
146
|
-
}
|
|
147
|
-
`;
|
|
148
|
-
|
|
149
|
-
connectedCallback() {
|
|
150
|
-
super.connectedCallback();
|
|
151
|
-
const vp = document.querySelector('meta[name="viewport"]');
|
|
152
|
-
if (vp) vp.setAttribute('content', 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no');
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
render() {
|
|
156
|
-
return html`
|
|
157
|
-
<div class="mobile-top">
|
|
158
|
-
<a href="/" style="display:flex;align-items:center;gap:8px;text-decoration:none;color:inherit">
|
|
159
|
-
<img class="m-avatar" src="https://avatars.githubusercontent.com/u/3775924?v=4" alt="A">
|
|
160
|
-
<span class="m-title">Home</span>
|
|
161
|
-
</a>
|
|
162
|
-
<div class="m-spacer"></div>
|
|
163
|
-
<a class="m-icon" href="/settings">${svg.settings}</a>
|
|
164
|
-
<a class="m-icon" href="/notifications">${svg.bell}</a>
|
|
165
|
-
<a class="m-icon" href="/messages">${svg.message}</a>
|
|
166
|
-
</div>
|
|
167
|
-
<div class="shell">
|
|
168
|
-
<nav class="sidebar">
|
|
169
|
-
<a class="logo" href="/">N</a>
|
|
170
|
-
<a href="/" title="Home">${svg.home}</a>
|
|
171
|
-
<a href="/explore" title="Explore">${svg.explore}</a>
|
|
172
|
-
<a href="/notifications" title="Notifications">${svg.bell}</a>
|
|
173
|
-
<a href="/messages" title="Messages">${svg.message}</a>
|
|
174
|
-
<a href="/bookmarks" title="Bookmarks">${svg.bookmark}</a>
|
|
175
|
-
<a href="/apps" title="Apps">${svg.explore}</a>
|
|
176
|
-
<a href="/profile/aymen" title="Profile">${svg.user}</a>
|
|
177
|
-
<a href="/settings" title="Settings">${svg.settings}</a>
|
|
178
|
-
<img class="avatar" src="https://avatars.githubusercontent.com/u/3775924?v=4" alt="A">
|
|
179
|
-
</nav>
|
|
180
|
-
<main class="main"><slot></slot></main>
|
|
181
|
-
<aside class="right">
|
|
182
|
-
<div class="search-wrap">
|
|
183
|
-
<span class="search-icon">${svg.search}</span>
|
|
184
|
-
<input class="search" type="text" placeholder="Search">
|
|
185
|
-
</div>
|
|
186
|
-
<div class="right-card widget-weather">
|
|
187
|
-
<div class="w-header"><span class="w-city">Tunisia</span><span class="w-temp">24°</span></div>
|
|
188
|
-
<div class="w-desc">Sunny</div>
|
|
189
|
-
<div class="w-forecast">
|
|
190
|
-
<div class="w-day"><span class="w-label">Mon</span><span class="w-val">22°</span></div>
|
|
191
|
-
<div class="w-day"><span class="w-label">Tue</span><span class="w-val">25°</span></div>
|
|
192
|
-
<div class="w-day"><span class="w-label">Wed</span><span class="w-val">23°</span></div>
|
|
193
|
-
<div class="w-day"><span class="w-label">Thu</span><span class="w-val">26°</span></div>
|
|
194
|
-
</div>
|
|
195
|
-
</div>
|
|
196
|
-
<div class="right-card widget-calendar">
|
|
197
|
-
<h3>Today, Mar 22</h3>
|
|
198
|
-
<div class="cal-event"><span class="cal-time">10:00</span><span class="cal-name">Team standup</span></div>
|
|
199
|
-
<div class="cal-event"><span class="cal-time">14:00</span><span class="cal-name">Design review</span></div>
|
|
200
|
-
<div class="cal-event"><span class="cal-time">16:30</span><span class="cal-name">Deploy v2.1</span></div>
|
|
201
|
-
</div>
|
|
202
|
-
<div class="right-card widget-apps">
|
|
203
|
-
<h3>Quick Apps</h3>
|
|
204
|
-
<div class="qapps-grid">
|
|
205
|
-
<a class="qapp" href="/apps/notes"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg><span>Notes</span></a>
|
|
206
|
-
<a class="qapp" href="/apps/poll"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/></svg><span>Polls</span></a>
|
|
207
|
-
<a class="qapp" href="/apps/calendar"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg><span>Calendar</span></a>
|
|
208
|
-
<a class="qapp" href="/apps/timer"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg><span>Timer</span></a>
|
|
209
|
-
</div>
|
|
210
|
-
<a class="show-more" href="/apps">All apps</a>
|
|
211
|
-
</div>
|
|
212
|
-
<div class="right-card">
|
|
213
|
-
<h3>Trending</h3>
|
|
214
|
-
<div class="trend"><div class="trend-name">#WebComponents</div><div class="trend-count">1,250 posts</div></div>
|
|
215
|
-
<div class="trend"><div class="trend-name">#TypeScript</div><div class="trend-count">3,400 posts</div></div>
|
|
216
|
-
<div class="trend"><div class="trend-name">#DevOps</div><div class="trend-count">890 posts</div></div>
|
|
217
|
-
</div>
|
|
218
|
-
</aside>
|
|
219
|
-
</div>
|
|
220
|
-
<nav class="mobile-bar">
|
|
221
|
-
<a href="/">${svg.home}</a>
|
|
222
|
-
<a href="/explore">${svg.explore}</a>
|
|
223
|
-
<a class="post-fab" href="/new">${svg.plus}</a>
|
|
224
|
-
<a href="/messages">${svg.message}</a>
|
|
225
|
-
<button class="more-btn" @click=${() => { const m = this.shadowRoot?.querySelector('.more-menu') as HTMLElement; if (m) m.classList.toggle('open'); }}><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="1"/><circle cx="12" cy="5" r="1"/><circle cx="12" cy="19" r="1"/></svg></button>
|
|
226
|
-
</nav>
|
|
227
|
-
<div class="more-menu" @click=${(e: Event) => { (e.currentTarget as HTMLElement).classList.remove('open'); }}>
|
|
228
|
-
<div class="more-sheet" @click=${(e: Event) => e.stopPropagation()}>
|
|
229
|
-
<div class="more-handle"></div>
|
|
230
|
-
<a class="more-item" href="/profile/aymen">${svg.user} <span>Profile</span></a>
|
|
231
|
-
<a class="more-item" href="/bookmarks">${svg.bookmark} <span>Bookmarks</span></a>
|
|
232
|
-
<a class="more-item" href="/apps">${svg.explore} <span>Apps</span></a>
|
|
233
|
-
<a class="more-item" href="/notifications">${svg.bell} <span>Notifications</span></a>
|
|
234
|
-
<a class="more-item" href="/settings">${svg.settings} <span>Settings</span></a>
|
|
235
|
-
</div>
|
|
236
|
-
</div>
|
|
237
|
-
`;
|
|
238
|
-
}
|
|
239
|
-
}
|
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
import { LitElement, html, css } from 'lit';
|
|
2
|
-
|
|
3
|
-
const APPS: Record<string, any> = {
|
|
4
|
-
poll: { name: 'Poll Creator', desc: 'Create polls and surveys for your community feed.', color: '#7c3aed', type: 'poll' },
|
|
5
|
-
calendar: { name: 'Mini Calendar', desc: 'Track events, meetings, and deadlines at a glance.', color: '#3b49df', type: 'calendar' },
|
|
6
|
-
notes: { name: 'Quick Notes', desc: 'Markdown notes that sync across devices. Simple, fast, distraction-free.', color: '#22c55e', type: 'notes' },
|
|
7
|
-
timer: { name: 'Focus Timer', desc: 'Pomodoro timer for deep work sessions.', color: '#ef4444', type: 'timer' },
|
|
8
|
-
weather: { name: 'Weather', desc: 'Local weather and 7-day forecasts.', color: '#f59e0b', type: 'generic' },
|
|
9
|
-
stocks: { name: 'Stock Tracker', desc: 'Watch your portfolio in real-time.', color: '#10b981', type: 'generic' },
|
|
10
|
-
news: { name: 'News Feed', desc: 'Curated tech news from top sources.', color: '#6366f1', type: 'generic' },
|
|
11
|
-
tasks: { name: 'Task Board', desc: 'Kanban-style task management.', color: '#8b5cf6', type: 'generic' },
|
|
12
|
-
translate: { name: 'Translator', desc: 'Translate text between 50+ languages.', color: '#06b6d4', type: 'generic' },
|
|
13
|
-
code: { name: 'Code Snippets', desc: 'Save and share code snippets with syntax highlighting.', color: '#0f172a', type: 'generic' },
|
|
14
|
-
fitness: { name: 'Step Counter', desc: 'Track daily steps and activity goals.', color: '#ec4899', type: 'generic' },
|
|
15
|
-
budget: { name: 'Budget Tracker', desc: 'Track expenses and income with categories.', color: '#14b8a6', type: 'generic' },
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
export async function loader({ params }: { params: { id: string } }) {
|
|
19
|
-
const app = APPS[params.id];
|
|
20
|
-
if (!app) return { notFound: true };
|
|
21
|
-
return { ...app, id: params.id };
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export class PageApp extends LitElement {
|
|
25
|
-
static properties = { loaderData: { type: Object } };
|
|
26
|
-
loaderData: any = {};
|
|
27
|
-
|
|
28
|
-
static styles = css`
|
|
29
|
-
:host { display: block; }
|
|
30
|
-
.card { background: var(--bg); border-radius: 6px; border: 1px solid var(--border); overflow: hidden; margin-bottom: 12px; }
|
|
31
|
-
|
|
32
|
-
.app-header { display: flex; gap: 14px; padding: 20px; align-items: flex-start; border-bottom: 1px solid var(--border-light); }
|
|
33
|
-
.app-icon { width: 56px; height: 56px; border-radius: 14px; display: flex; align-items: center; justify-content: center; color: #fff; font-size: 24px; font-weight: 800; flex-shrink: 0; }
|
|
34
|
-
.app-info { flex: 1; }
|
|
35
|
-
.app-name { font-size: 20px; font-weight: 800; }
|
|
36
|
-
.app-desc { font-size: 14px; color: var(--text-secondary); margin-top: 4px; line-height: 1.4; }
|
|
37
|
-
.app-actions { display: flex; gap: 8px; margin-top: 10px; }
|
|
38
|
-
.btn-open { padding: 7px 20px; border-radius: 9999px; background: var(--accent); color: #fff; border: none; font-size: 14px; font-weight: 600; cursor: pointer; }
|
|
39
|
-
.btn-open:hover { background: var(--accent-hover); }
|
|
40
|
-
.btn-add { padding: 7px 20px; border-radius: 9999px; border: 1px solid var(--border); background: var(--bg); color: var(--text); font-size: 14px; font-weight: 600; cursor: pointer; }
|
|
41
|
-
.btn-add:hover { background: var(--bg-secondary); }
|
|
42
|
-
|
|
43
|
-
.app-body { padding: 20px; min-height: 300px; }
|
|
44
|
-
|
|
45
|
-
/* Poll app mock */
|
|
46
|
-
.poll-create { padding: 0; }
|
|
47
|
-
.poll-create input { width: 100%; padding: 10px 0; border: none; border-bottom: 1px solid var(--border); font-size: 16px; font-weight: 600; outline: none; margin-bottom: 8px; }
|
|
48
|
-
.poll-create input::placeholder { color: var(--text-tertiary); }
|
|
49
|
-
.poll-option-input { display: flex; gap: 8px; margin-bottom: 8px; align-items: center; }
|
|
50
|
-
.poll-option-input input { flex: 1; padding: 8px 12px; border: 1px solid var(--border); border-radius: 6px; font-size: 14px; outline: none; }
|
|
51
|
-
.poll-option-input input:focus { border-color: var(--accent); }
|
|
52
|
-
.poll-num { font-size: 12px; color: var(--text-tertiary); width: 20px; }
|
|
53
|
-
.add-option { color: var(--accent); font-size: 14px; font-weight: 600; border: none; background: none; cursor: pointer; padding: 4px 0; }
|
|
54
|
-
.poll-settings { display: flex; gap: 16px; margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--border); font-size: 13px; color: var(--text-secondary); }
|
|
55
|
-
|
|
56
|
-
/* Calendar app mock */
|
|
57
|
-
.cal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
|
|
58
|
-
.cal-month { font-size: 18px; font-weight: 700; }
|
|
59
|
-
.cal-nav { display: flex; gap: 4px; }
|
|
60
|
-
.cal-nav button { width: 28px; height: 28px; border: 1px solid var(--border); border-radius: 6px; background: var(--bg); cursor: pointer; display: flex; align-items: center; justify-content: center; color: var(--text-secondary); }
|
|
61
|
-
.cal-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; text-align: center; }
|
|
62
|
-
.cal-dow { font-size: 11px; color: var(--text-tertiary); font-weight: 600; padding: 4px; }
|
|
63
|
-
.cal-day { font-size: 13px; padding: 8px 4px; border-radius: 6px; cursor: pointer; }
|
|
64
|
-
.cal-day:hover { background: var(--input-bg); }
|
|
65
|
-
.cal-day.today { background: var(--accent); color: #fff; font-weight: 700; border-radius: 50%; }
|
|
66
|
-
.cal-day.empty { visibility: hidden; }
|
|
67
|
-
|
|
68
|
-
/* Notes app mock */
|
|
69
|
-
.notes-list { display: flex; flex-direction: column; gap: 8px; }
|
|
70
|
-
.note-item { border: 1px solid var(--border); border-radius: 8px; padding: 12px; cursor: pointer; }
|
|
71
|
-
.note-item:hover { border-color: #d0d0d0; }
|
|
72
|
-
.note-title { font-size: 14px; font-weight: 700; }
|
|
73
|
-
.note-preview { font-size: 13px; color: var(--text-secondary); margin-top: 4px; }
|
|
74
|
-
.note-date { font-size: 11px; color: var(--text-tertiary); margin-top: 6px; }
|
|
75
|
-
.new-note { border: 1px dashed var(--border); border-radius: 8px; padding: 12px; text-align: center; color: var(--text-tertiary); font-size: 14px; cursor: pointer; }
|
|
76
|
-
.new-note:hover { border-color: var(--accent); color: var(--accent); }
|
|
77
|
-
|
|
78
|
-
/* Generic placeholder */
|
|
79
|
-
.placeholder { text-align: center; padding: 40px 20px; color: var(--text-tertiary); }
|
|
80
|
-
.placeholder-icon { width: 48px; height: 48px; border-radius: 12px; display: inline-flex; align-items: center; justify-content: center; color: #fff; font-size: 22px; font-weight: 700; margin-bottom: 12px; }
|
|
81
|
-
.placeholder-text { font-size: 14px; }
|
|
82
|
-
|
|
83
|
-
.not-found { padding: 48px 20px; text-align: center; color: var(--text-secondary); }
|
|
84
|
-
`;
|
|
85
|
-
|
|
86
|
-
_renderPoll() {
|
|
87
|
-
return html`
|
|
88
|
-
<div class="poll-create">
|
|
89
|
-
<input type="text" placeholder="Ask a question...">
|
|
90
|
-
<div class="poll-option-input"><span class="poll-num">1</span><input type="text" placeholder="Option 1"></div>
|
|
91
|
-
<div class="poll-option-input"><span class="poll-num">2</span><input type="text" placeholder="Option 2"></div>
|
|
92
|
-
<div class="poll-option-input"><span class="poll-num">3</span><input type="text" placeholder="Option 3 (optional)"></div>
|
|
93
|
-
<button class="add-option">+ Add option</button>
|
|
94
|
-
<div class="poll-settings">
|
|
95
|
-
<span>Duration: 1 day</span>
|
|
96
|
-
<span>Multiple choice: Off</span>
|
|
97
|
-
</div>
|
|
98
|
-
</div>
|
|
99
|
-
`;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
_renderCalendar() {
|
|
103
|
-
const days = ['S', 'M', 'T', 'W', 'T', 'F', 'S'];
|
|
104
|
-
const march = [0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31];
|
|
105
|
-
return html`
|
|
106
|
-
<div class="cal-header">
|
|
107
|
-
<span class="cal-month">March 2025</span>
|
|
108
|
-
<div class="cal-nav">
|
|
109
|
-
<button><</button>
|
|
110
|
-
<button>></button>
|
|
111
|
-
</div>
|
|
112
|
-
</div>
|
|
113
|
-
<div class="cal-grid">
|
|
114
|
-
${days.map(d => html`<div class="cal-dow">${d}</div>`)}
|
|
115
|
-
${march.map(d => html`<div class="cal-day ${d === 0 ? 'empty' : ''} ${d === 22 ? 'today' : ''}">${d || ''}</div>`)}
|
|
116
|
-
</div>
|
|
117
|
-
`;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
_renderNotes() {
|
|
121
|
-
return html`
|
|
122
|
-
<div class="notes-list">
|
|
123
|
-
<div class="new-note">+ New note</div>
|
|
124
|
-
<div class="note-item">
|
|
125
|
-
<div class="note-title">LumenJS Migration Plan</div>
|
|
126
|
-
<div class="note-preview">Steps to migrate from React Router to LumenJS file-based routing...</div>
|
|
127
|
-
<div class="note-date">Today, 10:30 AM</div>
|
|
128
|
-
</div>
|
|
129
|
-
<div class="note-item">
|
|
130
|
-
<div class="note-title">Design System Tokens</div>
|
|
131
|
-
<div class="note-preview">Color: #7c3aed (primary), #0f1419 (text), #536471 (secondary)...</div>
|
|
132
|
-
<div class="note-date">Yesterday</div>
|
|
133
|
-
</div>
|
|
134
|
-
<div class="note-item">
|
|
135
|
-
<div class="note-title">Meeting Notes — Sprint Review</div>
|
|
136
|
-
<div class="note-preview">Completed: social template, widget system, micro-app foundation...</div>
|
|
137
|
-
<div class="note-date">Mar 20</div>
|
|
138
|
-
</div>
|
|
139
|
-
</div>
|
|
140
|
-
`;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
render() {
|
|
144
|
-
if (this.loaderData.notFound) return html`<div class="card"><div class="not-found">App not found</div></div>`;
|
|
145
|
-
const { name, desc, color, type } = this.loaderData;
|
|
146
|
-
return html`
|
|
147
|
-
<div class="card">
|
|
148
|
-
<div class="app-header">
|
|
149
|
-
<div class="app-icon" style="background:${color}">${name?.charAt(0)}</div>
|
|
150
|
-
<div class="app-info">
|
|
151
|
-
<div class="app-name">${name}</div>
|
|
152
|
-
<div class="app-desc">${desc}</div>
|
|
153
|
-
<div class="app-actions">
|
|
154
|
-
<button class="btn-open">Open</button>
|
|
155
|
-
<button class="btn-add">Add to sidebar</button>
|
|
156
|
-
</div>
|
|
157
|
-
</div>
|
|
158
|
-
</div>
|
|
159
|
-
<div class="app-body">
|
|
160
|
-
${type === 'poll' ? this._renderPoll()
|
|
161
|
-
: type === 'calendar' ? this._renderCalendar()
|
|
162
|
-
: type === 'notes' ? this._renderNotes()
|
|
163
|
-
: html`
|
|
164
|
-
<div class="placeholder">
|
|
165
|
-
<div class="placeholder-icon" style="background:${color}">${name?.charAt(0)}</div>
|
|
166
|
-
<div class="placeholder-text">${name} app will appear here</div>
|
|
167
|
-
</div>
|
|
168
|
-
`}
|
|
169
|
-
</div>
|
|
170
|
-
</div>
|
|
171
|
-
`;
|
|
172
|
-
}
|
|
173
|
-
}
|