includio-cms 0.0.68 → 0.1.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 (55) hide show
  1. package/CHANGELOG.md +181 -0
  2. package/ROADMAP.md +92 -0
  3. package/dist/admin/client/account/lang.d.ts +0 -5
  4. package/dist/admin/client/account/lang.js +2 -12
  5. package/dist/admin/client/account/preferences-section.svelte +27 -24
  6. package/dist/admin/client/account/profile-section.svelte +11 -15
  7. package/dist/admin/client/account/security-section.svelte +0 -11
  8. package/dist/admin/client/account/sessions-section.svelte +2 -1
  9. package/dist/admin/client/collection/collection-entries.svelte +168 -43
  10. package/dist/admin/client/collection/collection-view.svelte.d.ts +5 -0
  11. package/dist/admin/client/collection/collection-view.svelte.js +22 -5
  12. package/dist/admin/client/collection/collection.svelte +2 -2
  13. package/dist/admin/client/collection/data-table.svelte +15 -3
  14. package/dist/admin/client/collection/data-table.svelte.d.ts +3 -0
  15. package/dist/admin/client/entry/entry.svelte +11 -7
  16. package/dist/admin/client/entry/header/status-badge.svelte +6 -3
  17. package/dist/admin/client/entry/header/version-history-sheet.svelte +6 -3
  18. package/dist/admin/client/form/form-submissions.svelte +3 -26
  19. package/dist/admin/components/dashboard/form-submissions-widget.svelte +2 -12
  20. package/dist/admin/components/dashboard/recent-activity.svelte +2 -12
  21. package/dist/admin/components/fields/array-field.svelte +126 -71
  22. package/dist/admin/components/fields/relation-field.svelte +6 -10
  23. package/dist/admin/components/layout/header-actions.svelte +4 -3
  24. package/dist/admin/components/layout/nav-search.svelte +43 -31
  25. package/dist/admin/components/media/media-library.svelte +17 -6
  26. package/dist/admin/remote/entry.remote.d.ts +10 -1
  27. package/dist/admin/remote/entry.remote.js +16 -4
  28. package/dist/admin/state/interface-language.svelte.d.ts +4 -7
  29. package/dist/admin/state/interface-language.svelte.js +19 -18
  30. package/dist/admin/utils/arrayMove.d.ts +5 -0
  31. package/dist/admin/utils/arrayMove.js +12 -0
  32. package/dist/admin/utils/formatDate.d.ts +1 -0
  33. package/dist/admin/utils/formatDate.js +5 -1
  34. package/dist/components/ui/input-group/input-group-input.svelte.d.ts +1 -1
  35. package/dist/components/ui/sidebar/sidebar-input.svelte.d.ts +1 -1
  36. package/dist/core/server/entries/operations/get.d.ts +2 -0
  37. package/dist/core/server/entries/operations/get.js +6 -0
  38. package/dist/db-postgres/index.js +26 -1
  39. package/dist/sveltekit/components/hybrid-context.d.ts +4 -0
  40. package/dist/sveltekit/components/hybrid-context.js +9 -0
  41. package/dist/sveltekit/components/hybrid-target.svelte +4 -1
  42. package/dist/sveltekit/components/image.svelte +5 -2
  43. package/dist/sveltekit/components/preview.svelte +3 -0
  44. package/dist/sveltekit/components/video.svelte +4 -1
  45. package/dist/sveltekit/index.d.ts +1 -0
  46. package/dist/sveltekit/index.js +1 -0
  47. package/dist/types/adapters/db.d.ts +3 -1
  48. package/dist/types/entries.d.ts +10 -2
  49. package/dist/types/languages.d.ts +2 -1
  50. package/dist/updates/0.0.69/index.d.ts +2 -0
  51. package/dist/updates/0.0.69/index.js +12 -0
  52. package/dist/updates/0.1.0/index.d.ts +2 -0
  53. package/dist/updates/0.1.0/index.js +25 -0
  54. package/dist/updates/index.js +3 -1
  55. package/package.json +7 -2
