@jskit-ai/users-web 0.2.17 → 0.2.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ - The account settings page and the account-settings pattern's sections sit directly on the page, without bordered sheets, and the section list has no extra border or tint.
6
+ - The account settings page has no in-page title or Back button: it gives the shell's top app bar its title (`useShellPageTitle`) and a Done action (`ShellPageAction`) that returns to `returnTo`, another of the app's origins included, as Back did. Message keys: `jskit.users.accountSettings.back` is removed and `jskit.users.accountSettings.done` ("Done") is new. The pattern's section buttons no longer set a fixed height; they are Material 3's 40dp buttons inside material-web's 48dp touch target.
7
+ - Account settings text uses Vuetify 4's Material 3 type classes (`text-body-medium`, `text-title-large`, ...) instead of Material 2 ones, which Vuetify 4 no longer styles, and the account settings page pattern declares its title and its parent (the home surface's root). Upgrading: see the guide's [Move an app to the Material 3 defaults](https://mobily-enterprises.github.io/jskit-ai/guide/app-setup/material-3-migration).
8
+
3
9
  ## 0.2.0 - 2026-10-29
4
10
 
5
11
  - Translation ready: every user-visible text is a message key with its English in `locales/en.json`, so an app translates it by supplying translation files only. With no translations, texts and behaviour are unchanged. See the guide "Translating a JSKIT app".
@@ -2,10 +2,11 @@ import { createApp, h, reactive, ref, shallowReactive } from "vue";
2
2
  import { createRouter, createWebHistory, RouterView } from "vue-router";
3
3
  import { QueryClient, VueQueryPlugin } from "@tanstack/vue-query";
4
4
  import "vuetify/styles";
5
+ import "@jskit-ai/material-web/client/material.css";
6
+ import { createMaterialVuetifyOptions } from "@jskit-ai/material-web/client";
5
7
  import { createVuetify } from "vuetify";
6
8
  import * as components from "vuetify/components";
7
9
  import * as directives from "vuetify/directives";
8
- import { aliases, mdi } from "vuetify/iconsets/mdi-svg";
9
10
  import { SHELL_WEB_ERROR_RUNTIME_KEY } from "@jskit-ai/shell-web/client/error";
10
11
  import { SHELL_WEB_PLACEMENT_RUNTIME_KEY } from "@jskit-ai/shell-web/client/placement";
11
12
  import AccountSettingsClientElement from "../../src/client/components/AccountSettingsClientElement.vue";
@@ -135,6 +136,6 @@ app.provide(SHELL_WEB_PLACEMENT_RUNTIME_KEY, placement);
135
136
  app.provide(SHELL_WEB_ERROR_RUNTIME_KEY, errorRuntime);
136
137
  app.use(router);
137
138
  app.use(VueQueryPlugin, { queryClient: new QueryClient({ defaultOptions: { queries: { retry: false } } }) });
138
- app.use(createVuetify({ components, directives, icons: { defaultSet: "mdi", aliases, sets: { mdi } } }));
139
+ app.use(createVuetify({ components, directives, ...createMaterialVuetifyOptions() }));
139
140
  await router.isReady();
140
141
  app.mount("#app");
@@ -4,10 +4,11 @@ import { createI18n } from "vue-i18n";
4
4
  import { createRouter, createWebHistory, RouterView } from "vue-router";
5
5
  import { QueryClient, VueQueryPlugin, useQuery } from "@tanstack/vue-query";
6
6
  import "vuetify/styles";
7
+ import "@jskit-ai/material-web/client/material.css";
8
+ import { createMaterialVuetifyOptions } from "@jskit-ai/material-web/client";
7
9
  import { createVuetify } from "vuetify";
8
10
  import * as components from "vuetify/components";
9
11
  import * as directives from "vuetify/directives";
10
- import { aliases, mdi } from "vuetify/iconsets/mdi-svg";
11
12
  import { registerJskitMessages, setJskitI18n } from "@jskit-ai/kernel/client/i18n";
12
13
  import { silentLogger } from "@jskit-ai/kernel/shared/support/silentLogger";
13
14
  import { createHttpClient } from "@jskit-ai/http-runtime/client";
@@ -158,7 +159,7 @@ app.use(i18n);
158
159
  app.use(pinia);
