@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,351 +0,0 @@
|
|
|
1
|
-
import { LitElement, html, css } from 'lit';
|
|
2
|
-
|
|
3
|
-
const svg = {
|
|
4
|
-
comment: html`<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z"/></svg>`,
|
|
5
|
-
repost: html`<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><polyline points="17 1 21 5 17 9"/><path d="M3 11V9a4 4 0 014-4h14"/><polyline points="7 23 3 19 7 15"/><path d="M21 13v2a4 4 0 01-4 4H3"/></svg>`,
|
|
6
|
-
heart: html`<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M20.84 4.61a5.5 5.5 0 00-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 00-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 000-7.78z"/></svg>`,
|
|
7
|
-
share: html`<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 12v8a2 2 0 002 2h12a2 2 0 002-2v-8"/><polyline points="16 6 12 2 8 6"/><line x1="12" y1="2" x2="12" y2="15"/></svg>`,
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
export async function loader() {
|
|
11
|
-
return {
|
|
12
|
-
stories: [
|
|
13
|
-
{ username: 'aymen', name: 'You', initials: 'AL', color: '#7c3aed', avatar: 'https://avatars.githubusercontent.com/u/3775924?v=4', isYou: true, image: 'https://images.unsplash.com/photo-1555066931-4365d14bab8c?w=400&h=700&fit=crop', caption: 'Late night coding session' },
|
|
14
|
-
{ username: 'alex_design', name: 'Alex', initials: 'AR', color: '#e44d26', image: 'https://images.unsplash.com/photo-1561070791-2526d30994b5?w=400&h=700&fit=crop', caption: 'New design system preview' },
|
|
15
|
-
{ username: 'emma_data', name: 'Emma', initials: 'EW', color: '#3572a5', image: 'https://images.unsplash.com/photo-1551288049-bebda4e38f71?w=400&h=700&fit=crop', caption: 'Data viz experiments' },
|
|
16
|
-
{ username: 'mike_ops', name: 'Mike', initials: 'MJ', color: '#22c55e', image: 'https://images.unsplash.com/photo-1618401471353-b98afee0b2eb?w=400&h=700&fit=crop', caption: 'Deploying to prod' },
|
|
17
|
-
{ username: 'lisa_pm', name: 'Lisa', initials: 'LP', color: '#a855f7', image: 'https://images.unsplash.com/photo-1517245386807-bb43f82c33c4?w=400&h=700&fit=crop', caption: 'Team offsite recap' },
|
|
18
|
-
{ username: 'tom_arch', name: 'Tom', initials: 'TB', color: '#ef4444', image: 'https://images.unsplash.com/photo-1526374965328-7f61d4dc18c5?w=400&h=700&fit=crop', caption: 'Architecture deep dive' },
|
|
19
|
-
{ username: 'nina_sec', name: 'Nina', initials: 'NP', color: '#f59e0b', image: 'https://images.unsplash.com/photo-1550751827-4bd374c3f58b?w=400&h=700&fit=crop', caption: 'Security audit walkthrough' },
|
|
20
|
-
],
|
|
21
|
-
widgets: [
|
|
22
|
-
{ type: 'poll', position: 2, username: 'alex_design', display_name: 'Alex Rivera', initials: 'AR', color: '#e44d26', time: '3h', question: 'What framework do you use most in 2025?', options: [{ label: 'React', pct: 45 }, { label: 'LumenJS', pct: 35 }, { label: 'Vue', pct: 15 }, { label: 'Other', pct: 5 }], votes: 142 },
|
|
23
|
-
{ type: 'event', position: 4, username: 'mike_ops', display_name: 'Mike Johnson', initials: 'MJ', color: '#22c55e', time: '5h', title: 'DevOps Meetup 2025', date: 'Apr 5, 2025 · 6:00 PM', location: 'Online · Zoom', attendees: 48 },
|
|
24
|
-
{ type: 'job', position: 6, username: 'emma_data', display_name: 'Emma Williams', initials: 'EW', color: '#3572a5', time: '8h', company: 'DataFlow', role: 'Senior ML Engineer', loc: 'Remote', salary: '$150k - $200k' },
|
|
25
|
-
],
|
|
26
|
-
posts: [
|
|
27
|
-
{ id: 1, username: 'aymen', display_name: 'Aymen Labidi', initials: 'AL', color: '#7c3aed', avatar: 'https://avatars.githubusercontent.com/u/3775924?v=4', content: 'Just shipped a new feature using LumenJS! The file-based routing makes everything so clean. No more route config files. Every page is just a .ts file in the pages/ directory. Dynamic routes use [param] syntax. Layouts nest automatically with _layout.ts. Server loaders run before render and pass data as properties. SSR works out of the box with Lit SSR. This is the future of web development.', media: { type: 'image', url: 'https://images.unsplash.com/photo-1555066931-4365d14bab8c?w=600&h=340&fit=crop' }, likes: 42, comments: 5, reposts: 12, time: '2h' },
|
|
28
|
-
{ id: 2, username: 'alex_design', display_name: 'Alex Rivera', initials: 'AR', color: '#e44d26', content: 'Working on a new design system. The key insight after 6 months: consistency beats creativity when building at scale. Every component should feel like it belongs. We started with 40 one-off components and consolidated down to 12 primitives that compose into everything we need. The trick is establishing clear constraints early — spacing scale, color tokens, typography ramp — then letting the system do the heavy lifting.', likes: 38, comments: 3, reposts: 8, time: '4h' },
|
|
29
|
-
{ id: 3, username: 'emma_data', display_name: 'Emma Williams', initials: 'EW', color: '#3572a5', content: 'Trained a new model on customer feedback data. Accuracy went from 78% to 94% just by cleaning the input data. No architecture changes needed. The biggest wins: removing duplicates, fixing encoding issues, normalizing date formats, and filtering out bot-generated entries. Spent 3 days on data prep vs 1 day on model tuning. Garbage in, garbage out is the most underrated lesson in ML.', media: { type: 'image', url: 'https://images.unsplash.com/photo-1551288049-bebda4e38f71?w=600&h=340&fit=crop' }, likes: 67, comments: 8, reposts: 21, time: '6h' },
|
|
30
|
-
{ id: 4, username: 'mike_ops', display_name: 'Mike Johnson', initials: 'MJ', color: '#22c55e', content: 'Migrated our entire CI/CD pipeline from Jenkins to GitHub Actions. Results: 40% faster builds, half the YAML config, and our team actually understands the pipeline now. The reusable workflow feature is a game changer — we have one workflow that all 12 services share.', media: { type: 'video', url: 'https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm', poster: 'https://images.unsplash.com/photo-1618401471353-b98afee0b2eb?w=600&h=340&fit=crop' }, likes: 29, comments: 4, reposts: 6, time: '1d' },
|
|
31
|
-
{ id: 5, username: 'aymen', display_name: 'Aymen Labidi', initials: 'AL', color: '#7c3aed', avatar: 'https://avatars.githubusercontent.com/u/3775924?v=4', content: 'Hot take: TypeScript\'s type system is a programming language in itself. Conditional types, mapped types, template literal types, recursive types... you can implement a full type-level parser if you want to. And I love every bit of it. The DX improvement from strict types is worth every extra line of type annotation.', likes: 85, comments: 12, reposts: 15, time: '1d' },
|
|
32
|
-
{ id: 6, username: 'emma_data', display_name: 'Emma Williams', initials: 'EW', color: '#3572a5', content: 'PSA: If you\'re using pandas, try polars. I switched a production pipeline and it\'s not just faster — the lazy evaluation API is more intuitive, the error messages are clearer, and it handles larger-than-memory datasets natively. Here\'s a benchmark: 2GB CSV, pandas took 45s to groupby-aggregate, polars did it in 3s.', media: { type: 'image', url: 'https://images.unsplash.com/photo-1526374965328-7f61d4dc18c5?w=600&h=340&fit=crop' }, likes: 93, comments: 15, reposts: 34, time: '3d' },
|
|
33
|
-
],
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export class PageIndex extends LitElement {
|
|
38
|
-
static properties = { loaderData: { type: Object }, _storyOpen: { state: true }, _storyIdx: { state: true }, _storyProgress: { state: true } };
|
|
39
|
-
loaderData: any = {};
|
|
40
|
-
_storyOpen: boolean = false;
|
|
41
|
-
_storyIdx: number = 0;
|
|
42
|
-
_storyProgress: number = 0;
|
|
43
|
-
_storyTimer: any = null;
|
|
44
|
-
private _shell?: HTMLElement;
|
|
45
|
-
|
|
46
|
-
connectedCallback() {
|
|
47
|
-
super.connectedCallback();
|
|
48
|
-
requestAnimationFrame(() => {
|
|
49
|
-
const shell = this.closest('layout-root')?.shadowRoot?.querySelector('.shell') as HTMLElement;
|
|
50
|
-
if (shell) { shell.classList.add('show-right'); this._shell = shell; }
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
disconnectedCallback() {
|
|
55
|
-
super.disconnectedCallback();
|
|
56
|
-
if (this._shell) { this._shell.classList.remove('show-right'); this._shell = undefined; }
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
static styles = css`
|
|
61
|
-
:host { display: block; }
|
|
62
|
-
|
|
63
|
-
/* Stories bar */
|
|
64
|
-
.stories { display: flex; gap: 6px; padding: 12px 12px; border-bottom: 1px solid var(--border); overflow-x: auto; scrollbar-width: none; }
|
|
65
|
-
.stories::-webkit-scrollbar { display: none; }
|
|
66
|
-
.story { display: flex; flex-direction: column; align-items: center; gap: 6px; cursor: pointer; flex-shrink: 0; width: 64px; }
|
|
67
|
-
.story-ring { width: 56px; height: 80px; border-radius: 10px; overflow: visible; border: 1px solid var(--border); }
|
|
68
|
-
.story-ring.you { }
|
|
69
|
-
.story-avatar { width: 56px; height: 80px; border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 16px; font-weight: 700; color: #fff; overflow: hidden; }
|
|
70
|
-
.story-avatar img { width: 100%; height: 100%; object-fit: cover; }
|
|
71
|
-
.story-name { font-size: 11px; color: var(--text-secondary); max-width: 60px; text-align: center; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
72
|
-
.story-plus { position: absolute; bottom: -4px; left: 50%; transform: translateX(-50%); width: 18px; height: 18px; border-radius: 50%; background: var(--accent); color: #fff; display: flex; align-items: center; justify-content: center; border: 2px solid #fff; }
|
|
73
|
-
.story-plus svg { width: 10px; height: 10px; }
|
|
74
|
-
.story-avatar-wrap { position: relative; }
|
|
75
|
-
@keyframes pulse-ring { 0%,100% { opacity: 1; } 50% { opacity: 0.6; } }
|
|
76
|
-
|
|
77
|
-
/* Story/Reel viewer */
|
|
78
|
-
.story-viewer { position: fixed; inset: 0; z-index: 200; background: #000; display: flex; align-items: center; justify-content: center; }
|
|
79
|
-
.story-viewer-inner { position: relative; width: 100%; max-width: 420px; height: 100%; max-height: 100vh; }
|
|
80
|
-
.story-image { width: 100%; height: 100%; object-fit: cover; }
|
|
81
|
-
.story-progress { position: absolute; top: 8px; left: 8px; right: 8px; display: flex; gap: 4px; z-index: 2; }
|
|
82
|
-
.story-bar { flex: 1; height: 3px; background: rgba(255,255,255,0.3); border-radius: 2px; overflow: hidden; }
|
|
83
|
-
.story-bar-fill { height: 100%; background: var(--bg); border-radius: 2px; transition: width 0.1s linear; }
|
|
84
|
-
.story-top { position: absolute; top: 18px; left: 12px; right: 12px; display: flex; align-items: center; gap: 8px; z-index: 2; }
|
|
85
|
-
.story-top-avatar { width: 32px; height: 32px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 600; color: #fff; border: 2px solid #fff; overflow: hidden; }
|
|
86
|
-
.story-top-avatar img { width: 100%; height: 100%; object-fit: cover; }
|
|
87
|
-
.story-top-name { color: #fff; font-size: 14px; font-weight: 700; }
|
|
88
|
-
.story-top-time { color: rgba(255,255,255,0.7); font-size: 12px; }
|
|
89
|
-
.story-top-live { background: #e53935; color: #fff; font-size: 11px; font-weight: 700; padding: 2px 8px; border-radius: 4px; }
|
|
90
|
-
.story-close { margin-left: auto; width: 32px; height: 32px; border: none; background: none; color: #fff; cursor: pointer; display: flex; align-items: center; justify-content: center; }
|
|
91
|
-
.story-caption { position: absolute; bottom: 0; left: 0; right: 0; padding: 40px 16px 24px; background: linear-gradient(transparent, rgba(0,0,0,0.7)); z-index: 2; }
|
|
92
|
-
.story-caption-text { color: #fff; font-size: 15px; line-height: 1.4; }
|
|
93
|
-
.story-nav { position: absolute; inset: 0; display: flex; z-index: 1; }
|
|
94
|
-
.story-nav-prev, .story-nav-next { flex: 1; cursor: pointer; }
|
|
95
|
-
|
|
96
|
-
/* Widget cards */
|
|
97
|
-
.widget-card { border-bottom: 1px solid var(--border); padding: 12px 16px; }
|
|
98
|
-
.widget-header { display: flex; gap: 10px; margin-bottom: 10px; }
|
|
99
|
-
.widget-avatar { width: 38px; height: 38px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: 600; color: #fff; flex-shrink: 0; }
|
|
100
|
-
.widget-meta { flex: 1; }
|
|
101
|
-
.widget-name { font-weight: 700; font-size: 15px; }
|
|
102
|
-
.widget-handle { color: var(--text-secondary); font-size: 14px; }
|
|
103
|
-
.widget-badge { font-size: 11px; color: var(--accent); background: rgba(124,58,237,0.1); padding: 2px 8px; border-radius: 4px; font-weight: 600; margin-left: 6px; }
|
|
104
|
-
|
|
105
|
-
/* Poll widget */
|
|
106
|
-
.poll-question { font-size: 15px; font-weight: 600; margin-bottom: 8px; }
|
|
107
|
-
.poll-option { margin-bottom: 6px; }
|
|
108
|
-
.poll-bar { height: 28px; border-radius: 6px; background: var(--input-bg); position: relative; overflow: hidden; display: flex; align-items: center; padding: 0 10px; }
|
|
109
|
-
.poll-fill { position: absolute; left: 0; top: 0; bottom: 0; background: rgba(124,58,237,0.15); border-radius: 6px; }
|
|
110
|
-
.poll-label { position: relative; font-size: 13px; flex: 1; }
|
|
111
|
-
.poll-pct { position: relative; font-size: 13px; font-weight: 700; }
|
|
112
|
-
.poll-total { font-size: 12px; color: var(--text-secondary); margin-top: 6px; }
|
|
113
|
-
|
|
114
|
-
/* Event widget */
|
|
115
|
-
.event-card { background: var(--bg-secondary); border-radius: 12px; padding: 14px; }
|
|
116
|
-
.event-title { font-size: 16px; font-weight: 700; }
|
|
117
|
-
.event-detail { font-size: 13px; color: var(--text-secondary); margin-top: 4px; display: flex; align-items: center; gap: 6px; }
|
|
118
|
-
.event-footer { display: flex; justify-content: space-between; align-items: center; margin-top: 10px; }
|
|
119
|
-
.event-attendees { font-size: 12px; color: var(--text-secondary); }
|
|
120
|
-
.event-btn { padding: 6px 16px; border-radius: 9999px; border: 1px solid var(--accent); color: var(--accent); background: var(--bg); font-size: 13px; font-weight: 600; cursor: pointer; }
|
|
121
|
-
.event-btn:hover { background: rgba(124,58,237,0.1); }
|
|
122
|
-
|
|
123
|
-
/* Job widget */
|
|
124
|
-
.job-card { background: var(--bg-secondary); border-radius: 12px; padding: 14px; }
|
|
125
|
-
.job-company { font-size: 13px; color: var(--text-secondary); }
|
|
126
|
-
.job-role { font-size: 16px; font-weight: 700; margin-top: 2px; }
|
|
127
|
-
.job-details { display: flex; gap: 12px; margin-top: 6px; font-size: 13px; color: var(--text-secondary); }
|
|
128
|
-
.job-btn { margin-top: 10px; padding: 6px 16px; border-radius: 9999px; background: var(--accent); color: #fff; border: none; font-size: 13px; font-weight: 600; cursor: pointer; }
|
|
129
|
-
.job-btn:hover { background: var(--accent-hover); }
|
|
130
|
-
|
|
131
|
-
.post { display: flex; gap: 10px; padding: 12px 16px; border-bottom: 1px solid var(--border); cursor: pointer; text-decoration: none; color: inherit; }
|
|
132
|
-
.post:hover { background: var(--bg-secondary); }
|
|
133
|
-
.post-avatar { width: 38px; height: 38px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: 600; color: #fff; flex-shrink: 0; overflow: hidden; }
|
|
134
|
-
.post-avatar img { width: 100%; height: 100%; object-fit: cover; }
|
|
135
|
-
.post-media { margin-top: 8px; border-radius: 12px; overflow: hidden; aspect-ratio: 16/9; background: var(--input-bg); }
|
|
136
|
-
.post-media img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
|
137
|
-
.post-media video { width: 100%; height: 100%; object-fit: cover; display: block; }
|
|
138
|
-
.post-media .live-badge { position: absolute; top: 8px; left: 8px; background: #e53935; color: #fff; font-size: 11px; font-weight: 700; padding: 2px 8px; border-radius: 4px; }
|
|
139
|
-
.post-media-wrap { position: relative; height: 100%; }
|
|
140
|
-
.post-body { flex: 1; min-width: 0; }
|
|
141
|
-
.post-header { display: flex; align-items: baseline; gap: 4px; }
|
|
142
|
-
.post-name { font-weight: 700; font-size: 15px; }
|
|
143
|
-
.post-handle { color: var(--text-secondary); font-size: 14px; }
|
|
144
|
-
.post-dot { color: var(--text-secondary); font-size: 14px; }
|
|
145
|
-
.post-time { color: var(--text-secondary); font-size: 14px; }
|
|
146
|
-
.post-content { margin-top: 2px; line-height: 1.4; font-size: 15px; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; }
|
|
147
|
-
.post-content.expanded { -webkit-line-clamp: unset; }
|
|
148
|
-
.see-more { color: var(--accent); font-size: 14px; border: none; background: none; cursor: pointer; padding: 2px 0; font-weight: 500; }
|
|
149
|
-
.see-more:hover { text-decoration: underline; }
|
|
150
|
-
.post-actions { display: flex; gap: 0; margin-top: 8px; margin-left: -8px; }
|
|
151
|
-
.action { display: flex; align-items: center; gap: 4px; color: var(--text-secondary); font-size: 13px; border: none; background: none; cursor: pointer; padding: 4px 8px; border-radius: 9999px; min-width: 60px; }
|
|
152
|
-
.action:hover { color: var(--accent); background: rgba(29,155,240,0.1); }
|
|
153
|
-
.action.repost:hover { color: #00ba7c; background: rgba(0,186,124,0.1); }
|
|
154
|
-
.action.heart:hover { color: #f91880; background: rgba(249,24,128,0.1); }
|
|
155
|
-
`;
|
|
156
|
-
|
|
157
|
-
_openStory(idx: number) {
|
|
158
|
-
this._storyIdx = idx;
|
|
159
|
-
this._storyOpen = true;
|
|
160
|
-
this._storyProgress = 0;
|
|
161
|
-
this._startStoryTimer();
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
_closeStory() {
|
|
165
|
-
this._storyOpen = false;
|
|
166
|
-
if (this._storyTimer) { clearInterval(this._storyTimer); this._storyTimer = null; }
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
_startStoryTimer() {
|
|
170
|
-
if (this._storyTimer) clearInterval(this._storyTimer);
|
|
171
|
-
this._storyProgress = 0;
|
|
172
|
-
this._storyTimer = setInterval(() => {
|
|
173
|
-
this._storyProgress += 2;
|
|
174
|
-
if (this._storyProgress >= 100) this._nextStory();
|
|
175
|
-
}, 100);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
_nextStory() {
|
|
179
|
-
const stories = this.loaderData.stories || [];
|
|
180
|
-
if (this._storyIdx < stories.length - 1) {
|
|
181
|
-
this._storyIdx++;
|
|
182
|
-
this._startStoryTimer();
|
|
183
|
-
} else {
|
|
184
|
-
this._closeStory();
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
_prevStory() {
|
|
189
|
-
if (this._storyIdx > 0) {
|
|
190
|
-
this._storyIdx--;
|
|
191
|
-
this._startStoryTimer();
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
_renderWidget(w: any) {
|
|
196
|
-
if (w.type === 'poll') return html`
|
|
197
|
-
<div class="widget-card">
|
|
198
|
-
<div class="widget-header">
|
|
199
|
-
<div class="widget-avatar" style="background:${w.color}">${w.initials}</div>
|
|
200
|
-
<div class="widget-meta">
|
|
201
|
-
<span class="widget-name">${w.display_name}</span>
|
|
202
|
-
<span class="widget-handle">· ${w.time}</span>
|
|
203
|
-
<span class="widget-badge">Poll</span>
|
|
204
|
-
</div>
|
|
205
|
-
</div>
|
|
206
|
-
<div class="poll-question">${w.question}</div>
|
|
207
|
-
${w.options.map((o: any) => html`
|
|
208
|
-
<div class="poll-option">
|
|
209
|
-
<div class="poll-bar"><div class="poll-fill" style="width:${o.pct}%"></div><span class="poll-label">${o.label}</span><span class="poll-pct">${o.pct}%</span></div>
|
|
210
|
-
</div>
|
|
211
|
-
`)}
|
|
212
|
-
<div class="poll-total">${w.votes} votes</div>
|
|
213
|
-
</div>`;
|
|
214
|
-
if (w.type === 'event') return html`
|
|
215
|
-
<div class="widget-card">
|
|
216
|
-
<div class="widget-header">
|
|
217
|
-
<div class="widget-avatar" style="background:${w.color}">${w.initials}</div>
|
|
218
|
-
<div class="widget-meta">
|
|
219
|
-
<span class="widget-name">${w.display_name}</span>
|
|
220
|
-
<span class="widget-handle">· ${w.time}</span>
|
|
221
|
-
<span class="widget-badge">Event</span>
|
|
222
|
-
</div>
|
|
223
|
-
</div>
|
|
224
|
-
<div class="event-card">
|
|
225
|
-
<div class="event-title">${w.title}</div>
|
|
226
|
-
<div class="event-detail">${w.date}</div>
|
|
227
|
-
<div class="event-detail">${w.location}</div>
|
|
228
|
-
<div class="event-footer">
|
|
229
|
-
<span class="event-attendees">${w.attendees} attending</span>
|
|
230
|
-
<button class="event-btn">Interested</button>
|
|
231
|
-
</div>
|
|
232
|
-
</div>
|
|
233
|
-
</div>`;
|
|
234
|
-
if (w.type === 'job') return html`
|
|
235
|
-
<div class="widget-card">
|
|
236
|
-
<div class="widget-header">
|
|
237
|
-
<div class="widget-avatar" style="background:${w.color}">${w.initials}</div>
|
|
238
|
-
<div class="widget-meta">
|
|
239
|
-
<span class="widget-name">${w.display_name}</span>
|
|
240
|
-
<span class="widget-handle">· ${w.time}</span>
|
|
241
|
-
<span class="widget-badge">Job</span>
|
|
242
|
-
</div>
|
|
243
|
-
</div>
|
|
244
|
-
<div class="job-card">
|
|
245
|
-
<div class="job-company">${w.company}</div>
|
|
246
|
-
<div class="job-role">${w.role}</div>
|
|
247
|
-
<div class="job-details"><span>${w.loc}</span><span>${w.salary}</span></div>
|
|
248
|
-
<button class="job-btn">Apply</button>
|
|
249
|
-
</div>
|
|
250
|
-
</div>`;
|
|
251
|
-
return '';
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
render() {
|
|
255
|
-
const posts = this.loaderData.posts || [];
|
|
256
|
-
const stories = this.loaderData.stories || [];
|
|
257
|
-
const widgets = this.loaderData.widgets || [];
|
|
258
|
-
|
|
259
|
-
// Merge posts and widgets by position
|
|
260
|
-
const feed: any[] = [];
|
|
261
|
-
let postIdx = 0;
|
|
262
|
-
for (let i = 0; postIdx < posts.length; i++) {
|
|
263
|
-
const w = widgets.find((w: any) => w.position === i);
|
|
264
|
-
if (w) { feed.push({ _widget: true, ...w }); }
|
|
265
|
-
if (postIdx < posts.length) { feed.push(posts[postIdx]); postIdx++; }
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
return html`
|
|
269
|
-
<div class="stories">
|
|
270
|
-
${stories.map((s: any, i: number) => html`
|
|
271
|
-
<div class="story" @click=${() => this._openStory(i)}>
|
|
272
|
-
<div class="story-ring ${s.isYou ? 'you' : ''}">
|
|
273
|
-
<div class="story-avatar-wrap">
|
|
274
|
-
<div class="story-avatar">
|
|
275
|
-
<img src="${s.image}" alt="">
|
|
276
|
-
</div>
|
|
277
|
-
${s.isYou ? html`<div class="story-plus"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg></div>` : ''}
|
|
278
|
-
</div>
|
|
279
|
-
</div>
|
|
280
|
-
<span class="story-name">${s.name}</span>
|
|
281
|
-
</div>
|
|
282
|
-
`)}
|
|
283
|
-
</div>
|
|
284
|
-
${feed.map((item: any) => item._widget
|
|
285
|
-
? this._renderWidget(item)
|
|
286
|
-
: html`
|
|
287
|
-
<a class="post" href="/post/${item.id}">
|
|
288
|
-
<div class="post-avatar" style="background:${item.color}">${item.avatar ? html`<img src="${item.avatar}" alt="">` : item.initials}</div>
|
|
289
|
-
<div class="post-body">
|
|
290
|
-
<div class="post-header">
|
|
291
|
-
<span class="post-name">${item.display_name}</span>
|
|
292
|
-
<span class="post-handle">@${item.username}</span>
|
|
293
|
-
<span class="post-dot">·</span>
|
|
294
|
-
<span class="post-time">${item.time}</span>
|
|
295
|
-
</div>
|
|
296
|
-
<div class="post-content ${item.id === 1 ? '' : 'expanded'}" id="content-${item.id}">${item.content}</div>
|
|
297
|
-
${item.id === 1 ? html`<button class="see-more" @click=${(e: Event) => { e.preventDefault(); e.stopPropagation(); const el = this.shadowRoot?.getElementById('content-1'); if (el) { el.classList.toggle('expanded'); (e.target as HTMLElement).textContent = el.classList.contains('expanded') ? 'Show less' : 'See more'; }}}>See more</button>` : ''}
|
|
298
|
-
${item.media ? html`
|
|
299
|
-
<div class="post-media">
|
|
300
|
-
<div class="post-media-wrap">
|
|
301
|
-
${item.media.type === 'video' ? html`
|
|
302
|
-
<video src="${item.media.url}" poster="${item.media.poster || ''}" controls preload="none"></video>
|
|
303
|
-
` : html`
|
|
304
|
-
<img src="${item.media.url}" alt="" loading="lazy">
|
|
305
|
-
`}
|
|
306
|
-
</div>
|
|
307
|
-
</div>
|
|
308
|
-
` : ''}
|
|
309
|
-
<div class="post-actions">
|
|
310
|
-
<button class="action">${svg.comment} ${item.comments}</button>
|
|
311
|
-
<button class="action repost">${svg.repost} ${item.reposts}</button>
|
|
312
|
-
<button class="action heart">${svg.heart} ${item.likes}</button>
|
|
313
|
-
<button class="action">${svg.share}</button>
|
|
314
|
-
</div>
|
|
315
|
-
</div>
|
|
316
|
-
</a>
|
|
317
|
-
`)}
|
|
318
|
-
${this._storyOpen ? html`
|
|
319
|
-
<div class="story-viewer">
|
|
320
|
-
<div class="story-viewer-inner">
|
|
321
|
-
<img class="story-image" src="${stories[this._storyIdx]?.image}" alt="">
|
|
322
|
-
<div class="story-progress">
|
|
323
|
-
${stories.map((_: any, i: number) => html`
|
|
324
|
-
<div class="story-bar">
|
|
325
|
-
<div class="story-bar-fill" style="width:${i < this._storyIdx ? 100 : i === this._storyIdx ? this._storyProgress : 0}%"></div>
|
|
326
|
-
</div>
|
|
327
|
-
`)}
|
|
328
|
-
</div>
|
|
329
|
-
<div class="story-top">
|
|
330
|
-
<div class="story-top-avatar" style="background:${stories[this._storyIdx]?.color}">
|
|
331
|
-
${stories[this._storyIdx]?.avatar ? html`<img src="${stories[this._storyIdx]?.avatar}" alt="">` : stories[this._storyIdx]?.initials}
|
|
332
|
-
</div>
|
|
333
|
-
<span class="story-top-name">${stories[this._storyIdx]?.name}</span>
|
|
334
|
-
<span class="story-top-time">5h</span>
|
|
335
|
-
<button class="story-close" @click=${() => this._closeStory()}>
|
|
336
|
-
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
|
337
|
-
</button>
|
|
338
|
-
</div>
|
|
339
|
-
<div class="story-caption">
|
|
340
|
-
<div class="story-caption-text">${stories[this._storyIdx]?.caption}</div>
|
|
341
|
-
</div>
|
|
342
|
-
<div class="story-nav">
|
|
343
|
-
<div class="story-nav-prev" @click=${() => this._prevStory()}></div>
|
|
344
|
-
<div class="story-nav-next" @click=${() => this._nextStory()}></div>
|
|
345
|
-
</div>
|
|
346
|
-
</div>
|
|
347
|
-
</div>
|
|
348
|
-
` : ''}
|
|
349
|
-
`;
|
|
350
|
-
}
|
|
351
|
-
}
|