includio-cms 0.1.4 → 0.5.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 (296) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/ROADMAP.md +18 -13
  3. package/dist/admin/api/accept-invite.js +1 -5
  4. package/dist/admin/api/invite.js +7 -16
  5. package/dist/admin/client/account/account-page.svelte +20 -50
  6. package/dist/admin/client/account/lang.d.ts +15 -23
  7. package/dist/admin/client/account/lang.js +51 -67
  8. package/dist/admin/client/account/preferences-section.svelte +26 -84
  9. package/dist/admin/client/account/profile-section.svelte +60 -40
  10. package/dist/admin/client/account/schema.d.ts +11 -3
  11. package/dist/admin/client/account/schema.js +25 -16
  12. package/dist/admin/client/account/security-section.svelte +139 -105
  13. package/dist/admin/client/account/sessions-section.svelte +35 -34
  14. package/dist/admin/client/admin/admin-after-login-layout-content.svelte +3 -5
  15. package/dist/admin/client/admin/admin-layout.svelte +3 -2
  16. package/dist/admin/client/admin/admin-preloader.svelte +36 -0
  17. package/dist/admin/client/admin/admin-preloader.svelte.d.ts +18 -0
  18. package/dist/admin/client/admin/dashboard-page.svelte +55 -41
  19. package/dist/admin/client/collection/a11y-score-cell.svelte +45 -0
  20. package/dist/admin/client/collection/a11y-score-cell.svelte.d.ts +6 -0
  21. package/dist/admin/client/collection/bulk-actions-bar.svelte +83 -0
  22. package/dist/admin/client/collection/bulk-actions-bar.svelte.d.ts +9 -0
  23. package/dist/admin/client/collection/collection-entries.svelte +255 -260
  24. package/dist/admin/client/collection/collection-view.svelte.d.ts +4 -3
  25. package/dist/admin/client/collection/collection-view.svelte.js +9 -5
  26. package/dist/admin/client/collection/collection.svelte +22 -12
  27. package/dist/admin/client/collection/data-table.svelte +50 -39
  28. package/dist/admin/client/collection/data-table.svelte.d.ts +1 -0
  29. package/dist/admin/client/collection/date-cell.svelte +7 -5
  30. package/dist/admin/client/collection/date-cell.svelte.d.ts +1 -1
  31. package/dist/admin/client/collection/empty-state.svelte +28 -0
  32. package/dist/admin/client/collection/empty-state.svelte.d.ts +9 -0
  33. package/dist/admin/client/collection/entry-link.svelte +10 -4
  34. package/dist/admin/client/collection/entry-link.svelte.d.ts +1 -0
  35. package/dist/admin/client/collection/grid-view.svelte +21 -23
  36. package/dist/admin/client/collection/grid-view.svelte.d.ts +1 -2
  37. package/dist/admin/client/collection/row-actions.svelte +60 -0
  38. package/dist/admin/client/collection/row-actions.svelte.d.ts +9 -0
  39. package/dist/admin/client/collection/status-badge.svelte +7 -8
  40. package/dist/admin/client/collection/table-pagination.svelte +122 -79
  41. package/dist/admin/client/collection/table-pagination.svelte.d.ts +1 -0
  42. package/dist/admin/client/collection/table-toolbar.svelte +108 -88
  43. package/dist/admin/client/collection/table-toolbar.svelte.d.ts +8 -9
  44. package/dist/admin/client/entry/entry-form.svelte +109 -1
  45. package/dist/admin/client/entry/entry-header.svelte +96 -37
  46. package/dist/admin/client/entry/entry-header.svelte.d.ts +5 -0
  47. package/dist/admin/client/entry/entry.svelte +171 -60
  48. package/dist/admin/client/entry/header/a11y-validator.d.ts +46 -0
  49. package/dist/admin/client/entry/header/a11y-validator.js +311 -0
  50. package/dist/admin/client/entry/header/publish-panel.svelte +373 -131
  51. package/dist/admin/client/entry/header/publish-panel.svelte.d.ts +4 -0
  52. package/dist/admin/client/entry/header/save-indicator.svelte +33 -23
  53. package/dist/admin/client/entry/header/schedule-popover.svelte +1 -1
  54. package/dist/admin/client/entry/header/status-badge.svelte +25 -118
  55. package/dist/admin/client/entry/header/version-history-sheet.svelte +314 -98
  56. package/dist/admin/client/form/form-submission/form-submission.svelte +271 -83
  57. package/dist/admin/client/form/form-submission/submission-field.svelte +12 -12
  58. package/dist/admin/client/form/form-submissions.svelte +421 -139
  59. package/dist/admin/client/form/submission-link.svelte +8 -2
  60. package/dist/admin/client/form/submission-link.svelte.d.ts +1 -0
  61. package/dist/admin/client/form/submission-status-badge.svelte +18 -4
  62. package/dist/admin/client/form/submission-status-badge.svelte.d.ts +1 -0
  63. package/dist/admin/client/login/lang.d.ts +32 -0
  64. package/dist/admin/client/login/lang.js +66 -2
  65. package/dist/admin/client/login/login-form.svelte +237 -95
  66. package/dist/admin/client/login/login-form.svelte.d.ts +2 -17
  67. package/dist/admin/client/login/login-page.svelte +34 -98
  68. package/dist/admin/client/login/reset-password-page.svelte +235 -0
  69. package/dist/admin/client/login/reset-password-page.svelte.d.ts +4 -0
  70. package/dist/admin/client/login/schema.d.ts +15 -0
  71. package/dist/admin/client/login/schema.js +21 -0
  72. package/dist/admin/client/users/accept-invite-page.svelte +166 -37
  73. package/dist/admin/client/users/create-user-dialog.svelte +15 -7
  74. package/dist/admin/client/users/delete-user-dialog.svelte +81 -16
  75. package/dist/admin/client/users/delete-user-dialog.svelte.d.ts +4 -1
  76. package/dist/admin/client/users/edit-user-dialog.svelte +3 -0
  77. package/dist/admin/client/users/invite-user-dialog.svelte +16 -3
  78. package/dist/admin/client/users/lang.d.ts +27 -0
  79. package/dist/admin/client/users/lang.js +64 -10
  80. package/dist/admin/client/users/pending-invitations.svelte +59 -23
  81. package/dist/admin/client/users/users-page.svelte +471 -72
  82. package/dist/admin/components/accessibility/accessibility-overview.svelte +2 -7
  83. package/dist/admin/components/dashboard/a11y-gauge.svelte +90 -0
  84. package/dist/admin/components/dashboard/a11y-gauge.svelte.d.ts +18 -0
  85. package/dist/admin/components/dashboard/accessibility-hub.svelte +13 -12
  86. package/dist/admin/components/dashboard/form-submissions-widget.svelte +71 -113
  87. package/dist/admin/components/dashboard/index.d.ts +4 -2
  88. package/dist/admin/components/dashboard/index.js +4 -2
  89. package/dist/admin/components/dashboard/recent-activity.svelte +53 -75
  90. package/dist/admin/components/dashboard/recent-entries.svelte +94 -0
  91. package/dist/admin/components/dashboard/recent-entries.svelte.d.ts +18 -0
  92. package/dist/admin/components/dashboard/stat-card.svelte +2 -2
  93. package/dist/admin/components/dashboard/tip-of-the-day.svelte +109 -0
  94. package/dist/admin/components/dashboard/tip-of-the-day.svelte.d.ts +3 -0
  95. package/dist/admin/components/dashboard/welcome-header.svelte +45 -0
  96. package/dist/admin/components/dashboard/welcome-header.svelte.d.ts +3 -0
  97. package/dist/admin/components/fields/{array-field.svelte → blocks-field.svelte} +4 -4
  98. package/dist/admin/components/fields/{array-field.svelte.d.ts → blocks-field.svelte.d.ts} +5 -5
  99. package/dist/admin/components/fields/content-field.svelte +27 -0
  100. package/dist/admin/components/fields/content-field.svelte.d.ts +31 -0
  101. package/dist/admin/components/fields/field-renderer.svelte +9 -7
  102. package/dist/admin/components/fields/image-field.svelte +2 -2
  103. package/dist/admin/components/fields/media-field.svelte +2 -2
  104. package/dist/admin/components/fields/seo-field.svelte +205 -25
  105. package/dist/admin/components/fields/simple-array-field.svelte +289 -0
  106. package/dist/admin/components/fields/simple-array-field.svelte.d.ts +30 -0
  107. package/dist/admin/components/fields/slug-field.svelte +3 -2
  108. package/dist/admin/components/fields/standalone-field-renderer.svelte +148 -0
  109. package/dist/admin/components/fields/standalone-field-renderer.svelte.d.ts +9 -0
  110. package/dist/admin/components/fields/text-field-wrapper.svelte +13 -1
  111. package/dist/admin/components/fields/text-field-wrapper.svelte.d.ts +2 -2
  112. package/dist/admin/components/fields/url-field.svelte +5 -4
  113. package/dist/admin/components/layout/app-sidebar.svelte +27 -24
  114. package/dist/admin/components/layout/lang.d.ts +6 -0
  115. package/dist/admin/components/layout/lang.js +13 -1
  116. package/dist/admin/components/layout/layout-renderer.svelte +352 -0
  117. package/dist/admin/components/layout/layout-renderer.svelte.d.ts +14 -0
  118. package/dist/admin/components/layout/nav-breadcrumbs.svelte +4 -4
  119. package/dist/admin/components/layout/nav-collections.svelte +65 -36
  120. package/dist/admin/components/layout/nav-footer.svelte +31 -0
  121. package/dist/admin/components/layout/nav-footer.svelte.d.ts +18 -0
  122. package/dist/admin/components/layout/nav-forms.svelte +55 -30
  123. package/dist/admin/components/layout/nav-main.svelte +14 -52
  124. package/dist/admin/components/layout/nav-search.svelte +4 -3
  125. package/dist/admin/components/layout/nav-singletons.svelte +59 -17
  126. package/dist/admin/components/layout/nav-singletons.svelte.d.ts +17 -8
  127. package/dist/admin/components/layout/site-header.svelte +74 -13
  128. package/dist/admin/components/media/alt-input.svelte +32 -22
  129. package/dist/admin/components/media/bulk-action-bar.svelte +139 -150
  130. package/dist/admin/components/media/file/file-details.svelte +299 -217
  131. package/dist/admin/components/media/file/file-miniature.svelte +54 -41
  132. package/dist/admin/components/media/file/file-miniature.svelte.d.ts +1 -0
  133. package/dist/admin/components/media/file/file-preview.svelte +1 -1
  134. package/dist/admin/components/media/file-upload.svelte +24 -26
  135. package/dist/admin/components/media/files-list.svelte +112 -40
  136. package/dist/admin/components/media/files-list.svelte.d.ts +2 -0
  137. package/dist/admin/components/media/focal-point-input.svelte +122 -26
  138. package/dist/admin/components/media/media-library.svelte +127 -70
  139. package/dist/admin/components/media/media-search.svelte +6 -6
  140. package/dist/admin/components/media/media-sort.svelte +3 -1
  141. package/dist/admin/components/media/multi-file-summary.svelte +88 -68
  142. package/dist/admin/components/media/tag-combobox.svelte +141 -66
  143. package/dist/admin/components/media/tag-combobox.svelte.d.ts +1 -0
  144. package/dist/admin/components/media/tag-sidebar.svelte +139 -121
  145. package/dist/admin/components/tiptap/FigureNodeView.svelte +144 -15
  146. package/dist/admin/components/tiptap/InlineBlockNodeView.svelte +254 -0
  147. package/dist/admin/components/tiptap/InlineBlockNodeView.svelte.d.ts +4 -0
  148. package/dist/admin/components/tiptap/SlashCommandPopup.svelte +212 -0
  149. package/dist/admin/components/tiptap/SlashCommandPopup.svelte.d.ts +8 -0
  150. package/dist/admin/components/tiptap/content-editor.svelte +280 -0
  151. package/dist/admin/components/tiptap/content-editor.svelte.d.ts +9 -0
  152. package/dist/admin/components/tiptap/editor-toolbar.svelte +230 -0
  153. package/dist/admin/components/tiptap/editor-toolbar.svelte.d.ts +16 -0
  154. package/dist/admin/components/tiptap/heading-a11y-plugin.d.ts +2 -0
  155. package/dist/admin/components/tiptap/heading-a11y-plugin.js +67 -0
  156. package/dist/admin/components/tiptap/image-dialog.svelte +172 -11
  157. package/dist/admin/components/tiptap/inline-block-node.d.ts +19 -0
  158. package/dist/admin/components/tiptap/inline-block-node.js +98 -0
  159. package/dist/admin/components/tiptap/link-dialog.svelte +9 -4
  160. package/dist/admin/components/tiptap/slash-command.d.ts +17 -0
  161. package/dist/admin/components/tiptap/slash-command.js +181 -0
  162. package/dist/admin/components/tiptap/structured-content-utils.d.ts +21 -0
  163. package/dist/admin/components/tiptap/structured-content-utils.js +150 -0
  164. package/dist/admin/components/tiptap/tiptap-editor.svelte +18 -190
  165. package/dist/admin/email/invite-template.d.ts +8 -0
  166. package/dist/admin/email/invite-template.js +99 -0
  167. package/dist/admin/email/reset-password-template.d.ts +7 -0
  168. package/dist/admin/email/reset-password-template.js +96 -0
  169. package/dist/admin/remote/ai.remote.d.ts +1 -0
  170. package/dist/admin/remote/ai.remote.js +4 -1
  171. package/dist/admin/remote/entry.remote.d.ts +8 -0
  172. package/dist/admin/remote/entry.remote.js +53 -4
  173. package/dist/admin/remote/preview.remote.js +2 -1
  174. package/dist/admin/shared/password-schema.d.ts +5 -0
  175. package/dist/admin/shared/password-schema.js +10 -0
  176. package/dist/admin/styles/admin.css +1530 -151
  177. package/dist/admin/utils/formatDate.d.ts +1 -0
  178. package/dist/admin/utils/formatDate.js +8 -0
  179. package/dist/admin/utils/roleLabel.d.ts +2 -0
  180. package/dist/admin/utils/roleLabel.js +13 -0
  181. package/dist/ai-claude/index.d.ts +2 -0
  182. package/dist/ai-claude/index.js +56 -0
  183. package/dist/cms/runtime/api.d.ts +6 -1
  184. package/dist/cms/runtime/api.js +3 -0
  185. package/dist/cms/runtime/schemas.d.ts +9 -1
  186. package/dist/cms/runtime/schemas.js +8 -0
  187. package/dist/cms/runtime/types.d.ts +82 -10
  188. package/dist/cms/runtime/types.js +4 -0
  189. package/dist/components/ui/accordion/accordion.stories.svelte +39 -0
  190. package/dist/components/ui/accordion/accordion.stories.svelte.d.ts +27 -0
  191. package/dist/components/ui/alert/alert.stories.svelte +53 -0
  192. package/dist/components/ui/alert/alert.stories.svelte.d.ts +27 -0
  193. package/dist/components/ui/alert/alert.svelte +5 -0
  194. package/dist/components/ui/alert/alert.svelte.d.ts +9 -0
  195. package/dist/components/ui/avatar/avatar.stories.svelte +16 -0
  196. package/dist/components/ui/avatar/avatar.stories.svelte.d.ts +27 -0
  197. package/dist/components/ui/badge/badge.stories.svelte +33 -0
  198. package/dist/components/ui/badge/badge.stories.svelte.d.ts +27 -0
  199. package/dist/components/ui/breadcrumb/breadcrumb.stories.svelte +33 -0
  200. package/dist/components/ui/breadcrumb/breadcrumb.stories.svelte.d.ts +27 -0
  201. package/dist/components/ui/button/button.stories.svelte +43 -0
  202. package/dist/components/ui/button/button.stories.svelte.d.ts +27 -0
  203. package/dist/components/ui/button/button.svelte +1 -2
  204. package/dist/components/ui/button/button.svelte.d.ts +0 -3
  205. package/dist/components/ui/button-group/button-group-separator.svelte.d.ts +1 -1
  206. package/dist/components/ui/card/card.stories.svelte +42 -0
  207. package/dist/components/ui/card/card.stories.svelte.d.ts +27 -0
  208. package/dist/components/ui/command/command.stories.svelte +51 -0
  209. package/dist/components/ui/command/command.stories.svelte.d.ts +27 -0
  210. package/dist/components/ui/dialog/dialog.stories.svelte +29 -0
  211. package/dist/components/ui/dialog/dialog.stories.svelte.d.ts +27 -0
  212. package/dist/components/ui/field/field-label.svelte.d.ts +1 -1
  213. package/dist/components/ui/field/field.stories.svelte +21 -0
  214. package/dist/components/ui/field/field.stories.svelte.d.ts +27 -0
  215. package/dist/components/ui/input/input.stories.svelte +40 -0
  216. package/dist/components/ui/input/input.stories.svelte.d.ts +27 -0
  217. package/dist/components/ui/input/input.svelte +2 -4
  218. package/dist/components/ui/item/item-separator.svelte.d.ts +1 -1
  219. package/dist/components/ui/label/label.stories.svelte +20 -0
  220. package/dist/components/ui/label/label.stories.svelte.d.ts +27 -0
  221. package/dist/components/ui/popover/popover.stories.svelte +29 -0
  222. package/dist/components/ui/popover/popover.stories.svelte.d.ts +27 -0
  223. package/dist/components/ui/select/select-group-heading.svelte.d.ts +1 -1
  224. package/dist/components/ui/select/select.stories.svelte +23 -0
  225. package/dist/components/ui/select/select.stories.svelte.d.ts +27 -0
  226. package/dist/components/ui/separator/separator.stories.svelte +24 -0
  227. package/dist/components/ui/separator/separator.stories.svelte.d.ts +27 -0
  228. package/dist/components/ui/sheet/sheet.stories.svelte +29 -0
  229. package/dist/components/ui/sheet/sheet.stories.svelte.d.ts +27 -0
  230. package/dist/components/ui/sidebar/sidebar-group.svelte +3 -3
  231. package/dist/components/ui/sidebar/sidebar-group.svelte.d.ts +2 -2
  232. package/dist/components/ui/sidebar/sidebar-menu-button.svelte +28 -30
  233. package/dist/components/ui/sidebar/sidebar-menu-button.svelte.d.ts +7 -7
  234. package/dist/components/ui/sidebar/sidebar-separator.svelte.d.ts +1 -1
  235. package/dist/components/ui/sidebar/sidebar-trigger.svelte +4 -4
  236. package/dist/components/ui/sonner/sonner.stories.svelte +22 -0
  237. package/dist/components/ui/sonner/sonner.stories.svelte.d.ts +26 -0
  238. package/dist/components/ui/sonner/sonner.svelte +8 -2
  239. package/dist/components/ui/sonner/toast-demo.svelte +29 -0
  240. package/dist/components/ui/sonner/toast-demo.svelte.d.ts +6 -0
  241. package/dist/components/ui/textarea/textarea.stories.svelte +22 -0
  242. package/dist/components/ui/textarea/textarea.stories.svelte.d.ts +27 -0
  243. package/dist/components/ui/textarea/textarea.svelte +0 -2
  244. package/dist/components/ui/toggle/toggle.stories.svelte +22 -0
  245. package/dist/components/ui/toggle/toggle.stories.svelte.d.ts +27 -0
  246. package/dist/components/ui/toggle-group/toggle-group.stories.svelte +17 -0
  247. package/dist/components/ui/toggle-group/toggle-group.stories.svelte.d.ts +27 -0
  248. package/dist/components/ui/tooltip/tooltip.stories.svelte +26 -0
  249. package/dist/components/ui/tooltip/tooltip.stories.svelte.d.ts +27 -0
  250. package/dist/core/fields/fieldSchemaToTs.d.ts +1 -0
  251. package/dist/core/fields/fieldSchemaToTs.js +133 -1
  252. package/dist/core/fields/layoutUtils.d.ts +17 -0
  253. package/dist/core/fields/layoutUtils.js +149 -0
  254. package/dist/core/fields/structuredToHtml.d.ts +9 -0
  255. package/dist/core/fields/structuredToHtml.js +161 -0
  256. package/dist/core/server/entries/operations/create.js +2 -1
  257. package/dist/core/server/entries/operations/get.js +8 -6
  258. package/dist/core/server/entries/operations/update.d.ts +3 -0
  259. package/dist/core/server/entries/operations/update.js +30 -2
  260. package/dist/core/server/fields/queryStructuredContent.d.ts +15 -0
  261. package/dist/core/server/fields/queryStructuredContent.js +65 -0
  262. package/dist/core/server/fields/resolveImageFields.js +51 -2
  263. package/dist/core/server/fields/resolveRelationFields.js +2 -2
  264. package/dist/core/server/fields/resolveRichtextLinks.js +80 -13
  265. package/dist/core/server/fields/resolveUrlFields.js +57 -6
  266. package/dist/core/server/fields/slugResolver.d.ts +10 -0
  267. package/dist/core/server/fields/slugResolver.js +34 -0
  268. package/dist/core/server/generator/fields.js +15 -4
  269. package/dist/core/server/generator/generator.js +3 -2
  270. package/dist/files-local/index.js +126 -64
  271. package/dist/server/auth.d.ts +5 -0
  272. package/dist/server/auth.js +12 -1
  273. package/dist/sveltekit/components/structured-content.svelte +204 -0
  274. package/dist/sveltekit/components/structured-content.svelte.d.ts +21 -0
  275. package/dist/sveltekit/config.d.ts +13 -3
  276. package/dist/sveltekit/index.d.ts +3 -0
  277. package/dist/sveltekit/index.js +3 -0
  278. package/dist/sveltekit/server/handle.js +1 -0
  279. package/dist/types/config.d.ts +3 -0
  280. package/dist/types/fields.d.ts +19 -2
  281. package/dist/types/index.d.ts +2 -0
  282. package/dist/types/index.js +2 -0
  283. package/dist/types/layout.d.ts +54 -0
  284. package/dist/types/layout.js +6 -0
  285. package/dist/types/structured-content.d.ts +63 -0
  286. package/dist/types/structured-content.js +1 -0
  287. package/dist/updates/0.1.5/index.d.ts +2 -0
  288. package/dist/updates/0.1.5/index.js +18 -0
  289. package/dist/updates/0.2.0/index.d.ts +2 -0
  290. package/dist/updates/0.2.0/index.js +11 -0
  291. package/dist/updates/0.2.2/index.d.ts +2 -0
  292. package/dist/updates/0.2.2/index.js +13 -0
  293. package/dist/updates/0.5.0/index.d.ts +2 -0
  294. package/dist/updates/0.5.0/index.js +14 -0
  295. package/dist/updates/index.js +5 -1
  296. package/package.json +16 -9