159
160
  app.use(router);
160
161
  app.use(VueQueryPlugin, { queryClient });
161
- app.use(createVuetify({ components, directives, icons: { defaultSet: "mdi", aliases, sets: { mdi } } }));
162
+ app.use(createVuetify({ components, directives, ...createMaterialVuetifyOptions() }));
162
163
 
163
164
  const { shell } = ShellWebClientProvider.setup({ components: componentRegistry, logger: silentLogger, queryClient, router });
164
165
  shell.error.configure({});
package/locales/en.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "accountSettings": {
5
5
  "title": "Account settings",
6
6
  "subtitle": "Global profile, preferences, notifications, and account controls.",
7
- "back": "Back",
7
+ "done": "Done",
8
8
  "loadFailedTitle": "Unable to load account settings",
9
9
  "loadFailed": "Unable to load settings.",
10
10
  "retry": "Retry",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/users-web",
3
- "version": "0.2.17",
3
+ "version": "0.2.19",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test --test-concurrency=1"
@@ -13,15 +13,15 @@
13
13
  "./locales/en.json": "./locales/en.json"
14
14
  },
15
15
  "dependencies": {
16
- "@jskit-ai/uploads-image-web": "0.2.9",
17
- "@jskit-ai/users-core": "0.2.11"
16
+ "@jskit-ai/uploads-image-web": "0.2.10",
17
+ "@jskit-ai/users-core": "0.2.12"
18
18
  },
19
19
  "peerDependencies": {
20
- "@jskit-ai/auth-web": "0.2.12",
21
- "@jskit-ai/http-runtime": "0.2.9",
22
- "@jskit-ai/http-web": "0.2.15",
23
- "@jskit-ai/kernel": "0.2.9",
24
- "@jskit-ai/shell-web": "0.2.10",
20
+ "@jskit-ai/auth-web": "0.2.14",
21
+ "@jskit-ai/http-runtime": "0.2.10",
22
+ "@jskit-ai/http-web": "0.2.17",
23
+ "@jskit-ai/kernel": "0.2.10",
24
+ "@jskit-ai/shell-web": "0.2.12",
25
25
  "@tanstack/vue-query": "^5.90.5",
26
26
  "vue": "^3.5.13",
27
27
  "vue-router": "^5.0.4",
@@ -228,5 +228,8 @@
228
228
  "singleton": true
229
229
  }
230
230
  }
231
+ },
232
+ "devDependencies": {
233
+ "@jskit-ai/material-web": "0.1.1"
231
234
  }
232
235
  }
@@ -10,7 +10,7 @@ const notifications = props.runtime.notifications;
10
10
  </script>
11
11
 
12
12
  <template>
13
- <v-sheet rounded="lg" border class="account-settings-section">
13
+ <div class="account-settings-section">
14
14
  <div class="account-settings-section__body">
15
15
  <v-form @submit.prevent="notifications.submit" novalidate>
16
16
  <v-switch
@@ -47,7 +47,7 @@ const notifications = props.runtime.notifications;
47
47
  </v-btn>
48
48
  </v-form>
49
49
  </div>
50
- </v-sheet>
50
+ </div>
51
51
  </template>
52
52
 
53
53
  <style scoped>
@@ -58,10 +58,4 @@ const notifications = props.runtime.notifications;
58
58
  .account-settings-section__body {
59
59
  padding: 1rem;
60
60
  }
61
-
62
- @media (max-width: 640px) {
63
- .account-settings-section__body :deep(.v-btn) {
64
- min-height: 48px;
65
- }
66
- }
67
61
  </style>
@@ -10,7 +10,7 @@ const preferences = props.runtime.preferences;
10
10
  </script>
11
11
 
12
12
  <template>
13
- <v-sheet rounded="lg" border class="account-settings-section">
13
+ <div class="account-settings-section">
14
14
  <div class="account-settings-section__body">
15
15
  <v-form @submit.prevent="preferences.submit" novalidate>
16
16
  <v-row>
@@ -117,7 +117,7 @@ const preferences = props.runtime.preferences;
117
117
  </v-btn>
118
118
  </v-form>
119
119
  </div>
120
- </v-sheet>
120
+ </div>
121
121
  </template>