package/CHANGELOG.md ADDED
@@ -0,0 +1,181 @@
1
+ # Changelog
2
+
3
+ All notable changes to includio-cms are documented here.
4
+ Generated from `src/lib/updates/` — do not edit manually.
5
+
6
+ ## 0.1.0 — 2026-02-17
7
+
8
+ Stabilization — pagination, language switcher, and more
9
+
10
+ ### Added
11
+ - Server-side pagination for collection entries (limit/offset/orderBy)
12
+ - Persistent page index and active tab across navigation
13
+ - countEntries adapter method for total count queries
14
+ - Automatic fallback to client-side pagination for search and name sorting
15
+
16
+ ### Fixed
17
+ - Language switcher now reactive globally — all UI updates instantly without reload
18
+ - Removed hardcoded en/pl — locale list derived from Paraglide config
19
+ - Browser language auto-detected as default interface language
20
+ - Deduplicated formatRelativeDate across dashboard and form components
21
+ - Account profile: fixed name input not responding to typing
22
+ - Account preferences: added aria-pressed to theme/language toggles
23
+ - Removed placeholder 2FA stub from security section
24
+ - Media tag counts now display correct values — TagSidebar and BulkActionBar receive all files
25
+ - Hybrid target: data-hybrid-path no longer rendered on public pages — only in preview iframe
26
+
27
+ ### Breaking
28
+ - getRawEntries remote now returns { entries: RawEntry[], total: number } instead of RawEntry[]
29
+
30
+ ## 0.0.69 — 2026-02-17
31
+
32
+ DnD array reordering, nav-search batch fetch
33
+
34
+ ### Added
35
+ - Drag-and-drop reordering in array fields with arrayMove utility
36
+
37
+ ### Fixed
38
+ - Nav-search: batch-fetch data before rendering Command dialog — fixes missing data on open
39
+
40
+ ## 0.0.68 — 2026-02-11
41
+
42
+ Fix EXIF/blur/focal bugs, expand type exports, add utility functions
43
+
44
+ ### Added
45
+ - Export Entry, EntryType, CMSConfig, CollectionConfig, SingleConfig, FormConfig, FormSubmission, Language, Localized, MediaFileType, ImageStyle, MediaTag from includio-cms/types
46
+ - Add getLink() utility for resolving URL field data to strings
47
+ - Add isImageFieldData() / isVideoFieldData() type guards for media fields
48
+
49
+ ### Fixed
50
+ - Blur placeholder (LQIP) now removed after image loads — fixes visible blur behind transparent PNGs
51
+ - Focal point change now awaits style regeneration before returning — fixes stale/missing styles after save
52
+ - Explicit .rotate() in sharp pipeline — fixes upside-down WebP/JPEG when source has EXIF orientation
53
+ - Use oriented dimensions (post-EXIF) for focal crop calculation — fixes wrong crop on rotated images
54
+ - Codegen: media field type now generates ImageFieldData | VideoFieldData instead of any
55
+ - Codegen: hyphenated collection slugs now produce correct PascalCase (article-category → ArticleCategory)
56
+ - Codegen: generated interfaces now include id and slug fields
57
+ - Preview deep merge now preserves resolved references without url (e.g. category relations)
58
+
59
+ ## 0.0.67 — 2026-02-11
60
+
61
+ Image quality, LQIP, focal point, responsive srcset, auto-downscale
62
+
63
+ ### Added
64
+ - Image styles: configurable quality parameter for format output
65
+ - LQIP: auto-generated blur placeholder (base64 WebP) on image upload
66
+ - LQIP: lazy backfill for existing images without blur placeholder
67
+ - Image component: LQIP blur placeholder via background-image on img element
68
+ - Focal point picker in media file details
69
+ - Smart crop: styles with crop=true use focal point to calculate crop region
70
+ - Style cache invalidation on focal point change
71
+ - Default WebP/AVIF styles auto-generate 640/1024/1920w srcset variants
72
+ - Custom styles support srcset[] and sizes config
73
+ - Image component renders srcset + sizes on <source> elements
74
+ - Image component: sizes prop to override style-level sizes on <source> elements
75
+ - Skips srcset widths larger than original image
76
+ - CMSConfig: media.maxOriginalWidth / maxOriginalHeight settings
77
+ - Auto-downscale oversized images on upload (fit: inside)
78
+ - SVG and non-image files are not affected
79
+ - Auto-format expansion: styles without explicit format auto-expand to AVIF/WebP/original
80
+ - Eager background generation of default image styles on upload/replace/focal-point change
81
+
82
+ ### Fixed
83
+ - Case-insensitive SVG extension check in upload/downscale/LQIP
84
+ - Quality value clamped to 1-100 in generateImageStyle
85
+ - Silent errors replaced with console.warn for LQIP/downscale/file cleanup
86
+ - Remove hardcoded sizes: 100vw from default image styles
87
+ - Lazy load skeleton + error handling for hybrid layout imports
88
+ - AVIF <source> rendered before WebP for correct browser priority
89
+ - Cache-busting suffix in generated style filenames prevents stale CDN/browser cache
90
+
91
+ ### Breaking
92
+ - getImageStyles() now returns { styles, blurDataUrl } instead of plain styles record
93
+
94
+ ### Migration
95
+
96
+ ```sql
97
+ ALTER TABLE image_styles ADD COLUMN IF NOT EXISTS quality INTEGER;
98
+ ALTER TABLE media_file ADD COLUMN IF NOT EXISTS blur_data_url TEXT;
99
+ ALTER TABLE media_file ADD COLUMN IF NOT EXISTS focal_x REAL;
100
+ ALTER TABLE media_file ADD COLUMN IF NOT EXISTS focal_y REAL;
101
+ ```
102
+
103
+ ## 0.0.66 — 2025-02-10
104
+
105
+ Fix URL field validation for linked entries
106
+
107
+ ### Fixed
108
+ - URL field: linked entries no longer require manual URL per language to save
109
+
110
+ ## 0.0.65 — 2025-02-09
111
+
112
+ Move publish logic to entry table, replace folder-based media with tags
113
+
114
+ ### Added
115
+ - Account settings page with session management
116
+ - Redesigned sidebar with collapsible groups
117
+ - Tag-based media library replacing folder structure
118
+ - Unified UI components and styling
119
+
120
+ ### Breaking
121
+ - Media folders replaced with tags — existing folder assignments will be lost
122
+
123
+ ### Migration
124
+
125
+ ```sql
126
+ -- Move publish logic from entry_version to entry
127
+
128
+ ALTER TABLE entry ADD COLUMN published_at TIMESTAMP;
129
+ ALTER TABLE entry ADD COLUMN published_version_id UUID
130
+ REFERENCES entry_version(id) ON DELETE SET NULL;
131
+ ALTER TABLE entry ADD COLUMN published_by TEXT;
132
+
133
+ UPDATE entry e SET
134
+ published_version_id = latest.id,
135
+ published_at = first_pub.first_published_at,
136
+ published_by = latest.published_by
137
+ FROM (
138
+ SELECT DISTINCT ON (entry_id) id, entry_id, published_by
139
+ FROM entry_version
140
+ WHERE published_at IS NOT NULL AND published_at <= NOW()
141
+ ORDER BY entry_id, version_number DESC
142
+ ) latest
143
+ JOIN (
144
+ SELECT entry_id, MIN(published_at) as first_published_at
145
+ FROM entry_version
146
+ WHERE published_at IS NOT NULL AND published_at <= NOW()
147
+ GROUP BY entry_id
148
+ ) first_pub ON first_pub.entry_id = latest.entry_id
149
+ WHERE e.id = latest.entry_id;
150
+
151
+ UPDATE entry e SET
152
+ published_version_id = COALESCE(e.published_version_id, sub.id),
153
+ published_at = CASE WHEN e.published_at IS NULL THEN sub.published_at ELSE e.published_at END,
154
+ published_by = COALESCE(e.published_by, sub.published_by)
155
+ FROM (
156
+ SELECT DISTINCT ON (entry_id) id, entry_id, published_at, published_by
157
+ FROM entry_version
158
+ WHERE published_at IS NOT NULL AND published_at > NOW()
159
+ ORDER BY entry_id, published_at ASC
160
+ ) sub
161
+ WHERE e.id = sub.entry_id AND e.published_version_id IS NULL;
162
+
163
+ -- Replace folder-based media with tag-based media
164
+
165
+ CREATE TABLE IF NOT EXISTS media_tag (
166
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
167
+ name TEXT NOT NULL UNIQUE,
168
+ color TEXT NOT NULL DEFAULT '#3b82f6',
169
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL
170
+ );
171
+
172
+ CREATE TABLE IF NOT EXISTS media_file_tag (
173
+ file_id UUID NOT NULL REFERENCES media_file(id) ON DELETE CASCADE,
174
+ tag_id UUID NOT NULL REFERENCES media_tag(id) ON DELETE CASCADE,
175
+ PRIMARY KEY (file_id, tag_id)
176
+ );
177
+
178
+ ALTER TABLE media_file DROP COLUMN IF EXISTS folder_id;
179
+
180
+ DROP TABLE IF EXISTS media_folder;
181
+ ```
package/ROADMAP.md ADDED
@@ -0,0 +1,92 @@
1
+ # Roadmap
2
+
3
+ > `- [ ]` planned | `- [~]` in-progress | `- [x]` done
4
+ > `[feature]` `[fix]` `[breaking]` `[chore]`
5
+ > `[P0]` critical | `[P1]` important | `[P2]` nice-to-have
6
+ > `<!-- files: path/to/file.ts -->` optionally linked files
7
+ >
8
+ > **Versioning:** 0.0.69 is the last `0.0.x` release. From 0.1.0 onward: `0.MINOR.0` = features/changes, `0.MINOR.PATCH` = fixes.
9
+
10
+ ## 0.0.69 _(last 0.0.x)_
11
+
12
+ - [x] `[feature]` `[P1]` DnD reordering in array fields + arrayMove utility <!-- files: src/cms/fields/array-field -->
13
+ - [x] `[fix]` `[P1]` Nav-search: batch-fetch data before rendering Command dialog <!-- files: src/cms/nav-search -->
14
+
15
+ ## 0.1.0 — Stabilization
16
+
17
+ - [x] `[fix]` `[P0]` Collection table pagination — server-side, persistent page state, archived tab <!-- files: src/lib/admin/client/collection/collection-entries.svelte, src/lib/admin/client/collection/table-pagination.svelte -->
18
+ - [x] `[fix]` `[P0]` Language switcher — reactive globally without reload, remove hardcoded en/pl <!-- files: src/lib/admin/state/interface-language.svelte.ts, src/lib/admin/components/layout/header-actions.svelte -->
19
+ - [x] `[fix]` `[P0]` User account section — fix name input, aria-pressed on prefs, clean up 2FA stub (email change deferred to 0.1.2, avatar upload skipped) <!-- files: src/lib/admin/client/account/ -->
20
+ - [x] `[fix]` `[P0]` Media tag counts always 0 — pass actual files to TagSidebar <!-- files: src/lib/admin/components/media/media-library.svelte, src/lib/admin/components/media/tag-sidebar.svelte -->
21
+ - [x] `[fix]` `[P0]` Hybrid target — don't render `data-hybrid-path` for non-logged-in users <!-- files: src/lib/sveltekit/components/hybrid-target.svelte -->
22
+
23
+ ## 0.1.1 — Input integrity
24
+
25
+ - [ ] `[fix]` `[P1]` Input constraints UI — HTML maxlength, character counter, pattern feedback <!-- files: src/lib/admin/components/fields/text-field.svelte -->
26
+ - [ ] `[fix]` `[P1]` Array field maxItems — disable Add button when max reached <!-- files: src/lib/admin/components/fields/array-field.svelte -->
27
+ - [ ] `[feature]` `[P1]` Array field fixed length — fixed item count, no add/remove, reorder only
28
+ - [ ] `[feature]` `[P1]` Field constraint info display — show constraints before validation error (WCAG/ATAG)
29
+
30
+ ## 0.1.2 — User management & RBAC
31
+
32
+ ### Phase 1 — Core
33
+
34
+ - [ ] `[feature]` `[P0]` RBAC middleware — `requireRole()`, role check w `requireAuth()` <!-- files: src/lib/admin/remote/middleware/auth.ts -->
35
+ - [ ] `[feature]` `[P0]` Admin users page — list, search, pagination via `authClient.admin.listUsers` <!-- files: src/lib/admin/client/users/ -->
36
+ - [ ] `[feature]` `[P0]` Create user — dialog z email/password/name/role via `authClient.admin.createUser` <!-- files: src/lib/admin/client/users/create-user-dialog.svelte -->
37
+ - [ ] `[feature]` `[P0]` Edit user — name, email, role via `adminUpdateUser` + `setRole` <!-- files: src/lib/admin/client/users/edit-user-dialog.svelte -->
38
+ - [ ] `[feature]` `[P0]` Delete user — confirmation dialog via `removeUser` <!-- files: src/lib/admin/client/users/ -->
39
+ - [ ] `[feature]` `[P0]` Route/sidebar gating — ukryj Users nav + chroń `/admin/users` dla non-admin <!-- files: src/lib/admin/components/layout/nav-main.svelte, src/lib/sveltekit/server/handle.ts -->
40
+ - [ ] `[feature]` `[P0]` First user bootstrap — pierwszy utworzony user auto-gets role `admin` <!-- files: src/lib/server/auth.ts -->
41
+
42
+ ### Phase 2 — Extended
43
+
44
+ - [ ] `[feature]` `[P1]` Ban/unban UI — reason + expiry, `banUser`/`unbanUser` <!-- files: src/lib/admin/client/users/ban-user-dialog.svelte -->
45
+ - [ ] `[feature]` `[P1]` Admin session mgmt — list/revoke sesji innych userów <!-- files: src/lib/admin/client/users/user-sessions-sheet.svelte -->
46
+ - [ ] `[feature]` `[P1]` Impersonation UI — impersonate/stop bar <!-- files: src/lib/admin/client/users/impersonation-bar.svelte -->
47
+ - [ ] `[feature]` `[P2]` Email invitation system — invite link generation (custom, nie w better-auth) <!-- files: src/lib/core/server/auth/invite.ts -->
48
+
49
+ ## 0.2.0 — Plugin system
50
+
51
+ - [ ] `[feature]` `[P0]` Wire plugin hooks into CRUD operations (before/afterCreate, Update, Delete) <!-- files: src/lib/types/plugins.ts, src/lib/core/server/entries/operations/ -->
52
+ - [ ] `[feature]` `[P0]` Plugin registration API — public surface for external plugins
53
+ - [ ] `[chore]` `[P1]` Plugin system documentation
54
+
55
+ ## 0.2.1 — CMS API improvements
56
+
57
+ - [ ] `[feature]` `[P1]` Server-side pagination API (formalize 0.1.0 fix)
58
+ - [ ] `[feature]` `[P1]` Improved type generation <!-- files: src/lib/core/server/generator/ -->
59
+ - [ ] `[feature]` `[P1]` Proper filtering API (SQL-level, not JS post-query)
60
+
61
+ ## 0.3.0 — Admin experience
62
+
63
+ - [ ] `[feature]` `[P1]` Admin overlay — built-in admin bar on site (quick edit, preview, audit)
64
+ - [ ] `[feature]` `[P1]` Media gallery virtualization/pagination <!-- files: src/lib/admin/components/media/files-list.svelte -->
65
+ - [ ] `[feature]` `[P2]` Image styles UI — preview generated variants in editor
66
+
67
+ ## 0.3.1 — SEO module
68
+
69
+ - [ ] `[feature]` `[P1]` SERP preview + character limits for title/description <!-- files: src/lib/admin/components/fields/seo-field.svelte -->
70
+ - [ ] `[feature]` `[P1]` Global SEO settings
71
+ - [ ] `[feature]` `[P1]` Dedicated frontend SEO components <!-- files: src/lib/sveltekit/components/seo.svelte -->
72
+ - [ ] `[feature]` `[P2]` Sitemap generation
73
+
74
+ ## 0.4.0 — WCAG/ATAG compliance
75
+
76
+ - [ ] `[chore]` `[P0]` Full WCAG/ATAG audit
77
+ - [ ] `[feature]` `[P0]` Accessibility rework based on audit findings
78
+
79
+ ## Security hardening
80
+
81
+ - [ ] `[feature]` `[P1]` `sanitizeHTML` utility — general HTML sanitization outside richtext (text fields, SEO fields)
82
+ - [ ] `[feature]` `[P1]` CSP headers — Content-Security-Policy middleware
83
+ - [ ] `[feature]` `[P1]` CSRF protection — tokens for mutating operations
84
+ - [ ] `[feature]` `[P1]` Rate limiting — API/auth endpoints
85
+ - [ ] `[chore]` `[P1]` Security audit — review `{@html}` usage, innerHTML, injection vectors
86
+ - [ ] `[chore]` `[P1]` Input sanitization audit — review all fields for XSS
87
+
88
+ ## Backlog
89
+
90
+ - [ ] `[feature]` `[P2]` Alternative richtext editor — Word-like mode, single richtext field instead of blocks
91
+ - [ ] `[chore]` `[P2]` Caching/performance layer (scope TBD)
92
+ - [ ] `[feature]` `[P2]` API/CLI for configuration (setup DX for less technical users)
@@ -23,9 +23,6 @@ export declare const accountLang: Record<string, {
23
23
  confirmPassword: string;
24
24
  updatePassword: string;
25
25
  passwordUpdated: string;
26
- twoFactor: string;
27
- twoFactorDesc: string;
28
- comingSoon: string;
29
26
  deleteAccount: string;
30
27
  deleteAccountDesc: string;
31
28
  deleteAccountButton: string;
@@ -42,8 +39,6 @@ export declare const accountLang: Record<string, {
42
39
  themeDark: string;
43
40
  themeSystem: string;
44
41
  language: string;
45
- languageEn: string;
46
- languagePl: string;
47
42
  };
48
43
  sessions: {
49
44
  title: string;
@@ -24,9 +24,6 @@ export const accountLang = {
24
24
  confirmPassword: 'Potwierdź hasło',
25
25
  updatePassword: 'Zmień hasło',
26
26
  passwordUpdated: 'Hasło zostało zmienione',
27
- twoFactor: 'Uwierzytelnianie dwuskładnikowe',
28
- twoFactorDesc: 'Dodatkowa warstwa zabezpieczeń dla Twojego konta',
29
- comingSoon: 'Wkrótce',
30
27
  deleteAccount: 'Usuń konto',
31
28
  deleteAccountDesc: 'Trwale usuń swoje konto i wszystkie dane',
32
29
  deleteAccountButton: 'Usuń konto',
@@ -42,9 +39,7 @@ export const accountLang = {
42
39
  themeLight: 'Jasny',
43
40
  themeDark: 'Ciemny',
44
41
  themeSystem: 'Systemowy',
45
- language: 'Język',
46
- languageEn: 'Angielski',
47
- languagePl: 'Polski'
42
+ language: 'Język'
48
43
  },
49
44
  sessions: {
50
45
  title: 'Aktywne sesje',
@@ -88,9 +83,6 @@ export const accountLang = {
88
83
  confirmPassword: 'Confirm password',
89
84
  updatePassword: 'Update password',
90
85
  passwordUpdated: 'Password updated',
91
- twoFactor: 'Two-factor authentication',
92
- twoFactorDesc: 'Add an extra layer of security to your account',
93
- comingSoon: 'Coming soon',
94
86
  deleteAccount: 'Delete account',
95
87
  deleteAccountDesc: 'Permanently delete your account and all data',
96
88
  deleteAccountButton: 'Delete account',
@@ -106,9 +98,7 @@ export const accountLang = {
106
98
  themeLight: 'Light',
107
99
  themeDark: 'Dark',
108
100
  themeSystem: 'System',
109
- language: 'Language',
110
- languageEn: 'English',
111
- languagePl: 'Polish'
101
+ language: 'Language'
112
102
  },
113
103
  sessions: {
114
104
  title: 'Active sessions',
@@ -1,11 +1,12 @@
1
1
  <script lang="ts">
2
2
  import * as Card from '../../../components/ui/card/index.js';
3
3
  import { accountLang } from './lang.js';
4
- import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
4
+ import { useInterfaceLanguage, locales } from '../../state/interface-language.svelte.js';
5
5
  import { userPrefersMode, setMode } from 'mode-watcher';
6
6
  import Sun from '@tabler/icons-svelte/icons/sun';
7
7
  import Moon from '@tabler/icons-svelte/icons/moon';
8
8
  import DeviceDesktop from '@tabler/icons-svelte/icons/device-desktop';
9
+ import type { InterfaceLanguage } from '../../../types/languages.js';
9
10
 
10
11
  const interfaceLanguage = useInterfaceLanguage();
11
12
 
@@ -15,9 +16,15 @@
15
16
  setMode(theme);
16
17
  }
17
18
 
18
- function setLanguage(language: 'en' | 'pl') {
19
+ function setLanguage(language: InterfaceLanguage) {
19
20
  interfaceLanguage.current = language;
20
21
  }
22
+
23
+ function getLanguageDisplayName(locale: string): string {
24
+ const name = new Intl.DisplayNames([interfaceLanguage.current], { type: 'language' }).of(locale);
25
+ if (!name) return locale.toUpperCase();
26
+ return name.charAt(0).toUpperCase() + name.slice(1);
27
+ }
21
28
  </script>
22
29
 
23
30
  <div class="space-y-6">
@@ -26,9 +33,10 @@
26
33
  <Card.Title>{lang.preferences.theme}</Card.Title>
27
34
  </Card.Header>
28
35
  <Card.Content>
29
- <div class="flex gap-2">
36
+ <div class="flex gap-2" role="group" aria-label={lang.preferences.theme}>
30
37
  <button
31
38
  type="button"
39
+ aria-pressed={userPrefersMode.current === 'light'}
32
40
  onclick={() => setTheme('light')}
33
41
  class="inline-flex h-10 items-center justify-center gap-2 rounded-md border px-4 py-2 text-sm font-medium transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none {userPrefersMode.current ===
34
42
  'light'
@@ -40,6 +48,7 @@
40
48
  </button>
41
49
  <button
42
50
  type="button"
51
+ aria-pressed={userPrefersMode.current === 'dark'}
43
52
  onclick={() => setTheme('dark')}
44
53
  class="inline-flex h-10 items-center justify-center gap-2 rounded-md border px-4 py-2 text-sm font-medium transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none {userPrefersMode.current ===
45
54
  'dark'
@@ -51,6 +60,7 @@
51
60
  </button>
52
61
  <button
53
62
  type="button"
63
+ aria-pressed={userPrefersMode.current === 'system'}
54
64
  onclick={() => setTheme('system')}
55
65
  class="inline-flex h-10 items-center justify-center gap-2 rounded-md border px-4 py-2 text-sm font-medium transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none {userPrefersMode.current ===
56
66
  'system'
@@ -69,27 +79,20 @@
69
79
  <Card.Title>{lang.preferences.language}</Card.Title>
70
80
  </Card.Header>
71
81
  <Card.Content>
72
- <div class="flex gap-2">
73
- <button
74
- type="button"
75
- onclick={() => setLanguage('en')}
76
- class="inline-flex h-10 items-center justify-center gap-2 rounded-md border px-4 py-2 text-sm font-medium transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none {interfaceLanguage.current ===
77
- 'en'
78
- ? 'border-[#2D4A77] bg-[#2D4A77]/10 text-[#2D4A77]'
79
- : 'border-input hover:bg-accent'}"
80
- >
81
- {lang.preferences.languageEn}
82
- </button>
83
- <button
84
- type="button"
85
- onclick={() => setLanguage('pl')}
86
- class="inline-flex h-10 items-center justify-center gap-2 rounded-md border px-4 py-2 text-sm font-medium transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none {interfaceLanguage.current ===
87
- 'pl'
88
- ? 'border-[#2D4A77] bg-[#2D4A77]/10 text-[#2D4A77]'
89
- : 'border-input hover:bg-accent'}"
90
- >
91
- {lang.preferences.languagePl}
92
- </button>
82
+ <div class="flex gap-2" role="group" aria-label={lang.preferences.language}>
83
+ {#each locales as locale}
84
+ <button
85
+ type="button"
86
+ aria-pressed={interfaceLanguage.current === locale}
87
+ onclick={() => setLanguage(locale)}
88
+ class="inline-flex h-10 items-center justify-center gap-2 rounded-md border px-4 py-2 text-sm font-medium transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none {interfaceLanguage.current ===
89
+ locale
90
+ ? 'border-[#2D4A77] bg-[#2D4A77]/10 text-[#2D4A77]'
91
+ : 'border-input hover:bg-accent'}"
92
+ >
93
+ {getLanguageDisplayName(locale)}
94
+ </button>
95
+ {/each}
93
96
  </div>
94
97
  </Card.Content>
95
98
  </Card.Root>
@@ -9,21 +9,27 @@
9
9
  import { profileSchema } from './schema.js';
10
10
  import { accountLang } from './lang.js';
11
11
  import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
12
+ import { toLocaleCode } from '../../utils/formatDate.js';
12
13
  import { authClient } from '../../auth-client.js';
13
14
  import { toast } from 'svelte-sonner';
15
+ import { untrack } from 'svelte';
14
16
 
15
17
  let session = authClient.useSession();
16
18
  const interfaceLanguage = useInterfaceLanguage();
17
19
 
18
20
  $effect(() => {
19
- if ($session.data?.user.name && $formData.name !== $session.data.user.name) {
20
- $formData.name = $session.data.user.name;
21
+ const sessionName = $session.data?.user.name;
22
+ if (sessionName) {
23
+ untrack(() => {
24
+ $formData.name = sessionName;
25
+ });
21
26
  }
22
27
  });
23
28
 
24
29
  const form = superForm(defaults(zod4(profileSchema)), {
25
30
  validators: zod4Client(profileSchema),
26
31
  SPA: true,
32
+ resetForm: false,
27
33
  onUpdate: async ({ form }) => {
28
34
  if (form.valid) {
29
35
  await authClient.updateUser({
@@ -48,7 +54,7 @@
48
54
  }
49
55
 
50
56
  function formatDate(date: Date | string): string {
51
- return new Date(date).toLocaleDateString(interfaceLanguage.current === 'pl' ? 'pl-PL' : 'en-US', {
57
+ return new Date(date).toLocaleDateString(toLocaleCode(interfaceLanguage.current), {
52
58
  year: 'numeric',
53
59
  month: 'long'
54
60
  });
@@ -79,12 +85,7 @@
79
85
  <Form.Control>
80
86
  {#snippet children({ props })}
81
87
  <Form.Label>{lang.profile.name}</Form.Label>
82
- <Input
83
- {...props}
84
- bind:value={$formData.name}
85
- type="text"
86
- class="mt-1"
87
- />
88
+ <Input {...props} bind:value={$formData.name} type="text" class="mt-1" />
88
89
  {/snippet}
89
90
  </Form.Control>
90
91
  <Form.FieldErrors class="text-sm text-red-500" />
@@ -92,12 +93,7 @@
92
93
 
93
94
  <div class="space-y-2">
94
95
  <label class="text-sm font-medium">{lang.profile.email}</label>
95
- <Input
96
- type="email"
97
- value={$session.data.user.email}
98
- disabled
99
- class="mt-1"
100
- />
96
+ <Input type="email" value={$session.data.user.email} disabled class="mt-1" />
101
97
  </div>
102
98
 
103
99
  <div class="flex items-center gap-4">
@@ -4,7 +4,6 @@
4
4
  import Button from '../../../components/ui/button/button.svelte';
5
5
  import * as Card from '../../../components/ui/card/index.js';
6
6
  import * as AlertDialog from '../../../components/ui/alert-dialog/index.js';
7
- import { Badge } from '../../../components/ui/badge/index.js';
8
7
  import { defaults, superForm } from 'sveltekit-superforms';
9
8
  import { zod4, zod4Client } from 'sveltekit-superforms/adapters';
10
9
  import { passwordSchema, deleteAccountSchema } from './schema.js';
@@ -124,16 +123,6 @@
124
123
  </Card.Content>
125
124
  </Card.Root>
126
125
 
127
- <Card.Root>
128
- <Card.Header>
129
- <Card.Title>{lang.security.twoFactor}</Card.Title>
130
- <Card.Description>{lang.security.twoFactorDesc}</Card.Description>
131
- </Card.Header>
132
- <Card.Content>
133
- <Badge variant="outline">{lang.security.comingSoon}</Badge>
134
- </Card.Content>
135
- </Card.Root>
136
-
137
126
  <Card.Root class="border-destructive/50">
138
127
  <Card.Header>
139
128
  <Card.Title class="text-destructive">{lang.security.deleteAccount}</Card.Title>
@@ -3,6 +3,7 @@
3
3
  import { Badge } from '../../../components/ui/badge/index.js';
4
4
  import { accountLang } from './lang.js';
5
5
  import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
6
+ import { toLocaleCode } from '../../utils/formatDate.js';
6
7
  import { authClient } from '../../auth-client.js';
7
8
  import { toast } from 'svelte-sonner';
8
9
  import DeviceDesktop from '@tabler/icons-svelte/icons/device-desktop';
@@ -86,7 +87,7 @@
86
87
  }
87
88
 
88
89
  function formatDate(date: Date | string): string {
89
- return new Date(date).toLocaleString(interfaceLanguage.current === 'pl' ? 'pl-PL' : 'en-US', {
90
+ return new Date(date).toLocaleString(toLocaleCode(interfaceLanguage.current), {
90
91
  year: 'numeric',
91
92
  month: 'short',
92
93
  day: 'numeric',