@frigear-nu/account 0.1.2 → 0.1.3

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 (35) hide show
  1. package/README.md +21 -9
  2. package/dist/module.d.mts +3 -1
  3. package/dist/module.json +1 -1
  4. package/dist/module.mjs +4 -2
  5. package/dist/runtime/app/components/Account/AccountApiTokenSlideover.d.vue.ts +3 -3
  6. package/dist/runtime/app/components/Account/AccountApiTokenSlideover.vue +2 -1
  7. package/dist/runtime/app/components/Account/AccountApiTokenSlideover.vue.d.ts +3 -3
  8. package/dist/runtime/app/components/Account/AccountChangeEmailSlideover.d.vue.ts +14 -0
  9. package/dist/runtime/app/components/Account/AccountChangeEmailSlideover.vue +54 -0
  10. package/dist/runtime/app/components/Account/AccountChangeEmailSlideover.vue.d.ts +14 -0
  11. package/dist/runtime/app/components/Account/AccountHeader.d.vue.ts +10 -1
  12. package/dist/runtime/app/components/Account/AccountHeader.vue +6 -3
  13. package/dist/runtime/app/components/Account/AccountHeader.vue.d.ts +10 -1
  14. package/dist/runtime/app/components/Account/AccountIdentity.vue +13 -0
  15. package/dist/runtime/app/components/Account/AccountMenu.vue +13 -3
  16. package/dist/runtime/app/components/Account/AccountPasskeySlideover.d.vue.ts +10 -0
  17. package/dist/runtime/app/components/Account/AccountPasskeySlideover.vue +60 -0
  18. package/dist/runtime/app/components/Account/AccountPasskeySlideover.vue.d.ts +10 -0
  19. package/dist/runtime/app/composables/useAccount.d.ts +2 -2
  20. package/dist/runtime/app/composables/useAccount.js +4 -3
  21. package/dist/runtime/app/layouts/account.d.vue.ts +2 -2
  22. package/dist/runtime/app/layouts/account.vue +13 -11
  23. package/dist/runtime/app/layouts/account.vue.d.ts +2 -2
  24. package/dist/runtime/app/pages/api-tokens.vue +23 -28
  25. package/dist/runtime/app/pages/documents.vue +22 -7
  26. package/dist/runtime/app/pages/index.vue +3 -6
  27. package/dist/runtime/app/pages/profile.vue +50 -89
  28. package/dist/runtime/app/pages/security.vue +40 -60
  29. package/dist/runtime/app/utils/api-tokens.d.ts +3 -14
  30. package/dist/runtime/i18n/lang/da.json +11 -9
  31. package/dist/runtime/i18n/lang/en.json +11 -9
  32. package/package.json +11 -11
  33. package/dist/runtime/app/components/Account/AccountHeaderRight.vue +0 -18
  34. /package/dist/runtime/app/components/Account/{AccountHeaderRight.d.vue.ts → AccountIdentity.d.vue.ts} +0 -0
  35. /package/dist/runtime/app/components/Account/{AccountHeaderRight.vue.d.ts → AccountIdentity.vue.d.ts} +0 -0
