@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.
Files changed (76) hide show
  1. package/dist/auth/native-auth.d.ts +9 -0
  2. package/dist/auth/native-auth.js +49 -2
  3. package/dist/auth/routes/login.js +24 -1
  4. package/dist/auth/routes/totp.d.ts +22 -0
  5. package/dist/auth/routes/totp.js +232 -0
  6. package/dist/auth/routes.js +14 -0
  7. package/dist/auth/token.js +2 -2
  8. package/dist/build/build-server.d.ts +2 -1
  9. package/dist/build/build-server.js +10 -1
  10. package/dist/build/build.js +13 -4
  11. package/dist/build/scan.d.ts +1 -0
  12. package/dist/build/scan.js +2 -1
  13. package/dist/build/serve.js +131 -11
  14. package/dist/dev-server/config.js +18 -1
  15. package/dist/dev-server/index-html.d.ts +1 -0
  16. package/dist/dev-server/index-html.js +4 -1
  17. package/dist/dev-server/plugins/vite-plugin-routes.js +3 -2
  18. package/dist/dev-server/plugins/vite-plugin-virtual-modules.js +34 -6
  19. package/dist/dev-server/server.js +146 -88
  20. package/dist/dev-server/ssr-render.js +10 -2
  21. package/dist/editor/ai/backend.js +11 -2
  22. package/dist/editor/ai/deepseek-client.d.ts +7 -0
  23. package/dist/editor/ai/deepseek-client.js +113 -0
  24. package/dist/editor/ai/opencode-client.d.ts +1 -1
  25. package/dist/editor/ai/opencode-client.js +21 -47
  26. package/dist/editor/ai-chat-panel.js +27 -1
  27. package/dist/editor/editor-bridge.js +2 -1
  28. package/dist/editor/overlay-hmr.js +2 -1
  29. package/dist/runtime/app-shell.d.ts +1 -1
  30. package/dist/runtime/app-shell.js +1 -0
  31. package/dist/runtime/island.d.ts +16 -0
  32. package/dist/runtime/island.js +80 -0
  33. package/dist/runtime/router-hydration.js +9 -2
  34. package/dist/runtime/router.d.ts +3 -1
  35. package/dist/runtime/router.js +49 -1
  36. package/dist/runtime/webrtc.d.ts +44 -0
  37. package/dist/runtime/webrtc.js +263 -13
  38. package/dist/shared/dom-shims.js +4 -2
  39. package/dist/shared/types.d.ts +1 -0
  40. package/dist/storage/adapters/s3.js +6 -3
  41. package/package.json +33 -7
  42. package/templates/social/api/posts/[id].ts +0 -14
  43. package/templates/social/api/posts.ts +0 -11
  44. package/templates/social/api/profile/[username].ts +0 -10
  45. package/templates/social/api/upload.ts +0 -19
  46. package/templates/social/data/migrations/001_init.sql +0 -78
  47. package/templates/social/data/migrations/002_add_image_url.sql +0 -1
  48. package/templates/social/data/migrations/003_auth.sql +0 -7
  49. package/templates/social/docs/architecture.md +0 -76
  50. package/templates/social/docs/components.md +0 -100
  51. package/templates/social/docs/data.md +0 -89
  52. package/templates/social/docs/pages.md +0 -96
  53. package/templates/social/docs/theming.md +0 -52
  54. package/templates/social/lib/media.ts +0 -130
  55. package/templates/social/lumenjs.auth.ts +0 -21
  56. package/templates/social/lumenjs.config.ts +0 -3
  57. package/templates/social/package.json +0 -5
  58. package/templates/social/pages/_layout.ts +0 -239
  59. package/templates/social/pages/apps/[id].ts +0 -173
  60. package/templates/social/pages/apps/index.ts +0 -116
  61. package/templates/social/pages/auth/login.ts +0 -92
  62. package/templates/social/pages/bookmarks.ts +0 -57
  63. package/templates/social/pages/explore.ts +0 -73
  64. package/templates/social/pages/index.ts +0 -351
  65. package/templates/social/pages/messages.ts +0 -298
  66. package/templates/social/pages/new.ts +0 -77
  67. package/templates/social/pages/notifications.ts +0 -73
  68. package/templates/social/pages/post/[id].ts +0 -124
  69. package/templates/social/pages/profile/[username].ts +0 -100
  70. package/templates/social/pages/settings/accessibility.ts +0 -153
  71. package/templates/social/pages/settings/account.ts +0 -260
  72. package/templates/social/pages/settings/help.ts +0 -141
  73. package/templates/social/pages/settings/language.ts +0 -103
  74. package/templates/social/pages/settings/privacy.ts +0 -183
  75. package/templates/social/pages/settings/security.ts +0 -133
  76. package/templates/social/pages/settings.ts +0 -185