122
122
 
123
123
  <style scoped>
@@ -128,10 +128,4 @@ const preferences = props.runtime.preferences;
128
128
  .account-settings-section__body {
129
129
  padding: 1rem;
130
130
  }
131
-
132
- @media (max-width: 640px) {
133
- .account-settings-section__body :deep(.v-btn) {
134
- min-height: 48px;
135
- }
136
- }
137
131
  </style>
@@ -10,16 +10,16 @@ const profile = props.runtime.profile;
10
10
  </script>
11
11
 
12
12
  <template>
13
- <v-sheet rounded="lg" border class="account-settings-section">
13
+ <div class="account-settings-section">
14
14
  <div class="account-settings-section__body">
15
15
  <v-form @submit.prevent="profile.submit" novalidate>
16
16
  <v-row class="mb-2">
17
17
  <v-col cols="12" md="4" class="d-flex flex-column align-center justify-center">
18
18
  <v-avatar :size="profile.avatar.size" color="surface-variant" rounded="circle" class="mb-3">
19
19
  <v-img v-if="profile.avatar.effectiveUrl" :src="profile.avatar.effectiveUrl" cover />
20
- <span v-else class="text-h6">{{ profile.initials.value }}</span>
20
+ <span v-else class="text-title-large">{{ profile.initials.value }}</span>
21
21
  </v-avatar>
22
- <div class="text-caption text-medium-emphasis">Preview size: {{ profile.avatar.size }} px</div>
22
+ <div class="text-body-small on-surface-variant">Preview size: {{ profile.avatar.size }} px</div>
23
23
  </v-col>
24
24
 
25
25
  <v-col cols="12" md="8">
@@ -44,7 +44,7 @@ const profile = props.runtime.profile;
44
44
  </v-btn>
45
45
  </div>
46
46
 
47
- <div v-if="profile.selectedAvatarFileName.value" class="text-caption text-medium-emphasis mb-2">
47
+ <div v-if="profile.selectedAvatarFileName.value" class="text-body-small on-surface-variant mb-2">
48
48
  Selected file: {{ profile.selectedAvatarFileName.value }}
49
49
  </div>
50
50
  </v-col>
@@ -86,7 +86,7 @@ const profile = props.runtime.profile;
86
86
  </v-btn>
87
87
  </v-form>
88
88
  </div>
89
- </v-sheet>
89
+ </div>
90
90
  </template>
91
91
 
92
92
  <style scoped>
@@ -97,10 +97,4 @@ const profile = props.runtime.profile;
97
97
  .account-settings-section__body {
98
98
  padding: 1rem;
99
99
  }
100
-
101
- @media (max-width: 640px) {
102
- .account-settings-section__body :deep(.v-btn) {
103
- min-height: 48px;
104
- }
105
- }
106
100
  </style>
@@ -10,7 +10,7 @@ const signInMethods = props.runtime.signInMethods;
10
10
  </script>
11
11
 
12
12
  <template>
13
- <v-sheet rounded="lg" border class="account-settings-section">
13
+ <div class="account-settings-section">
14
14
  <v-list lines="two" class="py-0">
15
15
  <v-list-item
16
16
  v-for="method in signInMethods.methods.value"
@@ -43,11 +43,11 @@ const signInMethods = props.runtime.signInMethods;
43
43
  </v-list>
44
44
  <p
45
45
  v-if="signInMethods.methods.value.some((method) => method.linked && !method.canUnlink)"
46
- class="account-settings-section__hint text-body-2 text-medium-emphasis mb-0"
46
+ class="account-settings-section__hint text-body-medium on-surface-variant mb-0"
47
47
  >
48
48
  Your only sign-in method can't be unlinked. Set a password or link another provider first.
49
49
  </p>
50
- </v-sheet>
50
+ </div>
51
51
  </template>
52
52
 
53
53
  <style scoped>
@@ -58,10 +58,4 @@ const signInMethods = props.runtime.signInMethods;
58
58
  .account-settings-section__hint {
59
59
  padding: 0 1rem 1rem;
60
60
  }
61
-
62
- @media (max-width: 640px) {
63
- .account-settings-section :deep(.v-btn) {
64
- min-height: 48px;
65
- }
66
- }
67
61
  </style>