@@ -4,8 +4,8 @@
4
4
  import Label from '../../../components/ui/label/label.svelte';
5
5
  import Button from '../../../components/ui/button/button.svelte';
6
6
  import Loader2 from '@tabler/icons-svelte/icons/loader-2';
7
+ import * as Alert from '../../../components/ui/alert/index.js';
7
8
  import { page } from '$app/state';
8
- import { goto } from '$app/navigation';
9
9
  import { usersLang } from './lang.js';
10
10
  import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
11
11
 
@@ -20,15 +20,52 @@
20
20
  let loading = $state(false);
21
21
  let error = $state('');
22
22
  let success = $state(false);
23
+ let showPassword = $state(false);
24
+ let showConfirm = $state(false);
25
+
26
+ // Password strength computation
27
+ type StrengthLevel = 'weak' | 'medium' | 'strong' | 'very-strong';
28
+
29
+ const strength = $derived.by((): { level: StrengthLevel; score: number } => {
30
+ if (!password) return { level: 'weak', score: 0 };
31
+
32
+ let score = 0;
33
+ if (password.length >= 8) score++;
34
+ if (/[A-Z]/.test(password)) score++;
35
+ if (/[0-9]/.test(password)) score++;
36
+ if (/[^A-Za-z0-9]/.test(password)) score++;
37
+
38
+ if (score <= 1) return { level: 'weak', score };
39
+ if (score === 2) return { level: 'medium', score };
40
+ if (score === 3) return { level: 'strong', score };
41
+ return { level: 'very-strong', score: 4 };
42
+ });
43
+
44
+ const strengthLabels = $derived({
45
+ weak: interfaceLanguage.current === 'pl' ? 'Słabe' : 'Weak',
46
+ medium: interfaceLanguage.current === 'pl' ? 'Średnie' : 'Medium',
47
+ strong: interfaceLanguage.current === 'pl' ? 'Silne' : 'Strong',
48
+ 'very-strong': interfaceLanguage.current === 'pl' ? 'Bardzo silne' : 'Very strong'
49
+ });
50
+
51
+ const strengthLabelText = $derived(
52
+ interfaceLanguage.current === 'pl' ? 'Siła hasła' : 'Password strength'
53
+ );
54
+
55
+ function validate(): string | null {
56
+ if (password.length < 8) return lang.passwordMinLength;
57
+ if (!/[A-Z]/.test(password) || !/[0-9]/.test(password) || !/[^A-Za-z0-9]/.test(password)) {
58
+ return lang.passwordRequirements;
59
+ }
60
+ if (password !== confirmPassword) return lang.passwordMismatch;
61
+ return null;
62
+ }
23
63
 