@@ -1,141 +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
- chevron: html`<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>`,
6
- chevronDown: html`<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>`,
7
- 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>`,
8
- book: html`<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 19.5A2.5 2.5 0 016.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 014 19.5v-15A2.5 2.5 0 016.5 2z"/></svg>`,
9
- messageCircle: html`<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M21 11.5a8.38 8.38 0 01-.9 3.8 8.5 8.5 0 01-7.6 4.7 8.38 8.38 0 01-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 01-.9-3.8 8.5 8.5 0 014.7-7.6 8.38 8.38 0 013.8-.9h.5a8.48 8.48 0 018 8v.5z"/></svg>`,
10
- 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>`,
11
- flag: html`<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"/><line x1="4" y1="22" x2="4" y2="15"/></svg>`,
12
- };
13
-
14
- const FAQ = [
15
- { q: 'How do I change my password?', a: 'Go to Settings > Password and security > Change password. Enter your current password, then your new password twice to confirm.' },
16
- { q: 'How do I make my account private?', a: 'Go to Settings > Privacy and safety and enable "Private profile". Only approved followers will be able to see your posts.' },
17
- { q: 'How do I delete my account?', a: 'Go to Settings > Account information and scroll to the bottom. Click "Deactivate" to disable your account. For permanent deletion, contact support.' },
18
- { q: 'How do I block or mute someone?', a: 'Visit their profile and click the three-dot menu. Select "Block" or "Mute". You can manage blocked and muted accounts in Settings > Privacy and safety.' },
19
- { q: 'How do I change the app language?', a: 'Go to Settings > Language and select your preferred language from the list. The interface will update immediately.' },
20
- { q: 'How do I enable dark mode?', a: 'Go to Settings and toggle "Dark mode" under the Preferences section. You can also use the keyboard shortcut Ctrl+Shift+D.' },
21
- { q: 'How do I report a bug?', a: 'Use the "Report a bug" option below to send us details about the issue. Include steps to reproduce and screenshots if possible.' },
22
- { q: 'How do I enable two-factor authentication?', a: 'Go to Settings > Password and security and enable the "Authenticator app" toggle. Follow the instructions to link your authenticator app.' },
23
- ];
24
-
25
- export async function loader() {
26
- return {};
27
- }
28
-
29
- export class PageSettingsHelp extends LitElement {
30
- static properties = {
31
- loaderData: { type: Object },
32
- _search: { state: true },
33
- _openFaq: { state: true },
34
- };
35
- loaderData: any = {};
36
- _search: string = '';
37
- _openFaq: number = -1;
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
-
43
- .header { display: flex; align-items: center; gap: 12px; padding: 16px 20px; border-bottom: 1px solid var(--border-light); }
44
- .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; }
45
- .back:hover { background: var(--bg-secondary); }
46
- .header h2 { font-size: 20px; font-weight: 700; margin: 0; }
47
-
48
- .search-wrap { position: relative; padding: 12px 20px; border-bottom: 1px solid var(--border); }
49
- .search-icon { position: absolute; left: 32px; top: 50%; transform: translateY(-50%); color: var(--text-secondary); display: flex; }
50
- .search-input { width: 100%; padding: 10px 12px 10px 36px; border: 1px solid var(--border); border-radius: 8px; font-size: 14px; outline: none; background: var(--input-bg); color: var(--text); }
51
- .search-input:focus { border-color: var(--accent); background: var(--bg); }
52
-
53
- .section-title { font-size: 12px; font-weight: 600; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.5px; padding: 12px 20px 6px; }
54
-
55
- .faq-item { border-bottom: 1px solid var(--border); }
56
- .faq-item:last-child { border-bottom: none; }
57
- .faq-q { display: flex; align-items: center; gap: 12px; padding: 14px 20px; cursor: pointer; font-size: 14px; font-weight: 600; color: var(--text); }
58
- .faq-q:hover { background: var(--bg-secondary); }
59
- .faq-q span { flex: 1; }
60
- .faq-q .faq-chevron { color: var(--text-tertiary); transition: transform 0.2s; }
61
- .faq-q .faq-chevron.open { transform: rotate(90deg); }
62
- .faq-a { padding: 0 20px 14px 20px; font-size: 13px; color: var(--text-secondary); line-height: 1.5; }
63
-
64
- .contact-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; padding: 14px 20px; }
65
- @media (max-width: 500px) { .contact-grid { grid-template-columns: 1fr; } }
66
- .contact-card { display: flex; align-items: center; gap: 12px; padding: 14px 16px; border: 1px solid var(--border); border-radius: 8px; cursor: pointer; }
67
- .contact-card:hover { background: var(--bg-secondary); border-color: var(--accent); }
68
- .contact-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; }
69
- .contact-body { flex: 1; }
70
- .contact-label { font-size: 14px; font-weight: 600; }
71
- .contact-desc { font-size: 12px; color: var(--text-secondary); margin-top: 1px; }
72
-
73
- .empty { padding: 20px; text-align: center; font-size: 13px; color: var(--text-tertiary); }
74
- `;
75
-
76
- render() {
77
- const q = this._search.toLowerCase();
78
- const filtered = FAQ.filter(f => f.q.toLowerCase().includes(q) || f.a.toLowerCase().includes(q));
79
-
80
- return html`
81
- <div class="card">
82
- <div class="header">
83
- <a class="back" href="/settings">${svg.back}</a>
84
- <h2>Help center</h2>
85
- </div>
86
-
87
- <div class="search-wrap">
88
- <span class="search-icon">${svg.search}</span>
89
- <input class="search-input" type="text" placeholder="Search help articles..." .value=${this._search} @input=${(e: Event) => { this._search = (e.target as HTMLInputElement).value; }}>
90
- </div>
91
- </div>
92
-
93
- <div class="card">
94
- <div class="section-title">Frequently asked questions</div>
95
- ${filtered.length === 0 ? html`<div class="empty">No results found</div>` : filtered.map((f, i) => html`
96
- <div class="faq-item">
97
- <div class="faq-q" @click=${() => { this._openFaq = this._openFaq === i ? -1 : i; }}>
98
- <span>${f.q}</span>
99
- <span class="faq-chevron ${this._openFaq === i ? 'open' : ''}">${svg.chevron}</span>
100
- </div>
101
- ${this._openFaq === i ? html`<div class="faq-a">${f.a}</div>` : ''}
102
- </div>
103
- `)}
104
- </div>
105
-
106
- <div class="card">
107
- <div class="section-title">Contact us</div>
108
- <div class="contact-grid">
109
- <div class="contact-card">
110
- <div class="contact-icon">${svg.messageCircle}</div>
111
- <div class="contact-body">
112
- <div class="contact-label">Live chat</div>
113
- <div class="contact-desc">Chat with our support team</div>
114
- </div>
115
- </div>
116
- <div class="contact-card">
117
- <div class="contact-icon">${svg.mail}</div>
118
- <div class="contact-body">
119
- <div class="contact-label">Email support</div>
120
- <div class="contact-desc">support@nuraly.io</div>
121
- </div>
122
- </div>
123
- <div class="contact-card">
124
- <div class="contact-icon">${svg.book}</div>
125
- <div class="contact-body">
126
- <div class="contact-label">Documentation</div>
127
- <div class="contact-desc">Browse the full docs</div>
128
- </div>
129
- </div>
130
- <div class="contact-card">
131
- <div class="contact-icon">${svg.flag}</div>
132
- <div class="contact-body">
133
- <div class="contact-label">Report a bug</div>
134
- <div class="contact-desc">Help us improve</div>
135
- </div>
136
- </div>
137
- </div>
138
- </div>
139
- `;
140
- }
141
- }
@@ -1,103 +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
- check: html`<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" stroke-width="2.5"><polyline points="20 6 9 17 4 12"/></svg>`,
6
- 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>`,
7
- };
8
-
9
- const LANGUAGES = [
10
- { code: 'en', name: 'English', native: 'English' },
11
- { code: 'fr', name: 'French', native: 'Fran\u00e7ais' },
12
- { code: 'ar', name: 'Arabic', native: '\u0627\u0644\u0639\u0631\u0628\u064a\u0629' },
13
- { code: 'es', name: 'Spanish', native: 'Espa\u00f1ol' },
14
- { code: 'de', name: 'German', native: 'Deutsch' },
15
- { code: 'it', name: 'Italian', native: 'Italiano' },
16
- { code: 'pt', name: 'Portuguese', native: 'Portugu\u00eas' },
17
- { code: 'ja', name: 'Japanese', native: '\u65e5\u672c\u8a9e' },
18
- { code: 'ko', name: 'Korean', native: '\ud55c\uad6d\uc5b4' },
19
- { code: 'zh', name: 'Chinese', native: '\u4e2d\u6587' },
20
- { code: 'ru', name: 'Russian', native: '\u0420\u0443\u0441\u0441\u043a\u0438\u0439' },
21
- { code: 'tr', name: 'Turkish', native: 'T\u00fcrk\u00e7e' },
22
- { code: 'nl', name: 'Dutch', native: 'Nederlands' },
23
- { code: 'hi', name: 'Hindi', native: '\u0939\u093f\u0928\u094d\u0926\u0940' },
24
- { code: 'sv', name: 'Swedish', native: 'Svenska' },
25
- ];
26
-
27
- export async function loader() {
28
- return { currentLang: 'en' };
29
- }
30
-
31
- export class PageSettingsLanguage extends LitElement {
32
- static properties = {
33
- loaderData: { type: Object },
34
- _selected: { state: true },
35
- _search: { state: true },
36
- };
37
- loaderData: any = {};
38
- _selected: string = 'en';
39
- _search: string = '';
40
-
41
- connectedCallback() { super.connectedCallback(); this._selected = this.loaderData.currentLang || 'en'; }
42
-
43
- static styles = css`
44
- :host { display: block; }
45
- .card { background: var(--bg); border-radius: 6px; border: 1px solid var(--border); overflow: hidden; margin-bottom: 12px; }
46
-
47
- .header { display: flex; align-items: center; gap: 12px; padding: 16px 20px; border-bottom: 1px solid var(--border-light); }
48
- .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; }
49
- .back:hover { background: var(--bg-secondary); }
50
- .header h2 { font-size: 20px; font-weight: 700; margin: 0; }
51
-
52
- .search-wrap { position: relative; padding: 12px 20px; border-bottom: 1px solid var(--border); }
53
- .search-icon { position: absolute; left: 32px; top: 50%; transform: translateY(-50%); color: var(--text-secondary); display: flex; }
54
- .search-input { width: 100%; padding: 10px 12px 10px 36px; border: 1px solid var(--border); border-radius: 8px; font-size: 14px; outline: none; background: var(--input-bg); color: var(--text); }
55
- .search-input:focus { border-color: var(--accent); background: var(--bg); }
56
-
57
- .lang-item { display: flex; align-items: center; gap: 12px; padding: 14px 20px; cursor: pointer; border-bottom: 1px solid var(--border); }
58
- .lang-item:last-child { border-bottom: none; }
59
- .lang-item:hover { background: var(--bg-secondary); }
60
- .lang-item.selected { background: var(--bg-secondary); }
61
- .lang-body { flex: 1; }
62
- .lang-name { font-size: 14px; font-weight: 600; }
63
- .lang-native { font-size: 12px; color: var(--text-secondary); margin-top: 1px; }
64
- .lang-check { width: 20px; display: flex; align-items: center; justify-content: center; }
65
-
66
- .section-title { font-size: 12px; font-weight: 600; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.5px; padding: 12px 20px 6px; }
67
-
68
- .info { padding: 12px 20px; font-size: 13px; color: var(--text-secondary); line-height: 1.4; }
69
-
70
- .empty { padding: 20px; text-align: center; font-size: 13px; color: var(--text-tertiary); }
71
- `;
72
-
73
- render() {
74
- const q = this._search.toLowerCase();
75
- const filtered = LANGUAGES.filter(l => l.name.toLowerCase().includes(q) || l.native.toLowerCase().includes(q));
76
- return html`
77
- <div class="card">
78
- <div class="header">
79
- <a class="back" href="/settings">${svg.back}</a>
80
- <h2>Language</h2>
81
- </div>
82
-
83
- <div class="search-wrap">
84
- <span class="search-icon">${svg.search}</span>
85
- <input class="search-input" type="text" placeholder="Search languages..." .value=${this._search} @input=${(e: Event) => { this._search = (e.target as HTMLInputElement).value; }}>
86
- </div>
87
-
88
- <div class="info">Select your preferred language. This changes the interface language across the app.</div>
89
-
90
- <div class="section-title">Available languages</div>
91
- ${filtered.length === 0 ? html`<div class="empty">No languages match your search</div>` : filtered.map(l => html`
92
- <div class="lang-item ${this._selected === l.code ? 'selected' : ''}" @click=${() => { this._selected = l.code; }}>
93
- <div class="lang-body">
94
- <div class="lang-name">${l.name}</div>
95
- <div class="lang-native">${l.native}</div>
96
- </div>
97
- <div class="lang-check">${this._selected === l.code ? svg.check : ''}</div>
98
- </div>
99
- `)}
100
- </div>
101
- `;
102
- }
103
- }
@@ -1,183 +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
- 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>`,
6
- eyeOff: html`<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M17.94 17.94A10.07 10.07 0 0112 20c-7 0-11-8-11-8a18.45 18.45 0 015.06-5.94M9.9 4.24A9.12 9.12 0 0112 4c7 0 11 8 11 8a18.5 18.5 0 01-2.16 3.19m-6.72-1.07a3 3 0 11-4.24-4.24"/><line x1="1" y1="1" x2="23" y2="23"/></svg>`,
7
- userX: html`<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><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="18" y1="8" x2="23" y2="13"/><line x1="23" y1="8" x2="18" y2="13"/></svg>`,
8
- volumeX: html`<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><line x1="23" y1="9" x2="17" y2="15"/><line x1="17" y1="9" x2="23" y2="15"/></svg>`,
9
- filter: html`<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"/></svg>`,
10
- mapPin: html`<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><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>`,
11
- tag: html`<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M20.59 13.41l-7.17 7.17a2 2 0 01-2.83 0L2 12V2h10l8.59 8.59a2 2 0 010 2.82z"/><line x1="7" y1="7" x2="7.01" y2="7"/></svg>`,
12
- };
13
-
14
- export async function loader() {
15
- return {
16
- blockedUsers: [
17
- { username: 'spammer42', display_name: 'Spam Bot', blockedDate: 'Mar 15, 2026' },
18
- { username: 'troll_account', display_name: 'Troll', blockedDate: 'Feb 28, 2026' },
19
- ],
20
- mutedUsers: [
21
- { username: 'loud_poster', display_name: 'Loud Poster', mutedDate: 'Mar 10, 2026' },
22
- ],
23
- mutedWords: ['spoiler', 'nsfw', 'spam'],
24
- };
25
- }
26
-
27
- export class PageSettingsPrivacy extends LitElement {
28
- static properties = {
29
- loaderData: { type: Object },
30
- _privateProfile: { state: true },
31
- _hideActivity: { state: true },
32
- _allowTagging: { state: true },
33
- _showLocation: { state: true },
34
- _sensitiveFilter: { state: true },
35
- };
36
- loaderData: any = {};
37
- _privateProfile = false;
38
- _hideActivity = false;
39
- _allowTagging = true;
40
- _showLocation = true;
41
- _sensitiveFilter = true;
42
-
43
- static styles = css`
44
- :host { display: block; }
45
- .card { background: var(--bg); border-radius: 6px; border: 1px solid var(--border); overflow: hidden; margin-bottom: 12px; }
46
-
47
- .header { display: flex; align-items: center; gap: 12px; padding: 16px 20px; border-bottom: 1px solid var(--border-light); }
48
- .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; }
49
- .back:hover { background: var(--bg-secondary); }
50
- .header h2 { font-size: 20px; font-weight: 700; margin: 0; }
51
-
52
- .section-title { font-size: 12px; font-weight: 600; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.5px; padding: 12px 20px 6px; }
53
-
54
- .toggle-row { display: flex; align-items: center; gap: 12px; padding: 14px 20px; border-bottom: 1px solid var(--border); }
55
- .toggle-row:last-child { border-bottom: none; }
56
- .toggle-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; }
57
- .toggle-body { flex: 1; }
58
- .toggle-label { font-size: 14px; font-weight: 600; }
59
- .toggle-desc { font-size: 12px; color: var(--text-secondary); margin-top: 1px; }
60
- .toggle { position: relative; width: 42px; height: 24px; flex-shrink: 0; }
61
- .toggle input { opacity: 0; width: 0; height: 0; }
62
- .toggle-slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background: #ccc; border-radius: 24px; transition: 0.2s; }
63
- .toggle-slider:before { content: ''; position: absolute; height: 18px; width: 18px; left: 3px; bottom: 3px; background: var(--bg); border-radius: 50%; transition: 0.2s; }
64
- .toggle input:checked + .toggle-slider { background: var(--accent); }
65
- .toggle input:checked + .toggle-slider:before { transform: translateX(18px); }
66
-
67
- .list-item { display: flex; align-items: center; gap: 12px; padding: 12px 20px; border-bottom: 1px solid var(--border); }
68
- .list-item:last-child { border-bottom: none; }
69
- .list-avatar { width: 36px; height: 36px; border-radius: 50%; background: var(--bg-secondary); display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: 700; color: var(--text-secondary); flex-shrink: 0; }
70
- .list-body { flex: 1; }
71
- .list-name { font-size: 14px; font-weight: 600; }
72
- .list-meta { font-size: 12px; color: var(--text-secondary); }
73
- .btn-unblock { padding: 4px 14px; border-radius: 6px; border: 1px solid var(--border); background: var(--bg); font-size: 12px; font-weight: 600; cursor: pointer; color: var(--text-secondary); }
74
- .btn-unblock:hover { background: var(--bg-secondary); color: var(--text); }
75
-
76
- .muted-words { display: flex; flex-wrap: wrap; gap: 8px; padding: 14px 20px; }
77
- .muted-word { display: flex; align-items: center; gap: 6px; padding: 4px 12px; border-radius: 9999px; background: var(--bg-secondary); font-size: 13px; color: var(--text-secondary); }
78
- .muted-word-remove { width: 16px; height: 16px; border-radius: 50%; border: none; background: none; cursor: pointer; color: var(--text-tertiary); font-size: 14px; display: flex; align-items: center; justify-content: center; }
79
- .muted-word-remove:hover { color: #e53935; }
80
- .btn-add-word { padding: 4px 12px; border-radius: 9999px; border: 1px dashed var(--border); background: none; font-size: 13px; color: var(--text-tertiary); cursor: pointer; }
81
- .btn-add-word:hover { border-color: var(--accent); color: var(--accent); }
82
-
83
- .empty { padding: 20px; text-align: center; font-size: 13px; color: var(--text-tertiary); }
84
- `;
85
-
86
- render() {
87
- const { blockedUsers = [], mutedUsers = [], mutedWords = [] } = this.loaderData;
88
- return html`
89
- <div class="card">
90
- <div class="header">
91
- <a class="back" href="/settings">${svg.back}</a>
92
- <h2>Privacy and safety</h2>
93
- </div>
94
- </div>
95
-
96
- <div class="card">
97
- <div class="section-title">Profile visibility</div>
98
- <div class="toggle-row">
99
- <div class="toggle-icon">${svg.eyeOff}</div>
100
- <div class="toggle-body">
101
- <div class="toggle-label">Private profile</div>
102
- <div class="toggle-desc">Only approved followers can see your posts</div>
103
- </div>
104
- <label class="toggle"><input type="checkbox" .checked=${this._privateProfile} @change=${(e: Event) => { this._privateProfile = (e.target as HTMLInputElement).checked; }}><span class="toggle-slider"></span></label>
105
- </div>
106
- <div class="toggle-row">
107
- <div class="toggle-icon">${svg.eye}</div>
108
- <div class="toggle-body">
109
- <div class="toggle-label">Hide online activity</div>
110
- <div class="toggle-desc">Don't show when you were last active</div>
111
- </div>
112
- <label class="toggle"><input type="checkbox" .checked=${this._hideActivity} @change=${(e: Event) => { this._hideActivity = (e.target as HTMLInputElement).checked; }}><span class="toggle-slider"></span></label>
113
- </div>
114
- <div class="toggle-row">
115
- <div class="toggle-icon">${svg.tag}</div>
116
- <div class="toggle-body">
117
- <div class="toggle-label">Allow tagging</div>
118
- <div class="toggle-desc">Let others tag you in posts and comments</div>
119
- </div>
120
- <label class="toggle"><input type="checkbox" .checked=${this._allowTagging} @change=${(e: Event) => { this._allowTagging = (e.target as HTMLInputElement).checked; }}><span class="toggle-slider"></span></label>
121
- </div>
122
- <div class="toggle-row">
123
- <div class="toggle-icon">${svg.mapPin}</div>
124
- <div class="toggle-body">
125
- <div class="toggle-label">Show location</div>
126
- <div class="toggle-desc">Display your location on your profile</div>
127
- </div>
128
- <label class="toggle"><input type="checkbox" .checked=${this._showLocation} @change=${(e: Event) => { this._showLocation = (e.target as HTMLInputElement).checked; }}><span class="toggle-slider"></span></label>
129
- </div>
130
- </div>
131
-
132
- <div class="card">
133
- <div class="section-title">Content filtering</div>
134
- <div class="toggle-row">
135
- <div class="toggle-icon">${svg.filter}</div>
136
- <div class="toggle-body">
137
- <div class="toggle-label">Filter sensitive content</div>
138
- <div class="toggle-desc">Hide content flagged as potentially sensitive</div>
139
- </div>
140
- <label class="toggle"><input type="checkbox" .checked=${this._sensitiveFilter} @change=${(e: Event) => { this._sensitiveFilter = (e.target as HTMLInputElement).checked; }}><span class="toggle-slider"></span></label>
141
- </div>
142
- </div>
143
-
144
- <div class="card">
145
- <div class="section-title">Muted words</div>
146
- <div class="muted-words">
147
- ${mutedWords.map((w: string) => html`
148
- <span class="muted-word">${w}<button class="muted-word-remove">&times;</button></span>
149
- `)}
150
- <button class="btn-add-word">+ Add word</button>
151
- </div>
152
- </div>
153
-
154
- <div class="card">
155
- <div class="section-title">Blocked accounts (${blockedUsers.length})</div>
156
- ${blockedUsers.length === 0 ? html`<div class="empty">No blocked accounts</div>` : blockedUsers.map((u: any) => html`
157
- <div class="list-item">
158
- <div class="list-avatar">${u.display_name.charAt(0)}</div>
159
- <div class="list-body">
160
- <div class="list-name">${u.display_name} <span style="font-weight:400;color:var(--text-secondary)">@${u.username}</span></div>
161
- <div class="list-meta">Blocked ${u.blockedDate}</div>
162
- </div>
163
- <button class="btn-unblock">Unblock</button>
164
- </div>
165
- `)}
166
- </div>
167
-
168
- <div class="card">
169
- <div class="section-title">Muted accounts (${mutedUsers.length})</div>
170
- ${mutedUsers.length === 0 ? html`<div class="empty">No muted accounts</div>` : mutedUsers.map((u: any) => html`
171
- <div class="list-item">
172
- <div class="list-avatar">${u.display_name.charAt(0)}</div>
173
- <div class="list-body">
174
- <div class="list-name">${u.display_name} <span style="font-weight:400;color:var(--text-secondary)">@${u.username}</span></div>
175
- <div class="list-meta">Muted ${u.mutedDate}</div>
176
- </div>
177
- <button class="btn-unblock">Unmute</button>
178
- </div>
179
- `)}
180
- </div>
181
- `;
182
- }
183
- }
@@ -1,133 +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
- lock: html`<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0110 0v4"/></svg>`,
6
- shield: html`<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>`,
7
- monitor: html`<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>`,
8
- smartphone: html`<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="5" y="2" width="14" height="20" rx="2" ry="2"/><line x1="12" y1="18" x2="12.01" y2="18"/></svg>`,
9
- check: html`<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#22c55e" stroke-width="2.5"><polyline points="20 6 9 17 4 12"/></svg>`,
10
- };
11
-
12
- export async function loader() {
13
- return {
14
- passwordLastChanged: '2 months ago',
15
- twoFactor: false,
16
- sessions: [
17
- { device: 'Chrome on macOS', location: 'Tunis, Tunisia', current: true, lastActive: 'Now', type: 'desktop' },
18
- { device: 'Safari on iPhone', location: 'Tunis, Tunisia', current: false, lastActive: '3 hours ago', type: 'mobile' },
19
- { device: 'Firefox on Ubuntu', location: 'Sfax, Tunisia', current: false, lastActive: '2 days ago', type: 'desktop' },
20
- ],
21
- };
22
- }
23
-
24
- export class PageSettingsSecurity extends LitElement {
25
- static properties = { loaderData: { type: Object }, _2fa: { state: true } };
26
- loaderData: any = {};
27
- _2fa: boolean = false;
28
-
29
- connectedCallback() { super.connectedCallback(); this._2fa = this.loaderData.twoFactor || false; }
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: 16px 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
- .row-btn { 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); }
49
- .row-btn:hover { background: var(--bg-secondary); }
50
-
51
- .toggle { position: relative; width: 42px; height: 24px; flex-shrink: 0; }
52
- .toggle input { opacity: 0; width: 0; height: 0; }
53
- .toggle-slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background: #ccc; border-radius: 24px; transition: 0.2s; }
54
- .toggle-slider:before { content: ''; position: absolute; height: 18px; width: 18px; left: 3px; bottom: 3px; background: var(--bg); border-radius: 50%; transition: 0.2s; }
55
- .toggle input:checked + .toggle-slider { background: var(--accent); }
56
- .toggle input:checked + .toggle-slider:before { transform: translateX(18px); }
57
-
58
- .session { display: flex; align-items: center; gap: 12px; padding: 14px 20px; border-bottom: 1px solid var(--border); }
59
- .session:last-child { border-bottom: none; }
60
- .session-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; }
61
- .session-body { flex: 1; }
62
- .session-device { font-size: 14px; font-weight: 600; display: flex; align-items: center; gap: 6px; }
63
- .session-meta { font-size: 12px; color: var(--text-secondary); margin-top: 1px; }
64
- .badge-current { font-size: 11px; background: #dcfce7; color: #16a34a; padding: 1px 8px; border-radius: 9999px; font-weight: 600; }
65
- .btn-revoke { padding: 4px 12px; border-radius: 6px; border: 1px solid #e53935; background: none; color: #e53935; font-size: 12px; font-weight: 600; cursor: pointer; }
66
- .btn-revoke:hover { background: #fef2f2; }
67
- .btn-revoke-all { padding: 8px 20px; border-radius: 6px; border: 1px solid #e53935; background: none; color: #e53935; font-size: 13px; font-weight: 600; cursor: pointer; margin: 12px 20px 16px; }
68
- .btn-revoke-all:hover { background: #fef2f2; }
69
- `;
70
-
71
- render() {
72
- const { passwordLastChanged, sessions = [] } = this.loaderData;
73
- return html`
74
- <div class="card">
75
- <div class="header">
76
- <a class="back" href="/settings">${svg.back}</a>
77
- <h2>Password and security</h2>
78
- </div>
79
-
80
- <div class="section-title">Password</div>
81
- <div class="row">
82
- <div class="row-icon">${svg.lock}</div>
83
- <div class="row-body">
84
- <div class="row-label">Change password</div>
85
- <div class="row-desc">Last changed ${passwordLastChanged}</div>
86
- </div>
87
- <button class="row-btn">Update</button>
88
- </div>
89
- </div>
90
-
91
- <div class="card">
92
- <div class="section-title">Two-factor authentication</div>
93
- <div class="row">
94
- <div class="row-icon">${svg.shield}</div>
95
- <div class="row-body">
96
- <div class="row-label">Authenticator app</div>
97
- <div class="row-desc">${this._2fa ? 'Enabled — your account has extra protection' : 'Add an extra layer of security to your account'}</div>
98
- </div>
99
- <label class="toggle">
100
- <input type="checkbox" .checked=${this._2fa} @change=${(e: Event) => { this._2fa = (e.target as HTMLInputElement).checked; }}>
101
- <span class="toggle-slider"></span>
102
- </label>
103
- </div>
104
- <div class="row">
105
- <div class="row-icon">${svg.smartphone}</div>
106
- <div class="row-body">
107
- <div class="row-label">SMS backup</div>
108
- <div class="row-desc">Use your phone number as a backup verification method</div>
109
- </div>
110
- <button class="row-btn">Set up</button>
111
- </div>
112
- </div>
113
-
114
- <div class="card">
115
- <div class="section-title">Active sessions</div>
116
- ${sessions.map((s: any) => html`
117
- <div class="session">
118
- <div class="session-icon">${s.type === 'mobile' ? svg.smartphone : svg.monitor}</div>
119
- <div class="session-body">
120
- <div class="session-device">
121
- ${s.device}
122
- ${s.current ? html`<span class="badge-current">${svg.check} Current</span>` : ''}
123
- </div>
124
- <div class="session-meta">${s.location} &middot; ${s.lastActive}</div>
125
- </div>
126
- ${!s.current ? html`<button class="btn-revoke">Revoke</button>` : ''}
127
- </div>
128
- `)}
129
- <button class="btn-revoke-all">Revoke all other sessions</button>
130
- </div>
131
- `;
132
- }
133
- }