@@ -3,6 +3,13 @@
3
3
  "meta": {
4
4
  "guard": {
5
5
  "policy": "authenticated"
6
+ },
7
+ "jskit": {
8
+ "title": "Account settings",
9
+ "parent": {
10
+ "surface": "home",
11
+ "path": "/"
12
+ }
6
13
  }
7
14
  }
8
15
  }
@@ -2,6 +2,9 @@
2
2
  import { computed } from "vue";
3
3
  import { useRoute, useRouter } from "vue-router";
4
4
  import { normalizeOneOf } from "@jskit-ai/kernel/shared/support/normalize";
5
+ import ShellPageAction from "@jskit-ai/shell-web/client/components/ShellPageAction";
6
+ import ShellPageActions from "@jskit-ai/shell-web/client/components/ShellPageActions";
7
+ import { useShellPageTitle } from "@jskit-ai/shell-web/client/composables/useShellPageTitle";
5
8
  import { useAccountSettingsRuntime } from "../composables/useAccountSettingsRuntime.js";
6
9
  import { useAccountSettingsSections } from "../account-settings/sections.js";
7
10
  import { useJskitText } from "../i18n.js";
@@ -11,6 +14,10 @@ const runtime = useAccountSettingsRuntime();
11
14
  const route = useRoute();
12
15
  const router = useRouter();
13
16
  const sections = useAccountSettingsSections();
17
+ // The app bar carries the page's title and its Done action, which returns to where the person came from (returnTo,
18
+ // which may be another of the app's origins).
19
+ useShellPageTitle(() => t("jskit.users.accountSettings.title"));
20
+ const doneTarget = computed(() => runtime.backNavigationTarget.value);
14
21
  const sectionValues = computed(() => Object.freeze(sections.value.map((section) => section.value)));