24
64
  async function handleSubmit(e: Event) {
25
65
  e.preventDefault();
26
- if (password !== confirmPassword) {
27
- error = lang.invite.confirmPassword + ' — mismatch';
28
- return;
29
- }
30
- if (password.length < 6) {
31
- error = 'Password must be at least 6 characters';
66
+ const validationError = validate();
67
+ if (validationError) {
68
+ error = validationError;
32
69
  return;
33
70
  }
34
71
 
@@ -51,7 +88,6 @@
51
88
  }
52
89
 
53
90
  success = true;
54
- setTimeout(() => goto('/admin/login'), 2000);
55
91
  } catch {
56
92
  error = 'Network error';
57
93
  loading = false;
@@ -59,53 +95,139 @@
59
95
  }
60
96
  </script>
61
97
 
62
- <div
63
- class="relative flex min-h-svh flex-col items-center justify-center overflow-hidden p-6 md:p-10"
64
- style="background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 50%, #cbd5e1 100%);"
98
+ <!-- Skip link (WCAG) -->
99
+ <a
100
+ href="#main-content"
101
+ class="sr-only focus:not-sr-only focus:fixed focus:left-4 focus:top-3 focus:z-[9999] focus:rounded-lg focus:bg-primary focus:px-4 focus:py-2 focus:text-[13px] focus:font-semibold focus:text-primary-foreground"
65
102
  >