@@ -10,26 +10,41 @@ const documents = computed(() => visibleDocuments(useRuntimeConfig().public.acco
10
10
 
11
11
  <template>
12
12
  <div class="flex flex-col gap-4">
13
- <SectionCard>
14
- <UPageGrid v-if="documents.length">
15
- <UPageCard
13
+ <AccountHeader />
14
+
15
+ <SectionCard flush>
16
+ <ul
17
+ v-if="documents.length"
18
+ class="divide-y divide-default"
19
+ >
20
+ <li
16
21
  v-for="document in documents"
17
22
  :key="document.id"
18
- :title="documentLabel(document, locale)"
19
- :icon="document.icon ?? 'i-lucide-file'"
23
+ class="flex items-center gap-3 px-4 py-3"
20
24
  >
25
+ <UIcon
26
+ :name="document.icon ?? 'i-lucide-file'"
27
+ class="size-5 shrink-0 text-muted"
28
+ />
29
+ <p class="min-w-0 flex-1 truncate text-sm font-medium text-highlighted">
30
+ {{ documentLabel(document, locale) }}
31
+ </p>
21
32
  <UButton
22
33
  download
23
34
  external
24
35
  :to="document.path"
36
+ color="neutral"
37
+ variant="outline"
25
38
  icon="i-lucide-download"
26
39
  :label="t('download', 'Download')"
40
+ :aria-label="t('downloadNamed', 'Download {name}', { name: documentLabel(document, locale) })"
27
41
  />
28
- </UPageCard>
29
- </UPageGrid>
42
+ </li>
43
+ </ul>
30
44
  <UEmpty
31
45
  v-else
32
46
  variant="naked"
47
+ icon="i-lucide-files"
33
48
  :title="t('empty', 'No documents for you yet.')"
34
49
  />
35
50
  </SectionCard>
@@ -1,6 +1,5 @@
1
1
  <script setup>
2
2
  defineOptions({ name: "AccountIndexPage" });
3
- definePageMeta({ heading: false });
4
3
  const t = useModuleT("account");
5
4
  const { user } = useUserSession();
6
5
  const { localize } = useLocaleLink();
@@ -18,10 +17,7 @@ const profile = computed(() => {
18
17
 
19
18
  <template>
20
19
  <div class="flex flex-col gap-4">
21
- <PageHeading
22
- :title="t('overview.title', 'Account')"
23
- :description="t('overview.description', 'Your details and everything that belongs to your account.')"
24
- />
20
+ <AccountHeader />
25
21
 
26
22
  <SectionCard>
27
23
  <div class="flex flex-wrap items-center justify-between gap-4">
@@ -32,7 +28,7 @@ const profile = computed(() => {
32
28
  size="lg"
33
29
  :ui="{ name: 'truncate', description: 'truncate' }"
34
30
  />
35
- <AccountHeaderRight />
31
+ <AccountIdentity />
36
32
  </div>
37
33
  </SectionCard>
38
34
 
@@ -41,6 +37,7 @@ const profile = computed(() => {
41
37
  v-if="widgetArea"
42
38
  name="account.overview"
43
39
  :context="{ user }"
40
+ grid-class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3"
44
41
  />
45
42
 
46
43
  <ListCard
@@ -1,14 +1,19 @@
1
1
  <script setup>
2
2
  import { z } from "zod";
3
+ import AccountChangeEmailSlideover from "../components/Account/AccountChangeEmailSlideover.vue";
3
4
  defineOptions({ name: "AccountProfilePage" });
4
5
  const t = useModuleT("account.profile");
5
6
  const toast = useToast();
6
7
  const route = useRoute();
7
8
  const { user } = useUserSession();
8
- const { updateProfileDetails, changeEmailAddress } = useAccount();
9
+ const { updateProfileDetails } = useAccount();
9
10
  const schema = z.object({
10
11
  name: z.string().trim().min(1, t("name.required", "Please enter your name.")),
11
- avatar: z.union([z.literal(""), z.url(t("avatar.invalid", "Enter a valid URL."))]).optional()
12
+ // The server's rule: an http(s) URL of at most 2048 characters (empty clears the avatar).
13
+ avatar: z.union([
14
+ z.literal(""),
15
+ z.url({ protocol: /^https?$/, message: t("avatar.invalid", "Enter a valid URL.") }).max(2048, t("avatar.invalid", "Enter a valid URL."))
16
+ ]).optional()
12
17
  });
13
18
  const state = reactive({
14
19
  name: user.value?.name ?? "",
@@ -28,32 +33,32 @@ const dirty = computed(() => state.name !== saved.value.name || state.avatar !==
28
33
  const profileAction = useAction();
29
34
  function onSubmitProfile(event) {
30
35
  return profileAction.run(
31
- () => updateProfileDetails({ name: event.data.name, avatar: event.data.avatar || void 0 }),
36
+ () => updateProfileDetails({ name: event.data.name, avatar: event.data.avatar || null }),
32
37
  { success: t("saved", "Profile updated"), error: t("saveFailed", "Could not update your profile.") }
33
38
  );
34
39
  }
35
- const emailModalOpen = ref(false);
36
- const newEmail = ref("");
37
40
  const emailVerificationSent = ref(false);
38
41
  const emailChanged = computed(() => route.query["email-changed"] === "1");
39
42
  const emailChangeFailed = computed(() => route.query.error === "email-change");
40
- const emailSchema = z.object({ email: z.email(t("email.invalid", "Enter a valid email address.")) });
41
- const emailAction = useAction();
42
- async function onSubmitEmail() {
43
- const result = await emailAction.run(
44
- () => changeEmailAddress({ email: newEmail.value }),
45
- { error: t("email.change.error", "Could not change your email address.") }
46
- );
43
+ const overlay = useOverlay();
44
+ const emailSlideover = overlay.create(AccountChangeEmailSlideover, { destroyOnClose: true });
45
+ async function onChangeEmail() {
46
+ const result = await emailSlideover.open({ email: user.value?.email ?? void 0 });
47
47
  if (!result) return;
48
- emailVerificationSent.value = result.verificationRequired === true;
49
- emailModalOpen.value = false;
50
- newEmail.value = "";
51
- toast.add({ title: result.verificationRequired ? t("email.change.sent.title", "Check your inbox") : t("email.change.done", "Your email address was changed."), color: "success" });
48
+ emailVerificationSent.value = result.verificationRequired;
49
+ toast.add({
50
+ title: result.verificationRequired ? t("email.change.sent.title", "Check your inbox") : t("email.change.done", "Your email address was changed."),
51
+ description: result.verificationRequired ? void 0 : user.value?.email ?? void 0,
52
+ color: "success",
53
+ icon: "i-lucide-check"
54
+ });
52
55
  }
53
56
  </script>
54
57
 
55
58
  <template>
56
59
  <div class="flex flex-col gap-4">
60
+ <AccountHeader />
61
+
57
62
  <UAlert
58
63
  v-if="emailChanged"
59
64
  color="success"
@@ -74,52 +79,51 @@ async function onSubmitEmail() {
74
79
  />
75
80
 
76
81
  <SectionCard
77
- :title="t('title', 'Profile')"
78
- :description="t('description', 'Change your information.')"
82
+ :title="t('details.title', 'Name and avatar')"
83
+ :description="t('details.description', 'How you appear to others.')"
79
84
  >
80
85
  <UForm
81
86
  id="account-profile"
82
87
  :schema="schema"
83
88
  :state="state"
84
- class="flex flex-col gap-4"
89
+ class="flex flex-col divide-y divide-default"
85
90
  @submit="onSubmitProfile"
86
91
  >
87
- <UFormField
92
+ <FormRow
88
93
  name="name"
89
94
  :label="t('name.label', 'Name')"
90
95
  :description="t('name.description', 'Your full name.')"
91
96
  required
92
- class="flex items-start justify-between gap-4 max-sm:flex-col"
93
97
  >
94
98
  <UInput
95
99
  v-model="state.name"
96
100
  autocomplete="name"
101
+ class="w-full sm:max-w-sm"
97
102
  />
98
- </UFormField>
99
- <USeparator />
100
- <UFormField
103
+ </FormRow>
104
+ <FormRow
101
105
  name="avatar"
102
106
  :label="t('avatar.label', 'Avatar')"
103
107
  :description="t('avatar.description', 'Link to a square image.')"
104
- class="flex items-start justify-between gap-4 max-sm:flex-col"
105
108
  >
106
- <div class="flex items-center gap-3">
109
+ <div class="flex items-center gap-3 sm:max-w-sm">
107
110
  <UAvatar
108
111
  :src="state.avatar || void 0"
109
112
  :alt="state.name"
110
- size="xl"
113
+ size="lg"
111
114
  />
112
115
  <UInput
113
116
  v-model="state.avatar"
114
117
  type="url"
115
118
  placeholder="https://"
116
119
  :aria-label="t('avatar.url', 'Avatar URL')"
120
+ class="min-w-0 flex-1"
117
121
  />
118
122
  </div>
119
- </UFormField>
123
+ </FormRow>
120
124
  </UForm>
121
125
  <template #footer>
122
- <div class="flex justify-end">
126
+ <div class="flex justify-end gap-2">
123
127
  <UButton
124
128
  type="submit"
125
129
  form="account-profile"
@@ -131,71 +135,28 @@ async function onSubmitEmail() {
131
135
  </template>
132
136
  </SectionCard>
133
137
 
134
- <SectionCard
135
- :title="t('email.label', 'Email')"
136
- :description="t('email.description', 'Your email address.')"
137
- >
138
- <template #actions>
139
- <UButton
140
- color="neutral"
141
- variant="outline"
142
- icon="i-lucide-pencil"
143
- :label="t('email.change.title', 'Change email')"
144
- @click="emailModalOpen = true"
145
- />
146
- </template>
147
- <UInput
148
- :model-value="user?.email ?? ''"
149
- type="email"
150
- readonly
151
- disabled
152
- class="w-full sm:max-w-xs"
153
- :aria-label="t('email.label', 'Email')"
154
- />
155
- </SectionCard>
156
-
157
- <UModal
158
- v-model:open="emailModalOpen"
159
- :title="t('email.change.title', 'Change email')"
160
- >
161
- <template #body>
162
- <UForm
163
- id="account-change-email"
164
- :schema="emailSchema"
165
- :state="{ email: newEmail }"
166
- class="flex flex-col gap-4"
167
- @submit="onSubmitEmail"
168
- >
169
- <UFormField
170
- name="email"
171
- :label="t('email.change.label', 'New e-mail')"
172
- required
173
- >
174
- <UInput
175
- v-model="newEmail"
176
- type="email"
177
- autocomplete="email"
178
- class="w-full"
179
- />
180
- </UFormField>
181
- </UForm>
182
- </template>
183
- <template #footer>
184
- <div class="flex w-full justify-end gap-2">
138
+ <SectionCard :title="t('email.label', 'Email')">
139
+ <FormRow
140
+ :label="t('email.address', 'Address')"
141
+ :description="t('email.description', 'Where we send messages and how you sign in.')"
142
+ >
143
+ <div class="flex flex-wrap items-center gap-2 sm:max-w-lg">
144
+ <UInput
145
+ :model-value="user?.email ?? ''"
146
+ type="email"
147
+ readonly
148
+ disabled
149
+ class="min-w-48 flex-1"
150
+ />
185
151
  <UButton
186
152
  color="neutral"
187
153
  variant="outline"
188
- :label="t('cancel', 'Cancel')"
189
- @click="emailModalOpen = false"
190
- />
191
- <UButton
192
- type="submit"
193
- form="account-change-email"
194
- :loading="emailAction.busy.value"
154
+ icon="i-lucide-pencil"
195
155
  :label="t('email.change.title', 'Change email')"
156
+ @click="onChangeEmail"
196
157
  />
197
158
  </div>
198
- </template>
199
- </UModal>
159
+ </FormRow>
160
+ </SectionCard>
200
161
  </div>
201
162
  </template>
@@ -1,5 +1,7 @@
1
1
  <script setup>
2
2
  import { z } from "zod";
3
+ import { authApi } from "@frigear-nu/auth/api";
4
+ import AccountPasskeySlideover from "../components/Account/AccountPasskeySlideover.vue";
3
5
  defineOptions({ name: "AccountSecurityPage" });
4
6
  const t = useModuleT("account.security");
5
7
  const toast = useToast();
@@ -33,34 +35,25 @@ async function onSubmitPassword(event) {
33
35
  passwordState.newPassword = "";
34
36
  passwordState.confirmNewPassword = "";
35
37
  }
36
- const passkeyName = ref("");
37
- const passkeyPending = ref(false);
38
38
  const {
39
39
  data: passkeyData,
40
40
  error: passkeysError,
41
41
  loading: passkeysLoading,
42
42
  refresh: loadPasskeys
43
- } = await useCachedFetch("/api/auth/webauthn/credentials", {
43
+ } = await useCachedFetch(authApi.me.passkeys, {
44
44
  key: () => `account:passkeys:${user.value?.id ?? "anonymous"}`,
45
45
  ttl: 0,
46
46
  immediate: webAuthnEnabled,
47
47
  watch: false
48
48
  });
49
49
  const passkeyList = computed(() => passkeyData.value ?? []);
50
- async function onRegisterPasskey() {
51
- passkeyPending.value = true;
52
- try {
53
- await passkeys?.registerPasskey(user.value?.email ?? "", passkeyName.value);
54
- passkeyName.value = "";
55
- toast.add({ title: t("passkeys.registered", "Passkey added."), color: "success", icon: "i-lucide-check" });
56
- await loadPasskeys();
57
- } catch (e) {
58
- if (e?.name !== "NotAllowedError") {
59
- showError(e, t("passkeys.registerError", "Could not register the passkey."));
60
- }
61
- } finally {
62
- passkeyPending.value = false;
63
- }
50
+ const overlay = useOverlay();
51
+ const passkeySlideover = overlay.create(AccountPasskeySlideover, { destroyOnClose: true });
52
+ async function onAddPasskey() {
53
+ const added = await passkeySlideover.open({ userName: user.value?.email ?? "" });
54
+ if (!added) return;
55
+ toast.add({ title: t("passkeys.registered", "Passkey added."), color: "success", icon: "i-lucide-check" });
56
+ await loadPasskeys();
64
57
  }
65
58
  const passkeyLabel = (passkey) => passkey.name || t("passkeys.unnamed", "Passkey");
66
59
  async function onRemovePasskey(passkey) {
@@ -86,6 +79,8 @@ const passkeyMenu = (passkey) => [[
86
79
 
87
80
  <template>
88
81
  <div class="flex flex-col gap-4">
82
+ <AccountHeader />
83
+
89
84
  <SectionCard
90
85
  :title="t('password.title', 'Password')"
91
86
  :description="t('password.description', 'Change your password.')"
@@ -94,10 +89,10 @@ const passkeyMenu = (passkey) => [[
94
89
  id="account-change-password"
95
90
  :schema="passwordSchema"
96
91
  :state="passwordState"
97
- class="flex flex-col gap-4"
92
+ class="flex flex-col divide-y divide-default"
98
93
  @submit="onSubmitPassword"
99
94
  >
100
- <UFormField
95
+ <FormRow
101
96
  :label="t('changePassword.current.label', 'Current password')"
102
97
  name="currentPassword"
103
98
  required
@@ -108,8 +103,8 @@ const passkeyMenu = (passkey) => [[
108
103
  autocomplete="current-password"
109
104
  class="w-full sm:max-w-sm"
110
105
  />
111
- </UFormField>
112
- <UFormField
106
+ </FormRow>
107
+ <FormRow
113
108
  :label="t('changePassword.new.label', 'New password')"
114
109
  name="newPassword"
115
110
  required
@@ -120,8 +115,8 @@ const passkeyMenu = (passkey) => [[
120
115
  autocomplete="new-password"
121
116
  class="w-full sm:max-w-sm"
122
117
  />
123
- </UFormField>
124
- <UFormField
118
+ </FormRow>
119
+ <FormRow
125
120
  :label="t('changePassword.confirm.label', 'Confirm new password')"
126
121
  name="confirmNewPassword"
127
122
  required
@@ -132,10 +127,10 @@ const passkeyMenu = (passkey) => [[
132
127
  autocomplete="new-password"
133
128
  class="w-full sm:max-w-sm"
134
129
  />
135
- </UFormField>
130
+ </FormRow>
136
131
  </UForm>
137
132
  <template #footer>
138
- <div class="flex justify-end">
133
+ <div class="flex justify-end gap-2">
139
134
  <UButton
140
135
  type="submit"
141
136
  form="account-change-password"
@@ -153,6 +148,13 @@ const passkeyMenu = (passkey) => [[
153
148
  :loading="passkeysLoading"
154
149
  flush
155
150
  >
151
+ <template #actions>
152
+ <UButton
153
+ icon="i-lucide-plus"
154
+ :label="t('passkeys.add', 'Add passkey')"
155
+ @click="onAddPasskey"
156
+ />
157
+ </template>
156
158
  <ul
157
159
  v-if="passkeyList.length"
158
160
  class="divide-y divide-default"
@@ -160,21 +162,19 @@ const passkeyMenu = (passkey) => [[
160
162
  <li
161
163
  v-for="passkey in passkeyList"
162
164
  :key="passkey.id"
163
- class="flex items-center justify-between gap-3 px-4 py-3"
165
+ class="flex items-center gap-3 px-4 py-3"
164
166
  >
165
- <div class="flex min-w-0 items-center gap-3">
166
- <UIcon
167
- name="i-lucide-key-round"
168
- class="size-5 shrink-0 text-muted"
169
- />
170
- <div class="min-w-0">
171
- <p class="truncate text-sm font-medium text-highlighted">
172
- {{ passkeyLabel(passkey) }}
173
- </p>
174
- <p class="text-sm text-muted">
175
- {{ t("passkeys.created", "Added {date}", { date: format.date(passkey.createdAt) }) }}
176
- </p>
177
- </div>
167
+ <UIcon
168
+ name="i-lucide-key-round"
169
+ class="size-5 shrink-0 text-muted"
170
+ />
171
+ <div class="min-w-0 flex-1">
172
+ <p class="truncate text-sm font-medium text-highlighted">
173
+ {{ passkeyLabel(passkey) }}
174
+ </p>
175
+ <p class="text-sm text-muted">
176
+ {{ t("passkeys.created", "Added {date}", { date: format.date(passkey.createdAt) }) }}
177
+ </p>
178
178
  </div>
179
179
  <RowMenu
180
180
  :items="passkeyMenu(passkey)"
@@ -195,28 +195,8 @@ const passkeyMenu = (passkey) => [[
195
195
  variant="naked"
196
196
  icon="i-lucide-key-round"
197
197
  :title="t('passkeys.empty', 'You have not added any passkeys yet.')"
198
+ :actions="[{ label: t('passkeys.add', 'Add passkey'), icon: 'i-lucide-plus', onClick: onAddPasskey }]"
198
199
  />
199
-
200
- <template #footer>
201
- <form
202
- class="flex flex-col gap-2 sm:flex-row sm:justify-end"
203
- @submit.prevent="onRegisterPasskey"
204
- >
205
- <UInput
206
- v-model="passkeyName"
207
- :placeholder="t('passkeys.name.placeholder', 'Name, e.g. MacBook')"
208
- :aria-label="t('passkeys.name.label', 'Passkey name')"
209
- maxlength="100"
210
- class="w-full sm:max-w-xs"
211
- />
212
- <UButton
213
- type="submit"
214
- icon="i-lucide-plus"
215
- :loading="passkeyPending"
216
- :label="t('passkeys.register', 'Register a passkey')"
217
- />
218
- </form>
219
- </template>
220
200
  </SectionCard>
221
201
  </div>
222
202
  </template>
@@ -1,19 +1,8 @@
1
- export interface ApiToken {
2
- id: string;
3
- name: string;
4
- prefix: string;
5
- last4: string;
6
- scopes: string[];
7
- expiresAt: string | null;
8
- lastUsedAt: string | null;
9
- createdAt: string;
10
- status: 'ok' | 'revoked' | 'expired';
11
- }
1
+ import type { AuthCreatedTokenDto, AuthTokenDto } from '@frigear-nu/auth/utils';
2
+ export type ApiToken = AuthTokenDto;
12
3
  export interface ApiTokenScope {
13
4
  name: string;
14
5
  description?: string;
15
6
  }
16
7
  /** The response to creating a token: the only time its plaintext `token` exists. */
17
- export type CreatedApiToken = ApiToken & {
18
- token: string;
19
- };
8
+ export type CreatedApiToken = AuthCreatedTokenDto;
@@ -39,14 +39,14 @@
39
39
  "system": "System"
40
40
  },
41
41
  "overview": {
42
- "title": "Konto",
43
- "description": "Dine oplysninger og alt, der hører til din konto.",
44
42
  "shortcuts": "Din konto",
45
43
  "empty": "Her er ikke noget endnu."
46
44
  },
47
45
  "profile": {
48
- "title": "Profil",
49
- "description": "Skift dine brugeroplysninger.",
46
+ "details": {
47
+ "title": "Navn og avatar",
48
+ "description": "Sådan ser andre dig."
49
+ },
50
50
  "name": {
51
51
  "label": "Navn",
52
52
  "description": "Dit fulde navn.",
@@ -54,10 +54,12 @@
54
54
  },
55
55
  "email": {
56
56
  "label": "E-mail",
57
- "description": "Din awesome e-mail.",
57
+ "address": "Adresse",
58
+ "description": "Hvor vi sender beskeder, og hvordan du logger ind.",
58
59
  "change": {
59
60
  "title": "Skift din e-mail",
60
61
  "label": "Ny e-mail",
62
+ "current": "Din adresse er nu {email}.",
61
63
  "sent": {
62
64
  "title": "Tjek din indbakke",
63
65
  "description": "Vi har sendt et link til din nye adresse. Ændringen træder først i kraft når du bekræfter den."
@@ -76,13 +78,13 @@
76
78
  },
77
79
  "save": "Gem profil",
78
80
  "saved": "Profil opdateret",
79
- "saveFailed": "Kunne ikke opdatere din profil.",
80
- "cancel": "Annuller"
81
+ "saveFailed": "Kunne ikke opdatere din profil."
81
82
  },
82
83
  "documents": {
83
84
  "title": "Dokumenter",
84
85
  "description": "Dine dokumenter.",
85
86
  "download": "Download",
87
+ "downloadNamed": "Download {name}",
86
88
  "empty": "Ingen dokumenter til dig endnu."
87
89
  },
88
90
  "security": {
@@ -112,7 +114,7 @@
112
114
  },
113
115
  "passkeys": {
114
116
  "title": "Passkeys",
115
- "register": "Registrér en passkey",
117
+ "add": "Tilføj passkey",
116
118
  "description": "Log ind med fingeraftryk, ansigt eller enhedens pinkode i stedet for et password.",
117
119
  "unnamed": "Passkey",
118
120
  "created": "Tilføjet {date}",
@@ -124,6 +126,7 @@
124
126
  "empty": "Du har ikke tilføjet nogen passkeys endnu.",
125
127
  "name": {
126
128
  "label": "Navn på passkey",
129
+ "description": "Valgfrit. Gør det nemmere at kende dine passkeys fra hinanden.",
127
130
  "placeholder": "Navn, fx MacBook"
128
131
  },
129
132
  "registered": "Passkey tilføjet.",
@@ -134,7 +137,6 @@
134
137
  "tryAgain": "Prøv igen"
135
138
  }
136
139
  },
137
- "signedInAs": "Logget ind som",
138
140
  "apiTokens": {
139
141
  "title": "API-nøgler",
140
142
  "description": "Opret nøgler til scripts og værktøjer.",
@@ -39,14 +39,14 @@
39
39
  "system": "System"
40
40
  },
41
41
  "overview": {
42
- "title": "Account",
43
- "description": "Your details and everything that belongs to your account.",
44
42
  "shortcuts": "Your account",
45
43
  "empty": "Nothing here yet."
46
44
  },
47
45
  "profile": {
48
- "title": "Profile",
49
- "description": "Change your information.",
46
+ "details": {
47
+ "title": "Name and avatar",
48
+ "description": "How you appear to others."
49
+ },
50
50
  "name": {
51
51
  "label": "Name",
52
52
  "description": "Your full name.",
@@ -54,10 +54,12 @@
54
54
  },
55
55
  "email": {
56
56
  "label": "E-mail",
57
- "description": "Your awesome E-mail.",
57
+ "address": "Address",
58
+ "description": "Where we send messages and how you sign in.",
58
59
  "change": {
59
60
  "title": "Change e-mail",
60
61
  "label": "New e-mail",
62
+ "current": "Your address is now {email}.",
61
63
  "sent": {
62
64
  "title": "Check your inbox",
63
65
  "description": "We sent a link to your new address. It takes effect once you confirm it."
@@ -76,13 +78,13 @@
76
78
  },
77
79
  "save": "Save profile",
78
80
  "saved": "Profile updated",
79
- "saveFailed": "Could not update your profile.",
80
- "cancel": "Cancel"
81
+ "saveFailed": "Could not update your profile."
81
82
  },
82
83
  "documents": {
83
84
  "title": "Documents",
84
85
  "description": "Your documents.",
85
86
  "download": "Download",
87
+ "downloadNamed": "Download {name}",
86
88
  "empty": "No documents for you yet."
87
89
  },
88
90
  "security": {
@@ -112,7 +114,7 @@
112
114
  },
113
115
  "passkeys": {
114
116
  "title": "Passkeys",
115
- "register": "Register a passkey",
117
+ "add": "Add passkey",
116
118
  "description": "Sign in with your fingerprint, face or device PIN instead of a password.",
117
119
  "unnamed": "Passkey",
118
120
  "created": "Added {date}",
@@ -124,6 +126,7 @@
124
126
  "empty": "You have not added any passkeys yet.",
125
127
  "name": {
126
128
  "label": "Passkey name",
129
+ "description": "Optional. Helps you tell your passkeys apart.",
127
130
  "placeholder": "Name, e.g. MacBook"
128
131
  },
129
132
  "registered": "Passkey added.",
@@ -134,7 +137,6 @@
134
137
  "tryAgain": "Try again"
135
138
  }
136
139
  },
137
- "signedInAs": "Signed in as",
138
140
  "apiTokens": {
139
141
  "title": "API tokens",
140
142
  "description": "Create tokens for scripts and tools.",