15
22
  const defaultSection = computed(() => {
16
23
  if (sectionValues.value.includes("profile")) {
@@ -58,23 +65,17 @@ const activeTab = computed({
58
65
 
59
66
  <template>
60
67
  <section class="settings-view py-2 py-md-4">
61
- <v-sheet class="settings-panel" rounded="lg" border>
62
- <header class="settings-panel__header">
63
- <div>
64
- <h1 class="settings-panel__title">{{ t("jskit.users.accountSettings.title") }}</h1>
65
- <p class="text-body-2 text-medium-emphasis mb-0">
66
- {{ t("jskit.users.accountSettings.subtitle") }}
67
- </p>
68
- </div>
69
- <v-btn
70
- variant="text"
71
- color="secondary"
72
- :to="runtime.backNavigationTarget.value.sameOrigin ? runtime.backNavigationTarget.value.href : undefined"
73
- :href="runtime.backNavigationTarget.value.sameOrigin ? undefined : runtime.backNavigationTarget.value.href"
74
- >
75
- {{ t("jskit.users.accountSettings.back") }}
76
- </v-btn>
77
- </header>
68
+ <ShellPageActions>
69
+ <ShellPageAction
70
+ :label="t('jskit.users.accountSettings.done')"
71
+ :to="doneTarget.sameOrigin ? doneTarget.href : undefined"
72
+ :href="doneTarget.sameOrigin ? undefined : doneTarget.href"
73
+ />
74
+ </ShellPageActions>
75
+ <div class="settings-panel">
76
+ <p class="settings-panel__intro text-body-medium on-surface-variant">
77
+ {{ t("jskit.users.accountSettings.subtitle") }}
78
+ </p>
78
79
 
79
80
  <div class="settings-panel__body">
80
81
  <template v-if="runtime.loadingSettings.value">
@@ -82,8 +83,8 @@ const activeTab = computed({
82
83
  <v-skeleton-loader type="text@2, paragraph, button" />
83
84
  </template>
84
85
  <div v-else-if="runtime.settingsLoadError.value" class="settings-panel__state">
85
- <h2 class="text-h6 mb-2">{{ t("jskit.users.accountSettings.loadFailedTitle") }}</h2>
86
- <p class="text-body-2 text-medium-emphasis mb-4">{{ runtime.settingsLoadError.value }}</p>
86
+ <h2 class="text-title-large mb-2">{{ t("jskit.users.accountSettings.loadFailedTitle") }}</h2>
87
+ <p class="text-body-medium on-surface-variant mb-4">{{ runtime.settingsLoadError.value }}</p>
87
88
  <v-btn
88
89
  color="primary"
89
90
  variant="tonal"
@@ -94,19 +95,18 @@ const activeTab = computed({
94
95
  </v-btn>
95
96
  </div>
96
97
  <template v-else-if="sections.length < 1">
97
- <p class="text-body-2 text-medium-emphasis mb-0">{{ t("jskit.users.accountSettings.noSections") }}</p>
98
+ <p class="text-body-medium on-surface-variant mb-0">{{ t("jskit.users.accountSettings.noSections") }}</p>
98
99
  </template>
99
100
  <template v-else>
100
101
  <v-progress-linear v-if="runtime.refreshingSettings.value" indeterminate class="mb-4" />
101
102
  <v-row class="settings-layout" no-gutters>
102
103
  <v-col cols="12" md="3" lg="2" class="pr-md-4 mb-4 mb-md-0">
103
- <v-list nav density="comfortable" class="settings-section-list rounded-lg">
104
+ <v-list nav class="settings-section-list">
104
105
  <v-list-item
105
106
  v-for="section in sections"
106
107
  :key="section.value"
107
108
  :title="text(section.title)"
108
109
  :active="activeTab === section.value"
109
- rounded="lg"
110
110
  @click="activeTab = section.value"
111
111
  />
112
112
  </v-list>
@@ -114,7 +114,13 @@ const activeTab = computed({
114
114
 
115
115
  <v-col cols="12" md="9" lg="10">
116
116
  <v-window v-model="activeTab" :touch="false" class="settings-sections-window">
117
- <v-window-item v-for="section in sections" :key="section.value" :value="section.value">
117
+ <v-window-item
118
+ v-for="section in sections"
119
+ :key="section.value"
120
+ :value="section.value"
121
+ :transition="false"
122
+ :reverse-transition="false"
123
+ >
118
124
  <component
119
125
  :is="section.component"
120
126
  v-bind="section.usesSharedRuntime ? { runtime } : undefined"
@@ -125,32 +131,20 @@ const activeTab = computed({
125
131
  </v-row>
126
132
  </template>
127
133
  </div>
128
- </v-sheet>
134
+ </div>
129
135
  </section>
130
136
  </template>
131
137
 
132
138
  <style scoped>
133
139
  .settings-panel {
134
- background-color: rgb(var(--v-theme-surface));
135
140
  overflow: hidden;
136
141
  }
137
142
 
138
- .settings-panel__header {
139
- align-items: flex-start;
140
- display: flex;
141
- gap: 1rem;
142
- justify-content: space-between;
143
+ .settings-panel__intro {
144
+ margin: 0;
143
145
  padding: 1rem 1rem 0;
144
146
  }
145
147
 
146
- .settings-panel__title {
147
- font-size: clamp(1.35rem, 2vw, 1.85rem);
148
- font-weight: 650;
149
- letter-spacing: -0.02em;
150
- line-height: 1.15;
151
- margin: 0 0 0.35rem;
152
- }
153
-
154
148
  .settings-panel__body {
155
149
  padding: 1rem;
156
150
  }
@@ -161,29 +155,4 @@ const activeTab = computed({
161
155
  padding: 2rem 1rem;
162
156
  text-align: center;
163
157
  }
164
-
165
- .settings-section-list {
166
- border: 1px solid rgba(var(--v-theme-outline), 0.35);
167
- }
168
-
169
- :deep(.settings-section-list .v-list-item--active) {
170
- background-color: rgba(var(--v-theme-primary), 0.14);
171
- }
172
-
173
- :deep(.settings-sections-window .v-window-x-transition-enter-active),
174
- :deep(.settings-sections-window .v-window-x-transition-leave-active),
175
- :deep(.settings-sections-window .v-window-x-reverse-transition-enter-active),
176
- :deep(.settings-sections-window .v-window-x-reverse-transition-leave-active) {
177
- transition: none !important;
178
- }
179
-
180
- @media (max-width: 960px) {
181
- .settings-panel__header {
182
- flex-direction: column;
183
- }
184
-
185
- .settings-panel__header :deep(.v-btn) {
186
- min-height: 48px;
187
- }
188
- }
189
158
  </style>
@@ -9,7 +9,7 @@
9
9
  :data-testid="uiTestIds.card"
10
10
  >
11
11
  <div v-if="resolvedFeatures.header" class="profile-client-panel__header">
12
- <h2 class="text-subtitle-1 mb-0">{{ copyText.title }}</h2>
12
+ <h2 class="text-title-medium mb-0">{{ copyText.title }}</h2>
13
13
  </div>
14
14
  <v-divider v-if="resolvedFeatures.header" />
15
15
  <div class="profile-client-panel__body">
@@ -20,9 +20,9 @@
20
20
  <v-col cols="12" md="4" class="d-flex flex-column align-center justify-center">
21
21
  <v-avatar :size="state.preferencesForm.avatarSize" color="surface-variant" rounded="circle" class="mb-3">
22
22
  <v-img v-if="state.profileAvatar.effectiveUrl" :src="state.profileAvatar.effectiveUrl" cover />
23
- <span v-else class="text-h6">{{ state.profileInitials }}</span>
23
+ <span v-else class="text-title-large">{{ state.profileInitials }}</span>
24
24
  </v-avatar>
25
- <div class="text-caption text-medium-emphasis">
25
+ <div class="text-body-small on-surface-variant">
26
26
  {{ copyText.previewSizePrefix }} {{ state.preferencesForm.avatarSize }} {{ copyText.previewSizeSuffix }}
27
27
  </div>
28
28
  </v-col>
@@ -43,7 +43,7 @@
43
43
  </v-btn>
44
44
  <slot name="avatar-actions-extra" :state="state" :actions="actions" />
45
45
  </div>
46
- <div v-if="state.selectedAvatarFileName" class="text-caption text-medium-emphasis mb-2">
46
+ <div v-if="state.selectedAvatarFileName" class="text-body-small on-surface-variant mb-2">
47
47
  {{ copyText.selectedFilePrefix }} {{ state.selectedAvatarFileName }}
48
48
  </div>
49
49
  </v-col>
@@ -127,7 +127,9 @@ test("the profile section loads, saves, shows a refused field and removes the av
127
127
  const name = page.getByLabel("Display name");
128
128
  await expect(name).toHaveValue("Ada");
129
129
  await expect(page.getByLabel("Email", { exact: true })).toHaveValue("ada@example.com");
130
- await expect(page.getByRole("link", { name: "Back" })).toHaveAttribute("href", "/home");
130
+ // The page's Done action (in the shell's app bar, or in place without one) returns to returnTo; there is no in-page h1.
131
+ await expect(page.getByRole("link", { name: "Done" })).toHaveAttribute("href", "/home");
132
+ await expect(page.getByRole("heading", { level: 1 })).toHaveCount(0);
131
133
 
132
134
  await name.fill(" Ada Lovelace ");
133
135
  await page.getByRole("button", { name: "Save profile" }).click();
@@ -147,7 +149,7 @@ test("the profile section loads, saves, shows a refused field and removes the av
147
149
  await expect(page.getByRole("button", { name: "Remove avatar" })).toHaveCount(0);
148
150
  await expect.poll(() => lastReport(page)).toBe("Avatar removed.");
149
151
 
150
- await page.getByRole("link", { name: "Back" }).click();
152
+ await page.getByRole("link", { name: "Done" }).click();
151
153
  await expect(page.getByTestId("home")).toBeVisible();
152
154
  await finish(api);
153
155
  });
@@ -90,7 +90,10 @@ test("package-owned account settings host is placement-backed rather than scaffo
90
90
  );
91
91
 
92
92
  assert.match(source, /useAccountSettingsSections/);
93
- assert.match(source, /settings-panel__header/);
93
+ // The app bar carries the title and the Done action; the page keeps only its intro.
94
+ assert.match(source, /settings-panel__intro/);
95
+ assert.match(source, /useShellPageTitle/);
96
+ assert.doesNotMatch(source, /<h1/u);
94
97
  assert.doesNotMatch(source, /AccountSettingsProfileSection|AccountSettingsPreferencesSection|AccountSettingsNotificationsSection/);
95
98
  });
96
99