103
+ {interfaceLanguage.current === 'pl' ? 'Przejdź do treści' : 'Skip to content'}
104
+ </a>
105
+
106
+ <!-- Background orbs -->
107
+ <div class="fixed inset-0 overflow-hidden pointer-events-none" aria-hidden="true">
108
+ <div class="login-orb login-orb--1"></div>
109
+ <div class="login-orb login-orb--2"></div>
110
+ <div class="login-orb login-orb--3"></div>
111
+ </div>
112
+
113
+ <!-- Main -->
114
+ <main
115
+ id="main-content"
116
+ class="relative z-[1] flex min-h-svh flex-col items-center justify-center p-6"
117
+ >
118
+ <!-- Card -->
66
119
  <div
67
- class="pointer-events-none absolute inset-0 hidden dark:block"
68
- style="background: linear-gradient(135deg, #0f172a 0%, #1e293b 50%, #2D4A77 100%);"
69
- ></div>
70
-
71
- <div class="relative z-10 w-full max-w-sm">
72
- <div
73
- class="overflow-hidden rounded-3xl border border-slate-200/50 bg-white/80 p-8 shadow-2xl backdrop-blur-xl dark:border-white/10 dark:bg-slate-900/60"
74
- >
75
- <div class="mb-6 text-center">
76
- <div class="mb-2 text-3xl font-bold" style="background: linear-gradient(135deg, #2D4A77, #4975AE); -webkit-background-clip: text; -webkit-text-fill-color: transparent;">
77
- includio
120
+ class="login-card-enter w-full max-w-[400px] rounded-xl border bg-card px-8 py-10 shadow-lg max-[480px]:rounded-lg max-[480px]:px-5 max-[480px]:py-8"
121
+ >
122
+ <div class="flex flex-col gap-5">
123
+ <!-- Brand -->
124
+ <div class="mb-2 flex flex-col items-center text-center">
125
+ <div class="mb-2 flex items-baseline gap-0.5" aria-hidden="true">
126
+ <span class="text-primary text-[28px] leading-none font-extrabold max-[480px]:text-[24px]">
127
+ Aria
128
+ </span>
129
+ <span
130
+ class="text-primary text-[16px] leading-none font-light tracking-[0.15em] uppercase max-[480px]:text-[14px]"
131
+ >
132
+ CMS
133
+ </span>
78
134
  </div>
