@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,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
|
-
}
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
import { LitElement, html, css } from 'lit';
|
|
2
|
-
|
|
3
|
-
const svg = {
|
|
4
|
-
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>`,
|
|
5
|
-
};
|
|
6
|
-
|
|
7
|
-
export async function loader() {
|
|
8
|
-
return {
|
|
9
|
-
featured: [
|
|
10
|
-
{ id: 'poll', name: 'Polls', desc: 'Create polls and surveys for your community', color: '#7c3aed', icon: 'bar-chart' },
|
|
11
|
-
{ id: 'calendar', name: 'Calendar', desc: 'Track events, meetings, and deadlines', color: '#3b49df', icon: 'calendar' },
|
|
12
|
-
{ id: 'notes', name: 'Quick Notes', desc: 'Markdown notes that sync across devices', color: '#22c55e', icon: 'file-text' },
|
|
13
|
-
],
|
|
14
|
-
categories: ['Productivity', 'Entertainment', 'Finance', 'Social', 'Developer Tools', 'Health'],
|
|
15
|
-
apps: [
|
|
16
|
-
{ id: 'poll', name: 'Poll Creator', desc: 'Create polls and surveys for your feed', category: 'Social', color: '#7c3aed' },
|
|
17
|
-
{ id: 'calendar', name: 'Mini Calendar', desc: 'Track events and meetings', category: 'Productivity', color: '#3b49df' },
|
|
18
|
-
{ id: 'notes', name: 'Quick Notes', desc: 'Markdown notes with cloud sync', category: 'Productivity', color: '#22c55e' },
|
|
19
|
-
{ id: 'timer', name: 'Focus Timer', desc: 'Pomodoro timer for deep work', category: 'Productivity', color: '#ef4444' },
|
|
20
|
-
{ id: 'weather', name: 'Weather', desc: 'Local weather and forecasts', category: 'Entertainment', color: '#f59e0b' },
|
|
21
|
-
{ id: 'stocks', name: 'Stock Tracker', desc: 'Watch your portfolio in real-time', category: 'Finance', color: '#10b981' },
|
|
22
|
-
{ id: 'news', name: 'News Feed', desc: 'Curated tech news from top sources', category: 'Entertainment', color: '#6366f1' },
|
|
23
|
-
{ id: 'tasks', name: 'Task Board', desc: 'Kanban-style task management', category: 'Productivity', color: '#8b5cf6' },
|
|
24
|
-
{ id: 'translate', name: 'Translator', desc: 'Translate text between languages', category: 'Developer Tools', color: '#06b6d4' },
|
|
25
|
-
{ id: 'code', name: 'Code Snippets', desc: 'Save and share code snippets', category: 'Developer Tools', color: '#0f172a' },
|
|
26
|
-
{ id: 'fitness', name: 'Step Counter', desc: 'Track daily steps and activity', category: 'Health', color: '#ec4899' },
|
|
27
|
-
{ id: 'budget', name: 'Budget Tracker', desc: 'Track expenses and income', category: 'Finance', color: '#14b8a6' },
|
|
28
|
-
],
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export class PageApps extends LitElement {
|
|
33
|
-
static properties = { loaderData: { type: Object }, _category: { state: true } };
|
|
34
|
-
loaderData: any = {};
|
|
35
|
-
_category: string = '';
|
|
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
|
-
h2 { font-size: 20px; font-weight: 700; padding: 16px 20px 8px; margin: 0; }
|
|
41
|
-
|
|
42
|
-
.search-wrap { padding: 0 20px 16px; position: relative; }
|
|
43
|
-
.search-icon { position: absolute; left: 32px; top: 50%; transform: translateY(-50%); color: var(--text-secondary); display: flex; }
|
|
44
|
-
.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); }
|
|
45
|
-
.search:focus { border-color: var(--accent); background: var(--bg); }
|
|
46
|
-
|
|
47
|
-
.featured { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; padding: 0 20px 16px; }
|
|
48
|
-
.feat-card { border: 1px solid var(--border); border-radius: 12px; padding: 16px; cursor: pointer; text-decoration: none; color: inherit; transition: box-shadow 0.15s; }
|
|
49
|
-
.feat-card:hover { box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
|
|
50
|
-
.feat-icon { width: 40px; height: 40px; border-radius: 10px; display: flex; align-items: center; justify-content: center; color: #fff; font-size: 18px; font-weight: 700; }
|
|
51
|
-
.feat-name { font-size: 15px; font-weight: 700; margin-top: 10px; }
|
|
52
|
-
.feat-desc { font-size: 12px; color: var(--text-secondary); margin-top: 4px; line-height: 1.3; }
|
|
53
|
-
|
|
54
|
-
.categories { display: flex; gap: 6px; padding: 0 20px 12px; overflow-x: auto; scrollbar-width: none; }
|
|
55
|
-
.categories::-webkit-scrollbar { display: none; }
|
|
56
|
-
.cat-btn { padding: 6px 14px; border-radius: 9999px; border: 1px solid var(--border); background: var(--bg); font-size: 13px; color: var(--text-secondary); cursor: pointer; white-space: nowrap; font-weight: 500; }
|
|
57
|
-
.cat-btn:hover { border-color: var(--accent); color: var(--accent); }
|
|
58
|
-
.cat-btn.active { background: var(--accent); color: #fff; border-color: var(--accent); }
|
|
59
|
-
|
|
60
|
-
.app-list { padding: 0 20px 8px; }
|
|
61
|
-
.app-item { display: flex; gap: 12px; padding: 10px 0; border-bottom: 1px solid var(--border-light); align-items: center; }
|
|
62
|
-
.app-item:last-child { border-bottom: none; }
|
|
63
|
-
.app-icon { width: 40px; height: 40px; border-radius: 10px; display: flex; align-items: center; justify-content: center; color: #fff; font-size: 16px; font-weight: 700; flex-shrink: 0; }
|
|
64
|
-
.app-info { flex: 1; min-width: 0; }
|
|
65
|
-
.app-name { font-size: 14px; font-weight: 700; }
|
|
66
|
-
.app-desc { font-size: 12px; color: var(--text-secondary); margin-top: 1px; }
|
|
67
|
-
.app-cat { font-size: 11px; color: var(--text-tertiary); margin-top: 2px; }
|
|
68
|
-
.open-btn { padding: 5px 14px; border-radius: 9999px; border: 1px solid var(--accent); color: var(--accent); background: var(--bg); font-size: 12px; font-weight: 600; cursor: pointer; text-decoration: none; }
|
|
69
|
-
.open-btn:hover { background: rgba(124,58,237,0.1); }
|
|
70
|
-
|
|
71
|
-
@media (max-width: 640px) { .featured { grid-template-columns: 1fr; } }
|
|
72
|
-
`;
|
|
73
|
-
|
|
74
|
-
render() {
|
|
75
|
-
const { featured, categories, apps } = this.loaderData;
|
|
76
|
-
const filtered = this._category ? apps?.filter((a: any) => a.category === this._category) : apps;
|
|
77
|
-
return html`
|
|
78
|
-
<div class="card">
|
|
79
|
-
<h2>Apps</h2>
|
|
80
|
-
<div class="search-wrap">
|
|
81
|
-
<span class="search-icon">${svg.search}</span>
|
|
82
|
-
<input class="search" type="text" placeholder="Search apps...">
|
|
83
|
-
</div>
|
|
84
|
-
<h2 style="font-size:16px;padding-top:0">Featured</h2>
|
|
85
|
-
<div class="featured">
|
|
86
|
-
${(featured || []).map((f: any) => html`
|
|
87
|
-
<a class="feat-card" href="/apps/${f.id}">
|
|
88
|
-
<div class="feat-icon" style="background:${f.color}">${f.name.charAt(0)}</div>
|
|
89
|
-
<div class="feat-name">${f.name}</div>
|
|
90
|
-
<div class="feat-desc">${f.desc}</div>
|
|
91
|
-
</a>
|
|
92
|
-
`)}
|
|
93
|
-
</div>
|
|
94
|
-
<div class="categories">
|
|
95
|
-
<button class="cat-btn ${this._category === '' ? 'active' : ''}" @click=${() => this._category = ''}>All</button>
|
|
96
|
-
${(categories || []).map((c: string) => html`
|
|
97
|
-
<button class="cat-btn ${this._category === c ? 'active' : ''}" @click=${() => this._category = c}>${c}</button>
|
|
98
|
-
`)}
|
|
99
|
-
</div>
|
|
100
|
-
<div class="app-list">
|
|
101
|
-
${(filtered || []).map((a: any) => html`
|
|
102
|
-
<div class="app-item">
|
|
103
|
-
<div class="app-icon" style="background:${a.color}">${a.name.charAt(0)}</div>
|
|
104
|
-
<div class="app-info">
|
|
105
|
-
<div class="app-name">${a.name}</div>
|
|
106
|
-
<div class="app-desc">${a.desc}</div>
|
|
107
|
-
<div class="app-cat">${a.category}</div>
|
|
108
|
-
</div>
|
|
109
|
-
<a class="open-btn" href="/apps/${a.id}">Open</a>
|
|
110
|
-
</div>
|
|
111
|
-
`)}
|
|
112
|
-
</div>
|
|
113
|
-
</div>
|
|
114
|
-
`;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import { LitElement, html, css } from 'lit';
|
|
2
|
-
|
|
3
|
-
// Google 'G' logo SVG (official colors)
|
|
4
|
-
const googleLogo = html`<svg width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
|
|
5
|
-
<path d="M17.64 9.2c0-.637-.057-1.251-.164-1.84H9v3.481h4.844a4.14 4.14 0 01-1.796 2.716v2.259h2.908c1.702-1.567 2.684-3.875 2.684-6.615z" fill="#4285F4"/>
|
|
6
|
-
<path d="M9 18c2.43 0 4.467-.806 5.956-2.18l-2.908-2.259c-.806.54-1.837.86-3.048.86-2.344 0-4.328-1.584-5.036-3.711H.957v2.332A8.997 8.997 0 009 18z" fill="#34A853"/>
|
|
7
|
-
<path d="M3.964 10.71A5.41 5.41 0 013.682 9c0-.593.102-1.17.282-1.71V4.958H.957A8.996 8.996 0 000 9c0 1.452.348 2.827.957 4.042l3.007-2.332z" fill="#FBBC05"/>
|
|
8
|
-
<path d="M9 3.58c1.321 0 2.508.454 3.44 1.345l2.582-2.58C13.463.891 11.426 0 9 0A8.997 8.997 0 00.957 4.958L3.964 6.29C4.672 4.163 6.656 3.58 9 3.58z" fill="#EA4335"/>
|
|
9
|
-
</svg>`;
|
|
10
|
-
|
|
11
|
-
export async function loader({ query }: any) {
|
|
12
|
-
return {
|
|
13
|
-
error: query?.error ?? null,
|
|
14
|
-
returnTo: query?.returnTo ?? '/',
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export class PageAuthLogin extends LitElement {
|
|
19
|
-
static properties = { loaderData: { type: Object } };
|
|
20
|
-
loaderData: any = {};
|
|
21
|
-
|
|
22
|
-
static styles = css`
|
|
23
|
-
:host { display: flex; align-items: center; justify-content: center; min-height: 80vh; padding: 24px 16px; }
|
|
24
|
-
|
|
25
|
-
.card {
|
|
26
|
-
width: 100%; max-width: 380px;
|
|
27
|
-
background: var(--bg); border: 1px solid var(--border);
|
|
28
|
-
border-radius: 16px; padding: 40px 32px;
|
|
29
|
-
text-align: center;
|
|
30
|
-
box-shadow: 0 4px 24px rgba(0,0,0,0.06);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
.logo {
|
|
34
|
-
width: 52px; height: 52px; border-radius: 50%;
|
|
35
|
-
background: var(--accent); color: #fff;
|
|
36
|
-
font-size: 24px; font-weight: 900;
|
|
37
|
-
display: flex; align-items: center; justify-content: center;
|
|
38
|
-
margin: 0 auto 20px;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
h1 { font-size: 22px; font-weight: 800; margin: 0 0 6px; color: var(--text); }
|
|
42
|
-
p { font-size: 14px; color: var(--text-secondary); margin: 0 0 28px; }
|
|
43
|
-
|
|
44
|
-
.error {
|
|
45
|
-
background: #fef2f2; border: 1px solid #fecaca; border-radius: 8px;
|
|
46
|
-
color: #dc2626; font-size: 13px; padding: 10px 14px;
|
|
47
|
-
margin-bottom: 20px; text-align: left;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
.google-btn {
|
|
51
|
-
display: flex; align-items: center; justify-content: center; gap: 10px;
|
|
52
|
-
width: 100%; padding: 11px 20px;
|
|
53
|
-
background: var(--bg); color: var(--text);
|
|
54
|
-
border: 1px solid var(--border); border-radius: 8px;
|
|
55
|
-
font-size: 14px; font-weight: 600;
|
|
56
|
-
text-decoration: none; cursor: pointer;
|
|
57
|
-
transition: background 0.15s, border-color 0.15s;
|
|
58
|
-
}
|
|
59
|
-
.google-btn:hover { background: var(--bg-secondary); border-color: var(--accent); }
|
|
60
|
-
|
|
61
|
-
.divider { display: flex; align-items: center; gap: 12px; margin: 20px 0; color: var(--text-tertiary); font-size: 12px; }
|
|
62
|
-
.divider::before, .divider::after { content: ''; flex: 1; height: 1px; background: var(--border); }
|
|
63
|
-
|
|
64
|
-
.terms { font-size: 12px; color: var(--text-tertiary); margin-top: 20px; line-height: 1.5; }
|
|
65
|
-
`;
|
|
66
|
-
|
|
67
|
-
render() {
|
|
68
|
-
const { error, returnTo } = this.loaderData;
|
|
69
|
-
const loginUrl = `/__nk_auth/login/google?returnTo=${encodeURIComponent(returnTo ?? '/')}`;
|
|
70
|
-
const errorMsg = error === 'access_denied' ? 'Access was denied. Please try again.'
|
|
71
|
-
: error === 'state_mismatch' ? 'Login session expired. Please try again.'
|
|
72
|
-
: error ? 'Authentication failed. Please try again.'
|
|
73
|
-
: null;
|
|
74
|
-
|
|
75
|
-
return html`
|
|
76
|
-
<div class="card">
|
|
77
|
-
<div class="logo">N</div>
|
|
78
|
-
<h1>Sign in</h1>
|
|
79
|
-
<p>Welcome back — sign in to continue</p>
|
|
80
|
-
|
|
81
|
-
${errorMsg ? html`<div class="error">${errorMsg}</div>` : ''}
|
|
82
|
-
|
|
83
|
-
<a class="google-btn" href="${loginUrl}">
|
|
84
|
-
${googleLogo}
|
|
85
|
-
Continue with Google
|
|
86
|
-
</a>
|
|
87
|
-
|
|
88
|
-
<p class="terms">By signing in you agree to our Terms of Service and Privacy Policy.</p>
|
|
89
|
-
</div>
|
|
90
|
-
`;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import { LitElement, html, css } from 'lit';
|
|
2
|
-
|
|
3
|
-
const svg = {
|
|
4
|
-
bookmark: html`<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" stroke="none"><path d="M19 21l-7-5-7 5V5a2 2 0 012-2h10a2 2 0 012 2z"/></svg>`,
|
|
5
|
-
};
|
|
6
|
-
|
|
7
|
-
export async function loader() {
|
|
8
|
-
return {
|
|
9
|
-
bookmarks: [
|
|
10
|
-
{ id: 1, username: 'aymen', display_name: 'Aymen Labidi', initials: 'AL', color: '#7c3aed', title: 'Why file-based routing changes everything', saved: '2 hours ago' },
|
|
11
|
-
{ id: 7, username: 'emma_data', display_name: 'Emma Williams', initials: 'EW', color: '#3572a5', title: 'Polars vs Pandas: a practical comparison', saved: '1 day ago' },
|
|
12
|
-
{ id: 4, username: 'mike_ops', display_name: 'Mike Johnson', initials: 'MJ', color: '#22c55e', title: 'Migrating CI/CD to GitHub Actions: a retrospective', saved: '3 days ago' },
|
|
13
|
-
],
|
|
14
|
-
};
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export class PageBookmarks extends LitElement {
|
|
18
|
-
static properties = { loaderData: { type: Object } };
|
|
19
|
-
loaderData: any = {};
|
|
20
|
-
|
|
21
|
-
static styles = css`
|
|
22
|
-
:host { display: block; }
|
|
23
|
-
.card { background: var(--bg); border-radius: 6px; border: 1px solid var(--border); overflow: hidden; }
|
|
24
|
-
h2 { font-size: 20px; font-weight: 700; padding: 16px 20px; margin: 0; border-bottom: 1px solid var(--border-light); }
|
|
25
|
-
.item { display: flex; gap: 10px; padding: 12px 20px; border-bottom: 1px solid var(--border-light); cursor: pointer; }
|
|
26
|
-
.item:hover { background: var(--bg-hover); }
|
|
27
|
-
.item:last-child { border-bottom: none; }
|
|
28
|
-
.item-icon { width: 36px; height: 36px; border-radius: 8px; background: var(--bg-secondary); display: flex; align-items: center; justify-content: center; color: var(--accent); flex-shrink: 0; }
|
|
29
|
-
.item-body { flex: 1; }
|
|
30
|
-
.item-title { font-size: 15px; font-weight: 600; }
|
|
31
|
-
.item-title a { color: var(--text); text-decoration: none; }
|
|
32
|
-
.item-title a:hover { color: var(--accent); }
|
|
33
|
-
.item-meta { font-size: 12px; color: var(--text-secondary); margin-top: 2px; }
|
|
34
|
-
.empty { padding: 48px 20px; text-align: center; color: var(--text-secondary); font-size: 14px; }
|
|
35
|
-
`;
|
|
36
|
-
|
|
37
|
-
render() {
|
|
38
|
-
const bookmarks = this.loaderData.bookmarks || [];
|
|
39
|
-
return html`
|
|
40
|
-
<div class="card">
|
|
41
|
-
<h2>Bookmarks</h2>
|
|
42
|
-
${bookmarks.length === 0
|
|
43
|
-
? html`<div class="empty">No bookmarks yet</div>`
|
|
44
|
-
: bookmarks.map((b: any) => html`
|
|
45
|
-
<div class="item">
|
|
46
|
-
<div class="item-icon">${svg.bookmark}</div>
|
|
47
|
-
<div class="item-body">
|
|
48
|
-
<div class="item-title"><a href="/post/${b.id}">${b.title}</a></div>
|
|
49
|
-
<div class="item-meta">${b.display_name} · Saved ${b.saved}</div>
|
|
50
|
-
</div>
|
|
51
|
-
</div>
|
|
52
|
-
`)
|
|
53
|
-
}
|
|
54
|
-
</div>
|
|
55
|
-
`;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import { LitElement, html, css } from 'lit';
|
|
2
|
-
|
|
3
|
-
export async function loader() {
|
|
4
|
-
return {
|
|
5
|
-
tags: [
|
|
6
|
-
{ name: 'webcomponents', count: 1250, color: '#3b49df' },
|
|
7
|
-
{ name: 'typescript', count: 3400, color: '#3178c6' },
|
|
8
|
-
{ name: 'devops', count: 890, color: '#e44d26' },
|
|
9
|
-
{ name: 'machinelearning', count: 2100, color: '#f7df1e' },
|
|
10
|
-
{ name: 'opensource', count: 1800, color: '#22c55e' },
|
|
11
|
-
{ name: 'css', count: 4200, color: '#a855f7' },
|
|
12
|
-
{ name: 'python', count: 5100, color: '#3572a5' },
|
|
13
|
-
{ name: 'beginners', count: 6300, color: '#ef4444' },
|
|
14
|
-
],
|
|
15
|
-
topPosts: [
|
|
16
|
-
{ id: 9, display_name: 'Aymen Labidi', title: 'Building a commit message generator with Claude', likes: 112, comments: 18, time: '4 days ago' },
|
|
17
|
-
{ id: 7, display_name: 'Emma Williams', title: 'Polars vs Pandas: a practical comparison', likes: 93, comments: 15, time: '3 days ago' },
|
|
18
|
-
{ id: 5, display_name: 'Aymen Labidi', title: 'TypeScript\'s type system is secretly a language', likes: 85, comments: 12, time: '2 days ago' },
|
|
19
|
-
],
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export class PageExplore extends LitElement {
|
|
24
|
-
static properties = { loaderData: { type: Object } };
|
|
25
|
-
loaderData: any = {};
|
|
26
|
-
|
|
27
|
-
static styles = css`
|
|
28
|
-
:host { display: block; }
|
|
29
|
-
.card { background: var(--bg); border-radius: 6px; border: 1px solid var(--border); padding: 20px; margin-bottom: 12px; }
|
|
30
|
-
h2 { font-size: 20px; font-weight: 700; margin: 0 0 16px; }
|
|
31
|
-
|
|
32
|
-
.tags-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 8px; }
|
|
33
|
-
.tag-card { border: 1px solid var(--border); border-radius: 6px; padding: 12px 16px; cursor: pointer; transition: all 0.15s; }
|
|
34
|
-
.tag-card:hover { border-color: #d0d0d0; box-shadow: 0 2px 4px rgba(0,0,0,0.04); }
|
|
35
|
-
.tag-top { width: 100%; height: 6px; border-radius: 3px; margin-bottom: 8px; }
|
|
36
|
-
.tag-name { font-size: 16px; font-weight: 600; }
|
|
37
|
-
.tag-count { font-size: 12px; color: var(--text-secondary); margin-top: 2px; }
|
|
38
|
-
|
|
39
|
-
.top-post { padding: 10px 0; border-bottom: 1px solid var(--border-light); }
|
|
40
|
-
.top-post:last-child { border-bottom: none; }
|
|
41
|
-
.top-title { font-size: 15px; font-weight: 600; }
|
|
42
|
-
.top-title a { color: var(--text); text-decoration: none; }
|
|
43
|
-
.top-title a:hover { color: var(--accent); }
|
|
44
|
-
.top-meta { font-size: 12px; color: var(--text-secondary); margin-top: 4px; }
|
|
45
|
-
`;
|
|
46
|
-
|
|
47
|
-
render() {
|
|
48
|
-
const { tags, topPosts } = this.loaderData;
|
|
49
|
-
return html`
|
|
50
|
-
<div class="card">
|
|
51
|
-
<h2>Popular Tags</h2>
|
|
52
|
-
<div class="tags-grid">
|
|
53
|
-
${(tags || []).map((t: any) => html`
|
|
54
|
-
<div class="tag-card">
|
|
55
|
-
<div class="tag-top" style="background:${t.color}"></div>
|
|
56
|
-
<div class="tag-name">#${t.name}</div>
|
|
57
|
-
<div class="tag-count">${t.count.toLocaleString()} posts published</div>
|
|
58
|
-
</div>
|
|
59
|
-
`)}
|
|
60
|
-
</div>
|
|
61
|
-
</div>
|
|
62
|
-
<div class="card">
|
|
63
|
-
<h2>Top Posts This Week</h2>
|
|
64
|
-
${(topPosts || []).map((p: any) => html`
|
|
65
|
-
<div class="top-post">
|
|
66
|
-
<div class="top-title"><a href="/post/${p.id}">${p.title}</a></div>
|
|
67
|
-
<div class="top-meta">${p.display_name} · ${p.likes} reactions · ${p.comments} comments · ${p.time}</div>
|
|
68
|
-
</div>
|
|
69
|
-
`)}
|
|
70
|
-
</div>
|
|
71
|
-
`;
|
|
72
|
-
}
|
|
73
|
-
}
|