@nuraly/lumenjs 0.1.4 → 0.3.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/README.md +48 -7
- 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-markdown.d.ts +15 -0
- package/dist/build/build-markdown.js +90 -0
- package/dist/build/build-server.d.ts +2 -1
- package/dist/build/build-server.js +12 -4
- package/dist/build/build.js +46 -5
- package/dist/build/scan.d.ts +1 -0
- package/dist/build/scan.js +2 -1
- package/dist/build/serve-static.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-llms.js +1 -0
- package/dist/dev-server/plugins/vite-plugin-loaders.d.ts +4 -3
- package/dist/dev-server/plugins/vite-plugin-loaders.js +4 -3
- 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/types.d.ts +1 -1
- package/dist/editor/ai/types.js +2 -2
- 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/llms/generate.d.ts +15 -1
- package/dist/llms/generate.js +54 -44
- package/dist/runtime/app-shell.d.ts +1 -1
- package/dist/runtime/app-shell.js +1 -0
- package/dist/runtime/communication.d.ts +65 -36
- package/dist/runtime/communication.js +117 -57
- 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 +51 -3
- 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/html-to-markdown.d.ts +6 -0
- package/dist/shared/html-to-markdown.js +73 -0
- package/dist/shared/types.d.ts +1 -0
- package/dist/storage/adapters/s3.js +6 -3
- package/package.json +33 -7
- package/templates/blog/pages/index.ts +3 -3
- package/templates/blog/pages/posts/[slug].ts +17 -6
- package/templates/blog/pages/tag/[tag].ts +6 -6
- package/templates/dashboard/pages/index.ts +7 -7
- package/templates/default/pages/index.ts +3 -3
- 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,100 +0,0 @@
|
|
|
1
|
-
import { LitElement, html, css } from 'lit';
|
|
2
|
-
|
|
3
|
-
const svg = {
|
|
4
|
-
location: html`<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0118 0z"/><circle cx="12" cy="10" r="3"/></svg>`,
|
|
5
|
-
briefcase: html`<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="7" width="20" height="14" rx="2" ry="2"/><path d="M16 21V5a2 2 0 00-2-2h-4a2 2 0 00-2 2v16"/></svg>`,
|
|
6
|
-
calendar: html`<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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>`,
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
const USERS: Record<string, any> = {
|
|
10
|
-
aymen: { display_name: 'Aymen Labidi', initials: 'AL', color: '#3b49df', avatar: 'https://avatars.githubusercontent.com/u/3775924?v=4', bio: 'Founder & Developer. Building Nuraly — workflow automation platform. LumenJS creator.', location: 'Tunisia', joined: 'Jun 15, 2024', work: 'Founder at Nuraly' },
|
|
11
|
-
alex_design: { display_name: 'Alex Rivera', initials: 'AR', color: '#e44d26', bio: 'UI/UX designer. Pixels matter. Currently exploring design systems.', location: 'New York, NY', joined: 'Aug 20, 2024', work: 'Design Lead at PixelCo' },
|
|
12
|
-
mike_ops: { display_name: 'Mike Johnson', initials: 'MJ', color: '#22c55e', bio: 'DevOps engineer. Infrastructure as code. Coffee as fuel.', location: 'Austin, TX', joined: 'Sep 10, 2024', work: 'SRE at CloudScale' },
|
|
13
|
-
emma_data: { display_name: 'Emma Williams', initials: 'EW', color: '#3572a5', bio: 'Data scientist. ML enthusiast. Turning data into insights.', location: 'Seattle, WA', joined: 'Mar 5, 2024', work: 'ML Engineer at DataFlow' },
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
const USER_POSTS: Record<string, any[]> = {
|
|
17
|
-
aymen: [
|
|
18
|
-
{ id: 1, title: 'Why file-based routing changes everything', tags: ['webdev', 'javascript'], likes: 42, comments: 5, time: '8 hours ago' },
|
|
19
|
-
{ id: 5, title: 'TypeScript\'s type system is secretly a language', tags: ['typescript'], likes: 85, comments: 12, time: '2 days ago' },
|
|
20
|
-
],
|
|
21
|
-
alex_design: [{ id: 2, title: 'Building a design system from scratch', tags: ['design', 'css'], likes: 38, comments: 3, time: '12 hours ago' }],
|
|
22
|
-
mike_ops: [{ id: 4, title: 'Migrating CI/CD to GitHub Actions', tags: ['devops', 'github'], likes: 29, comments: 4, time: '2 days ago' }],
|
|
23
|
-
emma_data: [
|
|
24
|
-
{ id: 3, title: 'Data cleaning is 80% of ML', tags: ['machinelearning', 'python'], likes: 67, comments: 8, time: '1 day ago' },
|
|
25
|
-
{ id: 6, title: 'Polars vs Pandas', tags: ['python', 'datascience'], likes: 93, comments: 15, time: '4 days ago' },
|
|
26
|
-
],
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
export async function loader({ params }: { params: { username: string } }) {
|
|
30
|
-
const user = USERS[params.username];
|
|
31
|
-
if (!user) return { notFound: true };
|
|
32
|
-
return { ...user, username: params.username, posts: USER_POSTS[params.username] || [] };
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export class PageProfile extends LitElement {
|
|
36
|
-
static properties = { loaderData: { type: Object } };
|
|
37
|
-
loaderData: any = {};
|
|
38
|
-
|
|
39
|
-
static styles = css`
|
|
40
|
-
:host { display: block; }
|
|
41
|
-
.card { background: var(--bg); border-radius: 6px; border: 1px solid var(--border); overflow: hidden; margin-bottom: 12px; }
|
|
42
|
-
.profile-body { padding: 20px; }
|
|
43
|
-
.profile-top { display: flex; gap: 14px; align-items: flex-start; }
|
|
44
|
-
.profile-info { flex: 1; min-width: 0; }
|
|
45
|
-
.profile-top .btn-follow { flex-shrink: 0; }
|
|
46
|
-
.avatar { width: 64px; height: 64px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 22px; font-weight: 700; color: #fff; overflow: hidden; flex-shrink: 0; }
|
|
47
|
-
.avatar img { width: 100%; height: 100%; object-fit: cover; }
|
|
48
|
-
.name { font-size: 20px; font-weight: 700; }
|
|
49
|
-
.bio { font-size: 14px; color: var(--text); line-height: 1.4; margin-top: 2px; }
|
|
50
|
-
.meta { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 10px; font-size: 13px; color: var(--text-secondary); }
|
|
51
|
-
.meta span { display: flex; align-items: center; gap: 4px; }
|
|
52
|
-
.actions { margin-top: 12px; }
|
|
53
|
-
.btn-follow { width: 36px; height: 36px; border-radius: 50%; background: #3b49df; color: #fff; border: none; cursor: pointer; display: flex; align-items: center; justify-content: center; }
|
|
54
|
-
.btn-follow:hover { background: #2f3ab2; }
|
|
55
|
-
.posts-header { padding: 16px 20px; font-size: 18px; font-weight: 700; border-bottom: 1px solid var(--border-light); }
|
|
56
|
-
.post-item { padding: 12px 20px; border-bottom: 1px solid var(--border-light); }
|
|
57
|
-
.post-item:last-child { border-bottom: none; }
|
|
58
|
-
.post-title { font-size: 16px; font-weight: 600; }
|
|
59
|
-
.post-title a { color: var(--text); text-decoration: none; }
|
|
60
|
-
.post-title a:hover { color: var(--accent); }
|
|
61
|
-
.post-tags { display: flex; gap: 4px; margin-top: 4px; }
|
|
62
|
-
.post-tag { font-size: 12px; color: var(--text-secondary); }
|
|
63
|
-
.post-stats { font-size: 12px; color: var(--text-tertiary); margin-top: 4px; }
|
|
64
|
-
.not-found { padding: 48px 20px; text-align: center; color: var(--text-secondary); }
|
|
65
|
-
`;
|
|
66
|
-
|
|
67
|
-
render() {
|
|
68
|
-
if (this.loaderData.notFound) return html`<div class="card"><div class="not-found">User not found</div></div>`;
|
|
69
|
-
const { display_name, username, initials, color, bio, location, joined, work, posts } = this.loaderData;
|
|
70
|
-
return html`
|
|
71
|
-
<div class="card">
|
|
72
|
-
<div class="profile-body">
|
|
73
|
-
<div class="profile-top">
|
|
74
|
-
<div class="avatar" style="background:${color}">${this.loaderData.avatar ? html`<img src="${this.loaderData.avatar}" alt="">` : initials}</div>
|
|
75
|
-
<div class="profile-info">
|
|
76
|
-
<div class="name">${display_name}</div>
|
|
77
|
-
<div class="bio">${bio}</div>
|
|
78
|
-
</div>
|
|
79
|
-
<button class="btn-follow"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M16 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2"/><circle cx="8.5" cy="7" r="4"/><line x1="20" y1="8" x2="20" y2="14"/><line x1="23" y1="11" x2="17" y2="11"/></svg></button>
|
|
80
|
-
</div>
|
|
81
|
-
<div class="meta">
|
|
82
|
-
${location ? html`<span>${svg.location} ${location}</span>` : ''}
|
|
83
|
-
${work ? html`<span>${svg.briefcase} ${work}</span>` : ''}
|
|
84
|
-
<span>${svg.calendar} Joined ${joined}</span>
|
|
85
|
-
</div>
|
|
86
|
-
</div>
|
|
87
|
-
</div>
|
|
88
|
-
<div class="card">
|
|
89
|
-
<div class="posts-header">Posts</div>
|
|
90
|
-
${(posts || []).map((p: any) => html`
|
|
91
|
-
<div class="post-item">
|
|
92
|
-
<div class="post-title"><a href="/post/${p.id}">${p.title}</a></div>
|
|
93
|
-
<div class="post-tags">${(p.tags || []).map((t: string) => html`<span class="post-tag">#${t}</span>`)}</div>
|
|
94
|
-
<div class="post-stats">${p.likes} reactions · ${p.comments} comments · ${p.time}</div>
|
|
95
|
-
</div>
|
|
96
|
-
`)}
|
|
97
|
-
</div>
|
|
98
|
-
`;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
import { LitElement, html, css } from 'lit';
|
|
2
|
-
|
|
3
|
-
const svg = {
|
|
4
|
-
back: html`<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"/></svg>`,
|
|
5
|
-
type: html`<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><polyline points="4 7 4 4 20 4 20 7"/><line x1="9" y1="20" x2="15" y2="20"/><line x1="12" y1="4" x2="12" y2="20"/></svg>`,
|
|
6
|
-
sun: html`<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>`,
|
|
7
|
-
zap: html`<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>`,
|
|
8
|
-
eye: html`<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>`,
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
export async function loader() {
|
|
12
|
-
return {};
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export class PageSettingsAccessibility extends LitElement {
|
|
16
|
-
static properties = {
|
|
17
|
-
loaderData: { type: Object },
|
|
18
|
-
_fontSize: { state: true },
|
|
19
|
-
_reduceMotion: { state: true },
|
|
20
|
-
_highContrast: { state: true },
|
|
21
|
-
_autoplayMedia: { state: true },
|
|
22
|
-
_colorBlind: { state: true },
|
|
23
|
-
};
|
|
24
|
-
loaderData: any = {};
|
|
25
|
-
_fontSize: string = 'medium';
|
|
26
|
-
_reduceMotion = false;
|
|
27
|
-
_highContrast = false;
|
|
28
|
-
_autoplayMedia = true;
|
|
29
|
-
_colorBlind: string = 'none';
|
|
30
|
-
|
|
31
|
-
static styles = css`
|
|
32
|
-
:host { display: block; }
|
|
33
|
-
.card { background: var(--bg); border-radius: 6px; border: 1px solid var(--border); overflow: hidden; margin-bottom: 12px; }
|
|
34
|
-
|
|
35
|
-
.header { display: flex; align-items: center; gap: 12px; padding: 16px 20px; border-bottom: 1px solid var(--border-light); }
|
|
36
|
-
.back { width: 32px; height: 32px; border-radius: 50%; border: none; background: none; cursor: pointer; display: flex; align-items: center; justify-content: center; color: var(--text); text-decoration: none; }
|
|
37
|
-
.back:hover { background: var(--bg-secondary); }
|
|
38
|
-
.header h2 { font-size: 20px; font-weight: 700; margin: 0; }
|
|
39
|
-
|
|
40
|
-
.section-title { font-size: 12px; font-weight: 600; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.5px; padding: 12px 20px 6px; }
|
|
41
|
-
|
|
42
|
-
.row { display: flex; align-items: center; gap: 12px; padding: 14px 20px; border-bottom: 1px solid var(--border); }
|
|
43
|
-
.row:last-child { border-bottom: none; }
|
|
44
|
-
.row-icon { width: 36px; height: 36px; border-radius: 8px; background: var(--bg-secondary); display: flex; align-items: center; justify-content: center; color: var(--text-secondary); flex-shrink: 0; }
|
|
45
|
-
.row-body { flex: 1; }
|
|
46
|
-
.row-label { font-size: 14px; font-weight: 600; }
|
|
47
|
-
.row-desc { font-size: 12px; color: var(--text-secondary); margin-top: 1px; }
|
|
48
|
-
|
|
49
|
-
.toggle { position: relative; width: 42px; height: 24px; flex-shrink: 0; }
|
|
50
|
-
.toggle input { opacity: 0; width: 0; height: 0; }
|
|
51
|
-
.toggle-slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background: #ccc; border-radius: 24px; transition: 0.2s; }
|
|
52
|
-
.toggle-slider:before { content: ''; position: absolute; height: 18px; width: 18px; left: 3px; bottom: 3px; background: var(--bg); border-radius: 50%; transition: 0.2s; }
|
|
53
|
-
.toggle input:checked + .toggle-slider { background: var(--accent); }
|
|
54
|
-
.toggle input:checked + .toggle-slider:before { transform: translateX(18px); }
|
|
55
|
-
|
|
56
|
-
.font-sizes { display: flex; gap: 8px; }
|
|
57
|
-
.font-size-btn { padding: 6px 16px; border-radius: 6px; border: 1px solid var(--border); background: var(--bg); cursor: pointer; font-weight: 600; color: var(--text-secondary); }
|
|
58
|
-
.font-size-btn:hover { background: var(--bg-secondary); }
|
|
59
|
-
.font-size-btn.active { background: var(--accent); color: #fff; border-color: var(--accent); }
|
|
60
|
-
.font-size-btn.sm { font-size: 12px; }
|
|
61
|
-
.font-size-btn.md { font-size: 14px; }
|
|
62
|
-
.font-size-btn.lg { font-size: 16px; }
|
|
63
|
-
.font-size-btn.xl { font-size: 18px; }
|
|
64
|
-
|
|
65
|
-
.preview { margin: 16px 20px; padding: 16px; border-radius: 8px; background: var(--bg-secondary); border: 1px solid var(--border); }
|
|
66
|
-
.preview-title { font-weight: 700; margin-bottom: 4px; }
|
|
67
|
-
.preview-text { color: var(--text-secondary); line-height: 1.5; }
|
|
68
|
-
.preview.sm { font-size: 13px; }
|
|
69
|
-
.preview.md { font-size: 15px; }
|
|
70
|
-
.preview.lg { font-size: 17px; }
|
|
71
|
-
.preview.xl { font-size: 19px; }
|
|
72
|
-
|
|
73
|
-
select { padding: 6px 12px; border-radius: 6px; border: 1px solid var(--border); background: var(--bg); font-size: 13px; color: var(--text); cursor: pointer; outline: none; }
|
|
74
|
-
select:focus { border-color: var(--accent); }
|
|
75
|
-
`;
|
|
76
|
-
|
|
77
|
-
render() {
|
|
78
|
-
return html`
|
|
79
|
-
<div class="card">
|
|
80
|
-
<div class="header">
|
|
81
|
-
<a class="back" href="/settings">${svg.back}</a>
|
|
82
|
-
<h2>Accessibility</h2>
|
|
83
|
-
</div>
|
|
84
|
-
</div>
|
|
85
|
-
|
|
86
|
-
<div class="card">
|
|
87
|
-
<div class="section-title">Font size</div>
|
|
88
|
-
<div class="row">
|
|
89
|
-
<div class="row-icon">${svg.type}</div>
|
|
90
|
-
<div class="row-body">
|
|
91
|
-
<div class="row-label">Text size</div>
|
|
92
|
-
<div class="row-desc">Adjust the size of text across the app</div>
|
|
93
|
-
</div>
|
|
94
|
-
<div class="font-sizes">
|
|
95
|
-
<button class="font-size-btn sm ${this._fontSize === 'small' ? 'active' : ''}" @click=${() => this._fontSize = 'small'}>A</button>
|
|
96
|
-
<button class="font-size-btn md ${this._fontSize === 'medium' ? 'active' : ''}" @click=${() => this._fontSize = 'medium'}>A</button>
|
|
97
|
-
<button class="font-size-btn lg ${this._fontSize === 'large' ? 'active' : ''}" @click=${() => this._fontSize = 'large'}>A</button>
|
|
98
|
-
<button class="font-size-btn xl ${this._fontSize === 'xlarge' ? 'active' : ''}" @click=${() => this._fontSize = 'xlarge'}>A</button>
|
|
99
|
-
</div>
|
|
100
|
-
</div>
|
|
101
|
-
<div class="preview ${this._fontSize === 'small' ? 'sm' : this._fontSize === 'large' ? 'lg' : this._fontSize === 'xlarge' ? 'xl' : 'md'}">
|
|
102
|
-
<div class="preview-title">Preview</div>
|
|
103
|
-
<div class="preview-text">This is how text will appear across the app with your selected font size.</div>
|
|
104
|
-
</div>
|
|
105
|
-
</div>
|
|
106
|
-
|
|
107
|
-
<div class="card">
|
|
108
|
-
<div class="section-title">Display</div>
|
|
109
|
-
<div class="row">
|
|
110
|
-
<div class="row-icon">${svg.sun}</div>
|
|
111
|
-
<div class="row-body">
|
|
112
|
-
<div class="row-label">High contrast</div>
|
|
113
|
-
<div class="row-desc">Increase contrast for better readability</div>
|
|
114
|
-
</div>
|
|
115
|
-
<label class="toggle"><input type="checkbox" .checked=${this._highContrast} @change=${(e: Event) => { this._highContrast = (e.target as HTMLInputElement).checked; }}><span class="toggle-slider"></span></label>
|
|
116
|
-
</div>
|
|
117
|
-
<div class="row">
|
|
118
|
-
<div class="row-icon">${svg.eye}</div>
|
|
119
|
-
<div class="row-body">
|
|
120
|
-
<div class="row-label">Color blind mode</div>
|
|
121
|
-
<div class="row-desc">Adjust colors for color vision deficiency</div>
|
|
122
|
-
</div>
|
|
123
|
-
<select @change=${(e: Event) => { this._colorBlind = (e.target as HTMLSelectElement).value; }}>
|
|
124
|
-
<option value="none" ?selected=${this._colorBlind === 'none'}>None</option>
|
|
125
|
-
<option value="protanopia" ?selected=${this._colorBlind === 'protanopia'}>Protanopia (red-weak)</option>
|
|
126
|
-
<option value="deuteranopia" ?selected=${this._colorBlind === 'deuteranopia'}>Deuteranopia (green-weak)</option>
|
|
127
|
-
<option value="tritanopia" ?selected=${this._colorBlind === 'tritanopia'}>Tritanopia (blue-weak)</option>
|
|
128
|
-
</select>
|
|
129
|
-
</div>
|
|
130
|
-
</div>
|
|
131
|
-
|
|
132
|
-
<div class="card">
|
|
133
|
-
<div class="section-title">Motion</div>
|
|
134
|
-
<div class="row">
|
|
135
|
-
<div class="row-icon">${svg.zap}</div>
|
|
136
|
-
<div class="row-body">
|
|
137
|
-
<div class="row-label">Reduce motion</div>
|
|
138
|
-
<div class="row-desc">Minimize animations and transitions</div>
|
|
139
|
-
</div>
|
|
140
|
-
<label class="toggle"><input type="checkbox" .checked=${this._reduceMotion} @change=${(e: Event) => { this._reduceMotion = (e.target as HTMLInputElement).checked; }}><span class="toggle-slider"></span></label>
|
|
141
|
-
</div>
|
|
142
|
-
<div class="row">
|
|
143
|
-
<div class="row-icon">${svg.eye}</div>
|
|
144
|
-
<div class="row-body">
|
|
145
|
-
<div class="row-label">Autoplay media</div>
|
|
146
|
-
<div class="row-desc">Automatically play videos and GIFs in feed</div>
|
|
147
|
-
</div>
|
|
148
|
-
<label class="toggle"><input type="checkbox" .checked=${this._autoplayMedia} @change=${(e: Event) => { this._autoplayMedia = (e.target as HTMLInputElement).checked; }}><span class="toggle-slider"></span></label>
|
|
149
|
-
</div>
|
|
150
|
-
</div>
|
|
151
|
-
`;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
@@ -1,260 +0,0 @@
|
|
|
1
|
-
import { LitElement, html, css } from 'lit';
|
|
2
|
-
|
|
3
|
-
const svg = {
|
|
4
|
-
back: html`<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"/></svg>`,
|
|
5
|
-
user: html`<svg width="18" height="18" 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>`,
|
|
6
|
-
mail: html`<svg width="18" height="18" 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>`,
|
|
7
|
-
phone: html`<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M22 16.92v3a2 2 0 01-2.18 2 19.79 19.79 0 01-8.63-3.07 19.5 19.5 0 01-6-6 19.79 19.79 0 01-3.07-8.67A2 2 0 014.11 2h3a2 2 0 012 1.72c.127.96.361 1.903.7 2.81a2 2 0 01-.45 2.11L8.09 9.91a16 16 0 006 6l1.27-1.27a2 2 0 012.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0122 16.92z"/></svg>`,
|
|
8
|
-
calendar: html`<svg width="18" height="18" 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>`,
|
|
9
|
-
trash: html`<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg>`,
|
|
10
|
-
upload: html`<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>`,
|
|
11
|
-
camera: html`<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M23 19a2 2 0 01-2 2H3a2 2 0 01-2-2V8a2 2 0 012-2h4l2-3h6l2 3h4a2 2 0 012 2z"/><circle cx="12" cy="13" r="4"/></svg>`,
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
export async function loader() {
|
|
15
|
-
return {
|
|
16
|
-
account: {
|
|
17
|
-
username: 'aymen',
|
|
18
|
-
display_name: 'Aymen Labidi',
|
|
19
|
-
email: 'aymen@nuraly.io',
|
|
20
|
-
phone: '+216 50 123 456',
|
|
21
|
-
joined: 'January 2024',
|
|
22
|
-
avatar: 'https://avatars.githubusercontent.com/u/3775924?v=4',
|
|
23
|
-
},
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export class PageSettingsAccount extends LitElement {
|
|
28
|
-
static properties = { loaderData: { type: Object }, _showPhotoModal: { state: true }, _previewUrl: { state: true }, _dragOver: { state: true }, _pendingFile: { state: true }, _saving: { state: true }, _avatarUrl: { state: true } };
|
|
29
|
-
loaderData: any = {};
|
|
30
|
-
_showPhotoModal = false;
|
|
31
|
-
_previewUrl: string | null = null;
|
|
32
|
-
_dragOver = false;
|
|
33
|
-
_pendingFile: File | null = null;
|
|
34
|
-
_saving = false;
|
|
35
|
-
_avatarUrl: string | null = null;
|
|
36
|
-
|
|
37
|
-
static styles = css`
|
|
38
|
-
:host { display: block; }
|
|
39
|
-
.card { background: var(--bg); border-radius: 6px; border: 1px solid var(--border); overflow: hidden; margin-bottom: 12px; }
|
|
40
|
-
|
|
41
|
-
.header { display: flex; align-items: center; gap: 12px; padding: 16px 20px; border-bottom: 1px solid var(--border-light); }
|
|
42
|
-
.back { width: 32px; height: 32px; border-radius: 50%; border: none; background: none; cursor: pointer; display: flex; align-items: center; justify-content: center; color: var(--text); }
|
|
43
|
-
.back:hover { background: var(--bg-secondary); }
|
|
44
|
-
.header h2 { font-size: 20px; font-weight: 700; margin: 0; }
|
|
45
|
-
|
|
46
|
-
.avatar-section { display: flex; align-items: center; gap: 16px; padding: 20px; border-bottom: 1px solid var(--border); }
|
|
47
|
-
.avatar { width: 64px; height: 64px; border-radius: 50%; object-fit: cover; }
|
|
48
|
-
.avatar-actions { display: flex; flex-direction: column; gap: 6px; }
|
|
49
|
-
.btn-change { padding: 6px 16px; border-radius: 6px; background: var(--accent); color: #fff; border: none; font-size: 13px; font-weight: 600; cursor: pointer; }
|
|
50
|
-
.btn-change:hover { background: var(--accent-hover); }
|
|
51
|
-
.btn-remove { padding: 6px 16px; border-radius: 6px; border: 1px solid var(--border); background: var(--bg); color: var(--text-secondary); font-size: 13px; font-weight: 600; cursor: pointer; }
|
|
52
|
-
.btn-remove:hover { background: var(--bg-secondary); }
|
|
53
|
-
|
|
54
|
-
.field { padding: 16px 20px; border-bottom: 1px solid var(--border); }
|
|
55
|
-
.field:last-child { border-bottom: none; }
|
|
56
|
-
.field-label { font-size: 12px; font-weight: 600; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 6px; }
|
|
57
|
-
.field-row { display: flex; align-items: center; gap: 12px; }
|
|
58
|
-
.field-icon { color: var(--text-secondary); display: flex; align-items: center; }
|
|
59
|
-
.field-value { flex: 1; font-size: 15px; color: var(--text); }
|
|
60
|
-
.field-edit { padding: 4px 12px; border-radius: 6px; border: 1px solid var(--border); background: var(--bg); font-size: 12px; font-weight: 600; cursor: pointer; color: var(--text-secondary); }
|
|
61
|
-
.field-edit:hover { background: var(--bg-secondary); color: var(--text); }
|
|
62
|
-
|
|
63
|
-
.danger-zone { padding: 20px; }
|
|
64
|
-
.danger-title { font-size: 14px; font-weight: 700; color: #e53935; margin-bottom: 4px; }
|
|
65
|
-
.danger-desc { font-size: 13px; color: var(--text-secondary); margin-bottom: 12px; line-height: 1.4; }
|
|
66
|
-
.btn-danger { padding: 8px 20px; border-radius: 6px; border: 1px solid #e53935; background: none; color: #e53935; font-size: 13px; font-weight: 600; cursor: pointer; }
|
|
67
|
-
.btn-danger:hover { background: #fef2f2; }
|
|
68
|
-
|
|
69
|
-
/* Photo modal */
|
|
70
|
-
.modal-overlay { position: fixed; inset: 0; background: var(--overlay, rgba(0,0,0,0.4)); z-index: 200; display: flex; align-items: center; justify-content: center; padding: 20px; }
|
|
71
|
-
.modal { background: var(--bg); border-radius: 12px; width: 100%; max-width: 440px; overflow: hidden; box-shadow: 0 20px 60px rgba(0,0,0,0.3); }
|
|
72
|
-
.modal-header { display: flex; align-items: center; justify-content: space-between; padding: 16px 20px; border-bottom: 1px solid var(--border); }
|
|
73
|
-
.modal-header h3 { font-size: 18px; font-weight: 700; margin: 0; }
|
|
74
|
-
.modal-close { width: 32px; height: 32px; border-radius: 50%; border: none; background: none; cursor: pointer; display: flex; align-items: center; justify-content: center; color: var(--text-secondary); font-size: 20px; }
|
|
75
|
-
.modal-close:hover { background: var(--bg-secondary); color: var(--text); }
|
|
76
|
-
.modal-body { padding: 20px; }
|
|
77
|
-
|
|
78
|
-
.drop-zone { border: 2px dashed var(--border); border-radius: 12px; padding: 32px 20px; text-align: center; cursor: pointer; transition: border-color 0.2s, background 0.2s; }
|
|
79
|
-
.drop-zone:hover, .drop-zone.drag-over { border-color: var(--accent); background: rgba(124, 58, 237, 0.04); }
|
|
80
|
-
.drop-icon { margin-bottom: 12px; color: var(--text-tertiary); }
|
|
81
|
-
.drop-title { font-size: 15px; font-weight: 600; color: var(--text); margin-bottom: 4px; }
|
|
82
|
-
.drop-desc { font-size: 13px; color: var(--text-secondary); }
|
|
83
|
-
.drop-desc a { color: var(--accent); font-weight: 600; cursor: pointer; text-decoration: none; }
|
|
84
|
-
.drop-hint { font-size: 11px; color: var(--text-tertiary); margin-top: 8px; }
|
|
85
|
-
|
|
86
|
-
.preview-section { text-align: center; }
|
|
87
|
-
.preview-img { width: 120px; height: 120px; border-radius: 50%; object-fit: cover; margin: 0 auto 16px; display: block; border: 3px solid var(--border); }
|
|
88
|
-
.preview-name { font-size: 13px; color: var(--text-secondary); margin-bottom: 16px; }
|
|
89
|
-
|
|
90
|
-
.modal-actions { display: flex; gap: 8px; justify-content: flex-end; padding: 16px 20px; border-top: 1px solid var(--border); }
|
|
91
|
-
.btn-cancel { padding: 8px 20px; border-radius: 8px; border: 1px solid var(--border); background: var(--bg); font-size: 14px; font-weight: 600; cursor: pointer; color: var(--text); }
|
|
92
|
-
.btn-cancel:hover { background: var(--bg-secondary); }
|
|
93
|
-
.btn-save { padding: 8px 20px; border-radius: 8px; border: none; background: var(--accent); color: #fff; font-size: 14px; font-weight: 600; cursor: pointer; }
|
|
94
|
-
.btn-save:hover { background: var(--accent-hover); }
|
|
95
|
-
.btn-save:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
96
|
-
.btn-reselect { padding: 6px 16px; border-radius: 6px; border: 1px solid var(--border); background: var(--bg); font-size: 13px; font-weight: 600; cursor: pointer; color: var(--text-secondary); }
|
|
97
|
-
.btn-reselect:hover { background: var(--bg-secondary); }
|
|
98
|
-
`;
|
|
99
|
-
|
|
100
|
-
_openFilePicker() {
|
|
101
|
-
const input = document.createElement('input');
|
|
102
|
-
input.type = 'file';
|
|
103
|
-
input.accept = 'image/png,image/jpeg,image/gif,image/webp';
|
|
104
|
-
input.onchange = () => {
|
|
105
|
-
const file = input.files?.[0];
|
|
106
|
-
if (file) this._handleFile(file);
|
|
107
|
-
};
|
|
108
|
-
input.click();
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
_handleFile(file: File) {
|
|
112
|
-
if (!file.type.startsWith('image/')) return;
|
|
113
|
-
if (this._previewUrl?.startsWith('blob:')) URL.revokeObjectURL(this._previewUrl);
|
|
114
|
-
this._pendingFile = file;
|
|
115
|
-
this._previewUrl = URL.createObjectURL(file);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
_onDrop(e: DragEvent) {
|
|
119
|
-
e.preventDefault();
|
|
120
|
-
this._dragOver = false;
|
|
121
|
-
const file = e.dataTransfer?.files?.[0];
|
|
122
|
-
if (file) this._handleFile(file);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
_onDragOver(e: DragEvent) { e.preventDefault(); this._dragOver = true; }
|
|
126
|
-
_onDragLeave() { this._dragOver = false; }
|
|
127
|
-
|
|
128
|
-
_closeModal() {
|
|
129
|
-
if (this._previewUrl?.startsWith('blob:')) URL.revokeObjectURL(this._previewUrl);
|
|
130
|
-
this._showPhotoModal = false;
|
|
131
|
-
this._previewUrl = null;
|
|
132
|
-
this._pendingFile = null;
|
|
133
|
-
this._dragOver = false;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
async _savePhoto() {
|
|
137
|
-
if (!this._pendingFile || this._saving) return;
|
|
138
|
-
this._saving = true;
|
|
139
|
-
try {
|
|
140
|
-
const fd = new FormData();
|
|
141
|
-
fd.append('file', this._pendingFile, this._pendingFile.name);
|
|
142
|
-
const res = await fetch('/api/upload', { method: 'POST', body: fd });
|
|
143
|
-
if (!res.ok) throw new Error('Upload failed');
|
|
144
|
-
const { url } = await res.json();
|
|
145
|
-
this._avatarUrl = url;
|
|
146
|
-
this._closeModal();
|
|
147
|
-
} catch {
|
|
148
|
-
this._saving = false;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
render() {
|
|
153
|
-
const a = this.loaderData.account || {};
|
|
154
|
-
const avatarSrc = this._avatarUrl || a.avatar;
|
|
155
|
-
return html`
|
|
156
|
-
${this._showPhotoModal ? html`
|
|
157
|
-
<div class="modal-overlay" @click=${this._closeModal}>
|
|
158
|
-
<div class="modal" @click=${(e: Event) => e.stopPropagation()}>
|
|
159
|
-
<div class="modal-header">
|
|
160
|
-
<h3>${this._previewUrl ? 'Preview' : 'Change photo'}</h3>
|
|
161
|
-
<button class="modal-close" @click=${this._closeModal}>×</button>
|
|
162
|
-
</div>
|
|
163
|
-
<div class="modal-body">
|
|
164
|
-
${this._previewUrl ? html`
|
|
165
|
-
<div class="preview-section">
|
|
166
|
-
<img class="preview-img" src="${this._previewUrl}" alt="Preview">
|
|
167
|
-
<button class="btn-reselect" @click=${() => { this._previewUrl = null; }}>Choose another</button>
|
|
168
|
-
</div>
|
|
169
|
-
` : html`
|
|
170
|
-
<div class="drop-zone ${this._dragOver ? 'drag-over' : ''}"
|
|
171
|
-
@click=${this._openFilePicker}
|
|
172
|
-
@drop=${this._onDrop}
|
|
173
|
-
@dragover=${this._onDragOver}
|
|
174
|
-
@dragleave=${this._onDragLeave}>
|
|
175
|
-
<div class="drop-icon">${svg.upload}</div>
|
|
176
|
-
<div class="drop-title">Drag and drop your photo here</div>
|
|
177
|
-
<div class="drop-desc">or <a>browse files</a></div>
|
|
178
|
-
<div class="drop-hint">JPG, PNG, GIF or WebP. Max 5MB.</div>
|
|
179
|
-
</div>
|
|
180
|
-
`}
|
|
181
|
-
</div>
|
|
182
|
-
<div class="modal-actions">
|
|
183
|
-
<button class="btn-cancel" @click=${this._closeModal}>Cancel</button>
|
|
184
|
-
<button class="btn-save" ?disabled=${!this._previewUrl || this._saving} @click=${this._savePhoto}>
|
|
185
|
-
${this._saving ? 'Uploading…' : 'Save photo'}
|
|
186
|
-
</button>
|
|
187
|
-
</div>
|
|
188
|
-
</div>
|
|
189
|
-
</div>
|
|
190
|
-
` : ''}
|
|
191
|
-
|
|
192
|
-
<div class="card">
|
|
193
|
-
<div class="header">
|
|
194
|
-
<a class="back" href="/settings">${svg.back}</a>
|
|
195
|
-
<h2>Account information</h2>
|
|
196
|
-
</div>
|
|
197
|
-
|
|
198
|
-
<div class="avatar-section">
|
|
199
|
-
<img class="avatar" src="${avatarSrc}" alt="">
|
|
200
|
-
<div class="avatar-actions">
|
|
201
|
-
<button class="btn-change" @click=${() => { this._showPhotoModal = true; }}>${svg.camera} Change photo</button>
|
|
202
|
-
<button class="btn-remove">Remove</button>
|
|
203
|
-
</div>
|
|
204
|
-
</div>
|
|
205
|
-
|
|
206
|
-
<div class="field">
|
|
207
|
-
<div class="field-label">Display name</div>
|
|
208
|
-
<div class="field-row">
|
|
209
|
-
${svg.user}
|
|
210
|
-
<span class="field-value">${a.display_name}</span>
|
|
211
|
-
<button class="field-edit">Edit</button>
|
|
212
|
-
</div>
|
|
213
|
-
</div>
|
|
214
|
-
|
|
215
|
-
<div class="field">
|
|
216
|
-
<div class="field-label">Username</div>
|
|
217
|
-
<div class="field-row">
|
|
218
|
-
<span class="field-icon">@</span>
|
|
219
|
-
<span class="field-value">${a.username}</span>
|
|
220
|
-
<button class="field-edit">Edit</button>
|
|
221
|
-
</div>
|
|
222
|
-
</div>
|
|
223
|
-
|
|
224
|
-
<div class="field">
|
|
225
|
-
<div class="field-label">Email address</div>
|
|
226
|
-
<div class="field-row">
|
|
227
|
-
${svg.mail}
|
|
228
|
-
<span class="field-value">${a.email}</span>
|
|
229
|
-
<button class="field-edit">Edit</button>
|
|
230
|
-
</div>
|
|
231
|
-
</div>
|
|
232
|
-
|
|
233
|
-
<div class="field">
|
|
234
|
-
<div class="field-label">Phone number</div>
|
|
235
|
-
<div class="field-row">
|
|
236
|
-
${svg.phone}
|
|
237
|
-
<span class="field-value">${a.phone}</span>
|
|
238
|
-
<button class="field-edit">Edit</button>
|
|
239
|
-
</div>
|
|
240
|
-
</div>
|
|
241
|
-
|
|
242
|
-
<div class="field">
|
|
243
|
-
<div class="field-label">Joined</div>
|
|
244
|
-
<div class="field-row">
|
|
245
|
-
${svg.calendar}
|
|
246
|
-
<span class="field-value">${a.joined}</span>
|
|
247
|
-
</div>
|
|
248
|
-
</div>
|
|
249
|
-
</div>
|
|
250
|
-
|
|
251
|
-
<div class="card">
|
|
252
|
-
<div class="danger-zone">
|
|
253
|
-
<div class="danger-title">Deactivate account</div>
|
|
254
|
-
<div class="danger-desc">This will disable your account. Your profile, posts, and data will be hidden until you reactivate by signing in again.</div>
|
|
255
|
-
<button class="btn-danger">${svg.trash} Deactivate</button>
|
|
256
|
-
</div>
|
|
257
|
-
</div>
|
|
258
|
-
`;
|
|
259
|
-
}
|
|
260
|
-
}
|