79
- <h1 class="text-lg font-semibold">{lang.invite.acceptTitle}</h1>
80
- <p class="text-muted-foreground mt-1 text-sm">{lang.invite.acceptDescription}</p>
135
+ </div>
136
+
137
+ <div class="text-center">
138
+ <h1 class="text-lg font-semibold text-foreground">{lang.invite.acceptTitle}</h1>
139
+ <p class="text-muted-foreground mt-1 text-[14px]">{lang.invite.acceptDescription}</p>
81
140
  </div>
82
141
 
83
142
  {#if success}
84
- <div class="rounded-lg bg-green-50 p-4 text-center text-sm text-green-700 dark:bg-green-900/20 dark:text-green-400">
85
- {lang.invite.accepted}
86
- </div>
143
+ <Alert.Root variant="success" class="text-center" role="status">
144
+ <Alert.Description>{lang.invite.accepted}</Alert.Description>
145
+ </Alert.Root>
146
+ <Button href="/admin/login" class="h-11 w-full text-[15px] font-semibold">
147
+ {lang.invite.goToLogin}
148
+ </Button>
87
149
  {:else if !token}
88
- <div class="rounded-lg bg-red-50 p-4 text-center text-sm text-red-700 dark:bg-red-900/20 dark:text-red-400">
89
- {lang.invite.invalidToken}
90
- </div>
150
+ <Alert.Root variant="destructive" class="text-center">
151
+ <Alert.Description>{lang.invite.invalidToken}</Alert.Description>
152
+ </Alert.Root>
91
153
  {:else}
92
- <form onsubmit={handleSubmit} class="space-y-4">
154
+ <form onsubmit={handleSubmit} class="space-y-4" aria-label={lang.invite.acceptTitle}>
93
155
  <div class="space-y-2">
94
156
  <Label for="invite-name">{lang.name}</Label>
95
- <Input id="invite-name" bind:value={name} required class="rounded-xl" />
157
+ <Input id="invite-name" bind:value={name} required autocomplete="name" />
96
158
  </div>
97
159
  <div class="space-y-2">
98
160
  <Label for="invite-password">{lang.password}</Label>
99
- <Input id="invite-password" type="password" bind:value={password} required minlength={6} class="rounded-xl" />
161
+ <div class="acct-input-wrap">
162
+ <Input
163
+ id="invite-password"
164
+ type={showPassword ? 'text' : 'password'}
165
+ bind:value={password}
166
+ required
167
+ minlength={8}
168
+ autocomplete="new-password"
169
+ aria-describedby="invite-pw-hint invite-pw-strength"
170
+ class="login-input"
171
+ />
172
+ <button
173
+ type="button"
174
+ class="acct-pw-toggle"
175
+ aria-label={showPassword ? (interfaceLanguage.current === 'pl' ? 'Ukryj hasło' : 'Hide password') : (interfaceLanguage.current === 'pl' ? 'Pokaż hasło' : 'Show password')}
176
+ onclick={() => showPassword = !showPassword}
177
+ >
178
+ {#if showPassword}
179
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/><line x1="1" y1="1" x2="23" y2="23"/></svg>
180
+ {:else}
181
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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>
182
+ {/if}
183
+ </button>
184
+ </div>
185
+
186
+ <!-- Strength meter -->
187
+ {#if password}
188
+ <div class="acct-strength-meter" role="img" aria-label="{strengthLabelText}: {strengthLabels[strength.level]}">
189
+ {#each [1, 2, 3, 4] as i}
190
+ <div class="acct-strength-seg {i <= strength.score ? `filled-${strength.level}` : ''}"></div>
191
+ {/each}
192
+ </div>
193
+ <p id="invite-pw-strength" class="acct-strength-text {strength.level}">
194
+ {strengthLabelText}: {strengthLabels[strength.level]}
195
+ </p>
196
+ {/if}
197
+ <p id="invite-pw-hint" class="text-text-light mt-1 text-xs">
198
+ {lang.passwordHint}
199
+ </p>
100
200
  </div>
101
201
  <div class="space-y-2">
102
202
  <Label for="invite-confirm">{lang.invite.confirmPassword}</Label>
103
- <Input id="invite-confirm" type="password" bind:value={confirmPassword} required minlength={6} class="rounded-xl" />
203
+ <div class="acct-input-wrap">
204
+ <Input
205
+ id="invite-confirm"
206
+ type={showConfirm ? 'text' : 'password'}
207
+ bind:value={confirmPassword}
208
+ required
209
+ minlength={8}
210
+ autocomplete="new-password"
211
+ class="login-input"
212
+ />
213
+ <button
214
+ type="button"
215
+ class="acct-pw-toggle"
216
+ aria-label={showConfirm ? (interfaceLanguage.current === 'pl' ? 'Ukryj hasło' : 'Hide password') : (interfaceLanguage.current === 'pl' ? 'Pokaż hasło' : 'Show password')}
217
+ onclick={() => showConfirm = !showConfirm}
218
+ >
219
+ {#if showConfirm}
220
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/><line x1="1" y1="1" x2="23" y2="23"/></svg>
221
+ {:else}
222
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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>
223
+ {/if}
224
+ </button>
225
+ </div>
104
226
  </div>
105
227
  {#if error}
106
- <p class="text-destructive text-sm">{error}</p>
228
+ <p class="text-destructive text-sm" role="alert">{error}</p>
107
229
  {/if}
108
- <Button type="submit" class="w-full rounded-xl" disabled={loading}>
230
+ <Button type="submit" class="h-11 w-full text-[15px] font-semibold" disabled={loading}>
109
231
  {#if loading}
110
232
  <Loader2 class="mr-2 size-4 animate-spin" />
111
233
  {/if}
@@ -115,4 +237,11 @@
115
237
  {/if}
116
238
  </div>
117
239
  </div>
118
- </div>
240
+
241
+ <!-- Footer -->
242
+ <footer class="login-footer-enter mt-6 text-center">
243
+ <p class="text-xs text-text-light">
244
+ {interfaceLanguage.current === 'pl' ? 'Stworzone przez Includio' : 'Created by Includio'}
245
+ </p>
246
+ </footer>
247
+ </main>
@@ -96,7 +96,7 @@
96
96
  </div>
97
97
  <div class="space-y-2">
98
98
  <Label for="create-password">{lang.password}</Label>
99
- <div class="flex gap-1">
99
+ <div class="flex gap-2">
100
100
  <div class="relative flex-1">
101
101
  <Input
102
102
  id="create-password"
@@ -105,10 +105,12 @@
105
105
  required
106
106
  minlength={6}
107
107
  class="pr-9"
108
+ aria-describedby="create-password-hint"
108
109
  />
109
110
  <button
110
111
  type="button"
111
- class="text-muted-foreground hover:text-foreground absolute right-2 top-1/2 -translate-y-1/2"
112
+ class="absolute right-2 top-1/2 -translate-y-1/2"
113
+ style="color: var(--text-light);"
112
114
  onclick={() => (showPassword = !showPassword)}
113
115
  >
114
116
  {#if showPassword}
@@ -120,23 +122,26 @@
120
122
  </div>
121
123
  <Button
122
124
  type="button"
123
- variant="outline"
124
- size="icon"
125
- class="shrink-0"
125
+ variant="secondary"
126
+ size="sm"
127
+ class="shrink-0 gap-1.5"
126
128
  onclick={() => {
127
129
  password = generatePassword();
128
130
  showPassword = true;
129
131
  }}
130
- title={lang.generatePassword}
131
132
  >
132
133
  <DiceIcon class="size-4" />
134
+ {lang.generatePassword}
133
135
  </Button>
134
136
  </div>
137
+ <p id="create-password-hint" class="text-xs" style="color: var(--text-light);">
138
+ {lang.passwordHint}
139
+ </p>
135
140
  </div>
136
141
  <div class="space-y-2">
137
142
  <Label>{lang.role}</Label>
138
143
  <Select.Root type="single" value={role} onValueChange={(v) => v && (role = v as UserRole)}>
139
- <Select.Trigger class="w-full">
144
+ <Select.Trigger class="w-full" aria-describedby="create-role-hint">
140
145
  {role === 'admin' ? lang.roleAdmin : lang.roleUser}
141
146
  </Select.Trigger>
142
147
  <Select.Content>
@@ -144,6 +149,9 @@
144
149
  <Select.Item value="admin">{lang.roleAdmin}</Select.Item>
145
150
  </Select.Content>
146
151
  </Select.Root>
152
+ <p id="create-role-hint" class="text-xs" style="color: var(--text-light);">
153
+ {lang.roleHint}
154
+ </p>
147
155
  </div>
148
156
  {#if error}
149
157
  <p class="text-destructive text-sm">{error}</p>
@@ -1,5 +1,9 @@
1
1
  <script lang="ts">
2
- import * as AlertDialog from '../../../components/ui/alert-dialog/index.js';
2
+ import * as Dialog from '../../../components/ui/dialog/index.js';
3
+ import { Input } from '../../../components/ui/input/index.js';
4
+ import Label from '../../../components/ui/label/label.svelte';
5
+ import Button from '../../../components/ui/button/button.svelte';
6
+ import AlertTriangle from '@tabler/icons-svelte/icons/alert-triangle';
3
7
  import { authClient } from '../../auth-client.js';
4
8
  import { toast } from 'svelte-sonner';
5
9
  import { usersLang } from './lang.js';
@@ -9,45 +13,106 @@
9
13
  open: boolean;
10
14
  onOpenChange: (open: boolean) => void;
11
15
  onDeleted: () => void;
12
- userId: string | null;
16
+ user: { id: string; name: string } | null;
13
17
  currentUserId: string;
14
18
  };
15
19
 
16
- let { open = $bindable(), onOpenChange, onDeleted, userId, currentUserId }: Props = $props();
20
+ let { open = $bindable(), onOpenChange, onDeleted, user, currentUserId }: Props = $props();
17
21
 
18
22
  const interfaceLanguage = useInterfaceLanguage();
19
23
  const lang = $derived(usersLang[interfaceLanguage.current]);
20
24
 
25
+ let confirmInput = $state('');
26
+ let loading = $state(false);
27
+
28
+ const confirmed = $derived(confirmInput === lang.deleteConfirmWord);
29
+
30
+ function reset() {
31
+ confirmInput = '';
32
+ loading = false;
33
+ }
34
+
21
35
  async function handleDelete() {
22
- if (!userId) return;
36
+ if (!user || !confirmed) return;
23
37
 
24
- if (userId === currentUserId) {
38
+ if (user.id === currentUserId) {
25
39
  toast.error(lang.cannotDeleteSelf);
26
40
  onOpenChange(false);
27
41
  return;
28
42
  }
29
43
 
30
- const { error } = await authClient.admin.removeUser({ userId });
44
+ loading = true;
45
+ const { error } = await authClient.admin.removeUser({ userId: user.id });
31
46
 
32
47
  if (error) {
33
48
  toast.error(error.message || 'Error');
49
+ loading = false;
34
50
  onOpenChange(false);
35
51
  return;
36
52
  }
37
53
 
38
54
  toast.success(lang.userDeleted);
55
+ reset();
39
56
  onOpenChange(false);
40
57
  onDeleted();
41
58
  }
42
59
  </script>
43
60
 
44
- <AlertDialog.Root {open} onOpenChange={(v) => onOpenChange(v)}>
45
- <AlertDialog.Content>
46
- <AlertDialog.Title>{lang.deleteConfirmTitle}</AlertDialog.Title>
47
- <AlertDialog.Description>{lang.deleteConfirmDescription}</AlertDialog.Description>
48
- <AlertDialog.Footer>
49
- <AlertDialog.Cancel>{lang.cancel}</AlertDialog.Cancel>
50
- <AlertDialog.Action onclick={handleDelete}>{lang.deleteUser}</AlertDialog.Action>
51
- </AlertDialog.Footer>
52
- </AlertDialog.Content>
53
- </AlertDialog.Root>
61
+ <Dialog.Root
62
+ {open}
63
+ onOpenChange={(v) => {
64
+ if (!v) reset();
65
+ onOpenChange(v);
66
+ }}
67
+ >
68
+ <Dialog.Content>
69
+ <Dialog.Header>
70
+ <Dialog.Title>{lang.deleteConfirmTitle}</Dialog.Title>
71
+ </Dialog.Header>
72
+
73
+ {#if user}
74
+ <div class="space-y-4">
75
+ <!-- Warning box -->
76
+ <div class="users-delete-warning">
77
+ <div class="users-delete-warning-icon">
78
+ <AlertTriangle class="size-5" />
79
+ </div>
80
+ <div class="flex-1">
81
+ <p class="text-sm font-semibold" style="color: var(--destructive);">
82
+ {lang.deleteWarningTitle}
83
+ </p>
84
+ <p class="mt-1 text-sm" style="color: var(--muted-foreground);">
85
+ {@html lang.deleteWarningDesc(user.name)}
86
+ </p>
87
+ </div>
88
+ </div>
89
+
90
+ <!-- Confirm input -->
91
+ <div class="space-y-2">
92
+ <Label for="delete-confirm">
93
+ {@html lang.deleteConfirmType}
94
+ </Label>
95
+ <Input
96
+ id="delete-confirm"
97
+ bind:value={confirmInput}
98
+ placeholder={lang.deleteConfirmWord}
99
+ autocomplete="off"
100
+ />
101
+ </div>
102
+
103
+ <Dialog.Footer>
104
+ <Button type="button" variant="outline" onclick={() => onOpenChange(false)}>
105
+ {lang.cancel}
106
+ </Button>
107
+ <Button
108
+ variant="destructive"
109
+ disabled={!confirmed || loading}
110
+ onclick={handleDelete}
111
+ >
112
+ {lang.deleteUser}
113
+ </Button>
114
+ </Dialog.Footer>
115
+ </div>
116
+ {/if}
117
+ </Dialog.Content>
118
+ </Dialog.Root>
@@ -2,7 +2,10 @@ type Props = {
2
2
  open: boolean;
3
3
  onOpenChange: (open: boolean) => void;
4
4
  onDeleted: () => void;
5
- userId: string | null;
5
+ user: {
6
+ id: string;
7
+ name: string;
8
+ } | null;
6
9
  currentUserId: string;
7
10
  };
8
11
  declare const DeleteUserDialog: import("svelte").Component<Props, {}, "open">;
@@ -113,6 +113,9 @@
113
113
  <Select.Item value="admin">{lang.roleAdmin}</Select.Item>
114
114
  </Select.Content>
115
115
  </Select.Root>
116
+ <p id="edit-role-hint" class="text-xs" style="color: var(--text-light);">
117
+ {lang.roleHint}
118
+ </p>
116
119
  {/if}
117
120
  </div>
118
121
  {#if error}
@@ -40,7 +40,7 @@
40
40
  const res = await fetch('/admin/api/invite', {
41
41
  method: 'POST',
42
42
  headers: { 'Content-Type': 'application/json' },
43
- body: JSON.stringify({ email, role })
43
+ body: JSON.stringify({ email, role, lang: interfaceLanguage.current })
44
44
  });
45
45
 
46
46
  const data = await res.json();
@@ -81,12 +81,22 @@
81
81
  <form onsubmit={handleSubmit} class="space-y-4">
82
82
  <div class="space-y-2">
83
83
  <Label for="invite-email">{lang.email}</Label>
84
- <Input id="invite-email" type="email" bind:value={email} required />
84
+ <Input
85
+ id="invite-email"
86
+ type="email"
87
+ bind:value={email}
88
+ required
89
+ placeholder={lang.invite.emailPlaceholder}
90
+ aria-describedby="invite-email-hint"
91
+ />
92
+ <p id="invite-email-hint" class="text-xs" style="color: var(--text-light);">
93
+ {lang.invite.emailHint}
94
+ </p>
85
95
  </div>
86
96
  <div class="space-y-2">
87
97
  <Label>{lang.role}</Label>
88
98
  <Select.Root type="single" value={role} onValueChange={(v) => v && (role = v as UserRole)}>
89
- <Select.Trigger class="w-full">
99
+ <Select.Trigger class="w-full" aria-describedby="invite-role-hint">
90
100
  {role === 'admin' ? lang.roleAdmin : lang.roleUser}
91
101
  </Select.Trigger>
92
102
  <Select.Content>
@@ -94,6 +104,9 @@
94
104
  <Select.Item value="admin">{lang.roleAdmin}</Select.Item>
95
105
  </Select.Content>
96
106
  </Select.Root>
107
+ <p id="invite-role-hint" class="text-xs" style="color: var(--text-light);">
108
+ {lang.invite.roleHint}
109
+ </p>
97
110
  </div>
98
111
  {#if error}
99
112
  <p class="text-destructive text-sm">{error}</p>
@@ -1,6 +1,7 @@
1
1
  import type { InterfaceLanguage } from '../../../types/languages.js';
2
2
  export declare const usersLang: Record<InterfaceLanguage, {
3
3
  title: string;
4
+ description: string;
4
5
  name: string;
5
6
  email: string;
6
7
  role: string;
@@ -13,10 +14,19 @@ export declare const usersLang: Record<InterfaceLanguage, {
13
14
  save: string;
14
15
  cancel: string;
15
16
  password: string;
17
+ passwordHint: string;
18
+ passwordMinLength: string;
19
+ passwordRequirements: string;
20
+ passwordMismatch: string;
16
21
  roleAdmin: string;
17
22
  roleUser: string;
23
+ roleHint: string;
18
24
  deleteConfirmTitle: string;
19
25
  deleteConfirmDescription: string;
26
+ deleteWarningTitle: string;
27
+ deleteWarningDesc: (name: string) => string;
28
+ deleteConfirmType: string;
29
+ deleteConfirmWord: string;
20
30
  userCreated: string;
21
31
  userUpdated: string;
22
32
  userDeleted: string;
@@ -24,6 +34,16 @@ export declare const usersLang: Record<InterfaceLanguage, {
24
34
  cannotChangeSelfRole: string;
25
35
  noResults: string;
26
36
  generatePassword: string;
37
+ filterByRole: string;
38
+ allRoles: string;
39
+ showing: (start: number, end: number, total: number) => string;
40
+ selected: (n: number) => string;
41
+ changeRole: string;
42
+ emptyTitle: string;
43
+ emptyDescription: string;
44
+ addUser: string;
45
+ selectAll: string;
46
+ selectUser: string;
27
47
  sessions: {
28
48
  title: string;
29
49
  noSessions: string;
@@ -49,9 +69,16 @@ export declare const usersLang: Record<InterfaceLanguage, {
49
69
  confirmPassword: string;
50
70
  acceptInvite: string;
51
71
  accepted: string;
72
+ goToLogin: string;
52
73
  invalidToken: string;
53
74
  emailConfigRequired: string;
54
75
  resend: string;
55
76
  resent: string;
77
+ emailPlaceholder: string;
78
+ emailHint: string;
79
+ roleHint: string;
80
+ expiringSoon: string;
81
+ resendLabel: (email: string) => string;
82
+ cancelLabel: (email: string) => string;
56
83
  };
57
84
  }>;