Просмотр исходного кода

Merge branch 'master' into support/148123-replace-tests-with-playwright

Shun Miyazawa 1 год назад
Родитель
Сommit
9e11a721fc
31 измененных файлов с 190 добавлено и 233 удалено
  1. 49 0
      apps/app/playwright/20-basic-features/click-page-icons.spec.ts
  2. 1 2
      apps/app/public/static/locales/en_US/translation.json
  3. 1 2
      apps/app/public/static/locales/fr_FR/translation.json
  4. 1 2
      apps/app/public/static/locales/ja_JP/translation.json
  5. 1 2
      apps/app/public/static/locales/zh_CN/translation.json
  6. 15 0
      apps/app/src/client/util/t-with-opt.ts
  7. 13 2
      apps/app/src/components/InstallerForm.tsx
  8. 13 20
      apps/app/src/components/LoginForm/LoginForm.tsx
  9. 3 1
      apps/app/src/components/SearchPage/SearchPageBase.module.scss
  10. 13 3
      apps/app/src/pages/installer.page.tsx
  11. 3 0
      apps/app/src/pages/login/index.page.tsx
  12. 4 6
      apps/app/src/server/middlewares/login-form-validator.ts
  13. 6 6
      apps/app/src/server/middlewares/register-form-validator.ts
  14. 3 2
      apps/app/src/server/routes/apiv3/index.js
  15. 4 1
      apps/app/src/server/routes/apiv3/installer.ts
  16. 19 0
      apps/app/src/styles/_marker.scss
  17. 0 7
      apps/app/src/styles/_variables.scss
  18. 1 1
      apps/app/src/styles/style-app.scss
  19. 0 176
      apps/app/test/cypress/e2e/20-basic-features/20-basic-features--click-page-icons.cy.ts
  20. 1 0
      apps/slackbot-proxy/tsconfig.json
  21. 3 0
      packages/preset-themes/src/styles/antarctic.scss
  22. 3 0
      packages/preset-themes/src/styles/blackboard.scss
  23. 3 0
      packages/preset-themes/src/styles/classic.scss
  24. 1 0
      packages/preset-themes/src/styles/default.scss
  25. 6 0
      packages/preset-themes/src/styles/fire-red.scss
  26. 3 0
      packages/preset-themes/src/styles/future.scss
  27. 2 0
      packages/preset-themes/src/styles/halloween.scss
  28. 6 0
      packages/preset-themes/src/styles/hufflepuff.scss
  29. 6 0
      packages/preset-themes/src/styles/jade-green.scss
  30. 3 0
      packages/preset-themes/src/styles/nature.scss
  31. 3 0
      packages/preset-themes/src/styles/wood.scss

+ 49 - 0
apps/app/playwright/20-basic-features/click-page-icons.spec.ts

@@ -0,0 +1,49 @@
+import { test, expect } from '@playwright/test';
+
+test('Successfully Subscribe/Unsubscribe a page', async({ page }) => {
+  await page.goto('/Sandbox');
+  const subscribeButton = page.locator('.btn-subscribe');
+
+  // Subscribe
+  await subscribeButton.click();
+  await expect(subscribeButton).toHaveClass(/active/);
+
+  // Unsubscribe
+  await subscribeButton.click();
+  await expect(subscribeButton).not.toHaveClass(/active/);
+});
+
+test('Successfully Like/Unlike a page', async({ page }) => {
+  await page.goto('/Sandbox');
+  const likeButton = page.locator('.btn-like').first();
+
+  // Like
+  await likeButton.click();
+  await expect(likeButton).toHaveClass(/active/);
+
+  // Unlike
+  await likeButton.click();
+  await expect(likeButton).not.toHaveClass(/active/);
+});
+
+test('Successfully Bookmark / Unbookmark a page', async({ page }) => {
+  await page.goto('/Sandbox');
+  const bookmarkButton = page.locator('.btn-bookmark').first();
+
+  // Bookmark
+  await bookmarkButton.click();
+  await expect(bookmarkButton).toHaveClass(/active/);
+
+  // Unbookmark
+  await page.locator('.grw-bookmark-folder-menu-item').click();
+  await expect(bookmarkButton).not.toHaveClass(/active/);
+});
+
+test('Successfully display list of "seen by user"', async({ page }) => {
+  await page.goto('/Sandbox');
+
+  await page.locator('.btn-seen-user').click();
+
+  const imgCount = await page.locator('.user-list-content').locator('img').count();
+  expect(imgCount).toBe(1);
+});

+ 1 - 2
apps/app/public/static/locales/en_US/translation.json

@@ -671,10 +671,9 @@
     "Email format is invalid": "Email format is invalid.",
     "Email format is invalid": "Email format is invalid.",
     "Email field is required": "Email field is required.",
     "Email field is required": "Email field is required.",
     "Password has invalid character": "Password has invalid character.",
     "Password has invalid character": "Password has invalid character.",
-    "Password minimum character should be more than 8 characters": "Password minimum character should be more than 8 characters.",
+    "Password minimum character should be more than n characters": "Password minimum character should be more than {{number}} characters.",
     "Password field is required": "Password field is required.",
     "Password field is required": "Password field is required.",
     "Username or E-mail has invalid characters": "Username or E-mail has invalid characters.",
     "Username or E-mail has invalid characters": "Username or E-mail has invalid characters.",
-    "Password minimum character should be more than 6 characters": "Password minimum character should be more than 6 characters.",
     "user_not_found": "User not found.",
     "user_not_found": "User not found.",
     "provider_duplicated_username_exception": "<p><strong><span class='material-symbols-outlined me-1'>cancel</span>DuplicatedUsernameException occured</strong></p><p class='mb-0'> Your {{ failedProviderForDuplicatedUsernameException }} authentication was succeeded, but a new user could not be created. See the issue <a href='https://github.com/weseek/growi/issues/193'>#193</a>.</p>"
     "provider_duplicated_username_exception": "<p><strong><span class='material-symbols-outlined me-1'>cancel</span>DuplicatedUsernameException occured</strong></p><p class='mb-0'> Your {{ failedProviderForDuplicatedUsernameException }} authentication was succeeded, but a new user could not be created. See the issue <a href='https://github.com/weseek/growi/issues/193'>#193</a>.</p>"
   },
   },

+ 1 - 2
apps/app/public/static/locales/fr_FR/translation.json

@@ -665,10 +665,9 @@
     "Email format is invalid": "Format d'adresse courriel invalide.",
     "Email format is invalid": "Format d'adresse courriel invalide.",
     "Email field is required": "Adresse courriel requise.",
     "Email field is required": "Adresse courriel requise.",
     "Password has invalid character": "Le mot de passe contient des caractères invalides",
     "Password has invalid character": "Le mot de passe contient des caractères invalides",
-    "Password minimum character should be more than 8 characters": "Le mot de passe doit contenir plus de 8 caractères.",
+    "Password minimum character should be more than n characters": "Le mot de passe doit contenir plus de {{number}} caractères.",
     "Password field is required": "Mot de passe requis.",
     "Password field is required": "Mot de passe requis.",
     "Username or E-mail has invalid characters": "Le nom d'utilisateur ou l'adresse courriel contient des caractères invalides",
     "Username or E-mail has invalid characters": "Le nom d'utilisateur ou l'adresse courriel contient des caractères invalides",
-    "Password minimum character should be more than 6 characters": "Le mot de passe doit contenir au moins 6 caractères.",
     "user_not_found": "Utilisateur introuvable.",
     "user_not_found": "Utilisateur introuvable.",
     "provider_duplicated_username_exception": "<p><strong><span class='material-symbols-outlined me-1'>cancel</span>DuplicatedUsernameException</strong></p><p class='mb-0'> L'authentification est réussie pour {{ failedProviderForDuplicatedUsernameException }} , mais la création d'un utilisateur a échouée. Voir <a href='https://github.com/weseek/growi/issues/193'>#193</a>.</p>"
     "provider_duplicated_username_exception": "<p><strong><span class='material-symbols-outlined me-1'>cancel</span>DuplicatedUsernameException</strong></p><p class='mb-0'> L'authentification est réussie pour {{ failedProviderForDuplicatedUsernameException }} , mais la création d'un utilisateur a échouée. Voir <a href='https://github.com/weseek/growi/issues/193'>#193</a>.</p>"
   },
   },

+ 1 - 2
apps/app/public/static/locales/ja_JP/translation.json

@@ -704,10 +704,9 @@
     "Email format is invalid": "メールアドレスのフォーマットが無効です",
     "Email format is invalid": "メールアドレスのフォーマットが無効です",
     "Email field is required": "メールアドレスは必須項目です",
     "Email field is required": "メールアドレスは必須項目です",
     "Password has invalid character": "パスワードに無効な文字があります",
     "Password has invalid character": "パスワードに無効な文字があります",
-    "Password minimum character should be more than 8 characters": "パスワードの最小文字数は8文字以上です",
+    "Password minimum character should be more than n characters": "パスワードの最小文字数は{{number}}文字以上です",
     "Password field is required": "パスワードの欄は必ず入力してください",
     "Password field is required": "パスワードの欄は必ず入力してください",
     "Username or E-mail has invalid characters": "ユーザー名または、メールアドレスに無効な文字があります",
     "Username or E-mail has invalid characters": "ユーザー名または、メールアドレスに無効な文字があります",
-    "Password minimum character should be more than 6 characters": "パスワードの最小文字数は6文字以上です",
     "user_not_found": "ユーザーが見つかりません",
     "user_not_found": "ユーザーが見つかりません",
     "provider_duplicated_username_exception": "<p><strong><span class='material-symbols-outlined me-1'>cancel</span>エラー: DuplicatedUsernameException</strong></p><p class='mb-0'> {{ failedProviderForDuplicatedUsernameException }} 認証は成功しましたが、新しいユーザーを作成できませんでした。詳しくは<a href='https://github.com/weseek/growi/issues/193'>こちら: #193</a>.</p>"
     "provider_duplicated_username_exception": "<p><strong><span class='material-symbols-outlined me-1'>cancel</span>エラー: DuplicatedUsernameException</strong></p><p class='mb-0'> {{ failedProviderForDuplicatedUsernameException }} 認証は成功しましたが、新しいユーザーを作成できませんでした。詳しくは<a href='https://github.com/weseek/growi/issues/193'>こちら: #193</a>.</p>"
   },
   },

+ 1 - 2
apps/app/public/static/locales/zh_CN/translation.json

@@ -674,10 +674,9 @@
     "Email format is invalid": "电子邮件的格式是无效的",
     "Email format is invalid": "电子邮件的格式是无效的",
     "Email field is required": "电子邮件字段是必需的",
     "Email field is required": "电子邮件字段是必需的",
     "Password has invalid character": "密码有无效字符",
     "Password has invalid character": "密码有无效字符",
-    "Password minimum character should be more than 8 characters": "密码最小字符应超过8个字符",
+    "Password minimum character should be more than n characters": "密码最小字符应超过{{number}}个字符",
     "Password field is required": "密码字段是必需的",
     "Password field is required": "密码字段是必需的",
     "Username or E-mail has invalid characters": "用户名或电子邮件有无效的字符",
     "Username or E-mail has invalid characters": "用户名或电子邮件有无效的字符",
-    "Password minimum character should be more than 6 characters": "密码最小字符应超过6个字符",
     "user_not_found": "未找到用户",
     "user_not_found": "未找到用户",
     "provider_duplicated_username_exception": "<p><strong><span class='material-symbols-outlined me-1'>cancel</span>发生了重复用户名异常</strong></p><p class='mb-0'> 你的 {{ failedProviderForDuplicatedUsernameException }} 认证成功了,但不能创建新的用户。参见问题<a href='https://github.com/weseek/growi/issues/193'>#193</a>.</p>"
     "provider_duplicated_username_exception": "<p><strong><span class='material-symbols-outlined me-1'>cancel</span>发生了重复用户名异常</strong></p><p class='mb-0'> 你的 {{ failedProviderForDuplicatedUsernameException }} 认证成功了,但不能创建新的用户。参见问题<a href='https://github.com/weseek/growi/issues/193'>#193</a>.</p>"
   },
   },

+ 15 - 0
apps/app/src/client/util/t-with-opt.ts

@@ -0,0 +1,15 @@
+import { useCallback } from 'react';
+
+import { useTranslation } from 'next-i18next';
+
+export const useTWithOpt = (): (key: string, opt?: any) => string => {
+
+  const { t } = useTranslation();
+
+  return useCallback((key, opt) => {
+    if (typeof opt === 'object') {
+      return t(key, opt).toString();
+    }
+    return t(key);
+  }, [t]);
+};

+ 13 - 2
apps/app/src/components/InstallerForm.tsx

@@ -9,19 +9,29 @@ import { useRouter } from 'next/router';
 import { i18n as i18nConfig } from '^/config/next-i18next.config';
 import { i18n as i18nConfig } from '^/config/next-i18next.config';
 
 
 import { apiv3Post } from '~/client/util/apiv3-client';
 import { apiv3Post } from '~/client/util/apiv3-client';
+import { useTWithOpt } from '~/client/util/t-with-opt';
 import { toastError } from '~/client/util/toastr';
 import { toastError } from '~/client/util/toastr';
 import type { IErrorV3 } from '~/interfaces/errors/v3-error';
 import type { IErrorV3 } from '~/interfaces/errors/v3-error';
 
 
+
 import styles from './InstallerForm.module.scss';
 import styles from './InstallerForm.module.scss';
 
 
+
 const moduleClass = styles['installer-form'] ?? '';
 const moduleClass = styles['installer-form'] ?? '';
 
 
+type Props = {
+  minPasswordLength: number,
+}
 
 
-const InstallerForm = memo((): JSX.Element => {
+const InstallerForm = memo((props: Props): JSX.Element => {
   const { t, i18n } = useTranslation();
   const { t, i18n } = useTranslation();
 
 
+  const { minPasswordLength } = props;
+
   const router = useRouter();
   const router = useRouter();
 
 
+  const tWithOpt = useTWithOpt();
+
   const isSupportedLang = AllLang.includes(i18n.language as Lang);
   const isSupportedLang = AllLang.includes(i18n.language as Lang);
 
 
   const [isValidUserName, setValidUserName] = useState(true);
   const [isValidUserName, setValidUserName] = useState(true);
@@ -113,7 +123,7 @@ const InstallerForm = memo((): JSX.Element => {
             <p className="alert alert-danger text-center">
             <p className="alert alert-danger text-center">
               {registerErrors.map(err => (
               {registerErrors.map(err => (
                 <span>
                 <span>
-                  {t(err.message)}<br />
+                  {tWithOpt(err.message, err.args)}<br />
                 </span>
                 </span>
               ))}
               ))}
             </p>
             </p>
@@ -218,6 +228,7 @@ const InstallerForm = memo((): JSX.Element => {
               <span className="material-symbols-outlined" aria-hidden>lock</span>
               <span className="material-symbols-outlined" aria-hidden>lock</span>
             </label>
             </label>
             <input
             <input
+              minLength={minPasswordLength}
               id="tiPassword"
               id="tiPassword"
               type="password"
               type="password"
               className="form-control rounded"
               className="form-control rounded"

+ 13 - 20
apps/app/src/components/LoginForm/LoginForm.tsx

@@ -9,6 +9,7 @@ import { useRouter } from 'next/router';
 import ReactCardFlip from 'react-card-flip';
 import ReactCardFlip from 'react-card-flip';
 
 
 import { apiv3Post } from '~/client/util/apiv3-client';
 import { apiv3Post } from '~/client/util/apiv3-client';
+import { useTWithOpt } from '~/client/util/t-with-opt';
 import type { IExternalAccountLoginError } from '~/interfaces/errors/external-account-login-error';
 import type { IExternalAccountLoginError } from '~/interfaces/errors/external-account-login-error';
 import { LoginErrorCode } from '~/interfaces/errors/login-error';
 import { LoginErrorCode } from '~/interfaces/errors/login-error';
 import type { IErrorV3 } from '~/interfaces/errors/v3-error';
 import type { IErrorV3 } from '~/interfaces/errors/v3-error';
@@ -21,6 +22,7 @@ import { ExternalAuthButton } from './ExternalAuthButton';
 
 
 import styles from './LoginForm.module.scss';
 import styles from './LoginForm.module.scss';
 
 
+
 const moduleClass = styles['login-form'];
 const moduleClass = styles['login-form'];
 
 
 
 
@@ -38,6 +40,7 @@ type LoginFormProps = {
   enabledExternalAuthType?: IExternalAuthProviderType[],
   enabledExternalAuthType?: IExternalAuthProviderType[],
   isMailerSetup?: boolean,
   isMailerSetup?: boolean,
   externalAccountLoginError?: IExternalAccountLoginError,
   externalAccountLoginError?: IExternalAccountLoginError,
+  minPasswordLength: number,
 }
 }
 export const LoginForm = (props: LoginFormProps): JSX.Element => {
 export const LoginForm = (props: LoginFormProps): JSX.Element => {
   const { t } = useTranslation();
   const { t } = useTranslation();
@@ -46,8 +49,9 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
 
 
   const {
   const {
     isLocalStrategySetup, isLdapStrategySetup, isLdapSetupFailed, isPasswordResetEnabled,
     isLocalStrategySetup, isLdapStrategySetup, isLdapSetupFailed, isPasswordResetEnabled,
-    isEmailAuthenticationEnabled, registrationMode, registrationWhitelist, isMailerSetup, enabledExternalAuthType,
+    isEmailAuthenticationEnabled, registrationMode, registrationWhitelist, isMailerSetup, enabledExternalAuthType, minPasswordLength,
   } = props;
   } = props;
+
   const isLocalOrLdapStrategiesEnabled = isLocalStrategySetup || isLdapStrategySetup;
   const isLocalOrLdapStrategiesEnabled = isLocalStrategySetup || isLdapStrategySetup;
   const isSomeExternalAuthEnabled = enabledExternalAuthType != null && enabledExternalAuthType.length > 0;
   const isSomeExternalAuthEnabled = enabledExternalAuthType != null && enabledExternalAuthType.length > 0;
 
 
@@ -71,6 +75,8 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
 
 
   const isRegistrationEnabled = isLocalStrategySetup && registrationMode !== RegistrationMode.CLOSED;
   const isRegistrationEnabled = isLocalStrategySetup && registrationMode !== RegistrationMode.CLOSED;
 
 
+  const tWithOpt = useTWithOpt();
+
   useEffect(() => {
   useEffect(() => {
     const { hash } = window.location;
     const { hash } = window.location;
     if (hash === '#register') {
     if (hash === '#register') {
@@ -78,13 +84,6 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
     }
     }
   }, []);
   }, []);
 
 
-  const tWithOpt = useCallback((key: string, opt?: any) => {
-    if (typeof opt === 'object') {
-      return t(key, opt).toString();
-    }
-    return t(key);
-  }, [t]);
-
   const resetLoginErrors = useCallback(() => {
   const resetLoginErrors = useCallback(() => {
     if (loginErrors.length === 0) return;
     if (loginErrors.length === 0) return;
     setLoginErrors([]);
     setLoginErrors([]);
@@ -253,15 +252,8 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
       </>
       </>
     );
     );
   }, [
   }, [
-    props,
-    separateErrorsBasedOnErrorCode,
-    loginErrors,
-    generateDangerouslySetErrors,
-    generateSafelySetErrors,
-    isLdapSetupFailed,
-    t,
-    handleLoginWithLocalSubmit,
-    isLoading,
+    props, separateErrorsBasedOnErrorCode, loginErrors, generateDangerouslySetErrors, generateSafelySetErrors,
+    isLdapSetupFailed, t, handleLoginWithLocalSubmit, isLoading,
   ]);
   ]);
 
 
 
 
@@ -360,7 +352,7 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
             <p className="alert alert-danger">
             <p className="alert alert-danger">
               {registerErrors.map(err => (
               {registerErrors.map(err => (
                 <span>
                 <span>
-                  {t(err.message)}<br />
+                  {tWithOpt(err.message, err.args)}<br />
                 </span>
                 </span>
               ))}
               ))}
             </p>
             </p>
@@ -461,6 +453,7 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
                   placeholder={t('Password')}
                   placeholder={t('Password')}
                   name="password"
                   name="password"
                   required
                   required
+                  minLength={minPasswordLength}
                 />
                 />
               </div>
               </div>
             </div>
             </div>
@@ -501,8 +494,8 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
       </React.Fragment>
       </React.Fragment>
     );
     );
   }, [
   }, [
-    t, isEmailAuthenticationEnabled, registrationMode, isMailerSetup, registerErrors, isSuccessToRagistration,
-    emailForRegistrationOrder, props.username, props.name, props.email, registrationWhitelist, switchForm, handleRegisterFormSubmit, isLoading,
+    t, isEmailAuthenticationEnabled, registrationMode, isMailerSetup, registerErrors, isSuccessToRagistration, emailForRegistrationOrder,
+    props.username, props.name, props.email, registrationWhitelist, minPasswordLength, isLoading, switchForm, tWithOpt, handleRegisterFormSubmit,
   ]);
   ]);
 
 
   if (registrationMode === RegistrationMode.RESTRICTED && isSuccessToRagistration && !isEmailAuthenticationEnabled) {
   if (registrationMode === RegistrationMode.RESTRICTED && isSuccessToRagistration && !isEmailAuthenticationEnabled) {

+ 3 - 1
apps/app/src/components/SearchPage/SearchPageBase.module.scss

@@ -9,6 +9,8 @@
 
 
 .search-result-content :global  {
 .search-result-content :global  {
   .highlighted-keyword {
   .highlighted-keyword {
-    background:linear-gradient(transparent 40%, #FCF0C0 40%);
+    background:linear-gradient(
+      transparent 40%,
+      var(--grw-marker-bg, var(--grw-marker-bg-yellow)) 40%);
   }
   }
 }
 }

+ 13 - 3
apps/app/src/pages/installer.page.tsx

@@ -10,6 +10,7 @@ import dynamic from 'next/dynamic';
 import Head from 'next/head';
 import Head from 'next/head';
 
 
 import { NoLoginLayout } from '~/components/Layout/NoLoginLayout';
 import { NoLoginLayout } from '~/components/Layout/NoLoginLayout';
+import type { CrowiRequest } from '~/interfaces/crowi-request';
 
 
 import InstallerForm from '../components/InstallerForm';
 import InstallerForm from '../components/InstallerForm';
 import {
 import {
@@ -31,7 +32,7 @@ async function injectNextI18NextConfigurations(context: GetServerSidePropsContex
 }
 }
 
 
 type Props = CommonProps & {
 type Props = CommonProps & {
-
+  minPasswordLength: number,
   pageWithMetaStr: string,
   pageWithMetaStr: string,
 };
 };
 
 
@@ -43,7 +44,7 @@ const InstallerPage: NextPage<Props> = (props: Props) => {
     return {
     return {
       user_infomation: {
       user_infomation: {
         Icon: () => <span className="material-symbols-outlined me-2">person</span>,
         Icon: () => <span className="material-symbols-outlined me-2">person</span>,
-        Content: InstallerForm,
+        Content: () => <InstallerForm minPasswordLength={props.minPasswordLength} />,
         i18n: t('installer.tab'),
         i18n: t('installer.tab'),
       },
       },
       external_accounts: {
       external_accounts: {
@@ -53,7 +54,7 @@ const InstallerPage: NextPage<Props> = (props: Props) => {
         i18n: tCommons('g2g_data_transfer.tab'),
         i18n: tCommons('g2g_data_transfer.tab'),
       },
       },
     };
     };
-  }, [t, tCommons]);
+  }, [props.minPasswordLength, t, tCommons]);
 
 
   // commons
   // commons
   useAppTitle(props.appTitle);
   useAppTitle(props.appTitle);
@@ -76,6 +77,14 @@ const InstallerPage: NextPage<Props> = (props: Props) => {
   );
   );
 };
 };
 
 
+async function injectServerConfigurations(context: GetServerSidePropsContext, props: Props): Promise<void> {
+  const req: CrowiRequest = context.req as CrowiRequest;
+  const { crowi } = req;
+  const { configManager } = crowi;
+
+  props.minPasswordLength = configManager.getConfig('crowi', 'app:minPasswordLength');
+}
+
 export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
 export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
   const result = await getServerSideCommonProps(context);
   const result = await getServerSideCommonProps(context);
 
 
@@ -88,6 +97,7 @@ export const getServerSideProps: GetServerSideProps = async(context: GetServerSi
   const props: Props = result.props as Props;
   const props: Props = result.props as Props;
 
 
   await injectNextI18NextConfigurations(context, props, ['translation']);
   await injectNextI18NextConfigurations(context, props, ['translation']);
+  injectServerConfigurations(context, props);
 
 
   return {
   return {
     props,
     props,

+ 3 - 0
apps/app/src/pages/login/index.page.tsx

@@ -36,6 +36,7 @@ type Props = CommonProps & {
   isPasswordResetEnabled: boolean,
   isPasswordResetEnabled: boolean,
   isEmailAuthenticationEnabled: boolean,
   isEmailAuthenticationEnabled: boolean,
   externalAccountLoginError?: IExternalAccountLoginError,
   externalAccountLoginError?: IExternalAccountLoginError,
+  minPasswordLength: number,
 };
 };
 
 
 const LoginPage: NextPage<Props> = (props: Props) => {
 const LoginPage: NextPage<Props> = (props: Props) => {
@@ -66,6 +67,7 @@ const LoginPage: NextPage<Props> = (props: Props) => {
         isMailerSetup={props.isMailerSetup}
         isMailerSetup={props.isMailerSetup}
         registrationMode={props.registrationMode}
         registrationMode={props.registrationMode}
         externalAccountLoginError={props.externalAccountLoginError}
         externalAccountLoginError={props.externalAccountLoginError}
+        minPasswordLength={props.minPasswordLength}
       />
       />
     </NoLoginLayout>
     </NoLoginLayout>
   );
   );
@@ -117,6 +119,7 @@ async function injectServerConfigurations(context: GetServerSidePropsContext, pr
   props.registrationWhitelist = configManager.getConfig('crowi', 'security:registrationWhitelist');
   props.registrationWhitelist = configManager.getConfig('crowi', 'security:registrationWhitelist');
   props.isEmailAuthenticationEnabled = configManager.getConfig('crowi', 'security:passport-local:isEmailAuthenticationEnabled');
   props.isEmailAuthenticationEnabled = configManager.getConfig('crowi', 'security:passport-local:isEmailAuthenticationEnabled');
   props.registrationMode = configManager.getConfig('crowi', 'security:registrationMode');
   props.registrationMode = configManager.getConfig('crowi', 'security:registrationMode');
+  props.minPasswordLength = configManager.getConfig('crowi', 'app:minPasswordLength');
 }
 }
 
 
 export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
 export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {

+ 4 - 6
apps/app/src/server/middlewares/login-form-validator.ts

@@ -1,7 +1,7 @@
-import { body, validationResult } from 'express-validator';
-
+import { body, validationResult, type ValidationChain } from 'express-validator';
 // form rules
 // form rules
-export const loginRules = () => {
+export const loginRules = (): ValidationChain[] => {
+
   return [
   return [
     body('loginForm.username')
     body('loginForm.username')
       .matches(/^[\da-zA-Z\-_.+@]+$/)
       .matches(/^[\da-zA-Z\-_.+@]+$/)
@@ -12,8 +12,6 @@ export const loginRules = () => {
     body('loginForm.password')
     body('loginForm.password')
       .matches(/^[\x20-\x7F]*$/)
       .matches(/^[\x20-\x7F]*$/)
       .withMessage('message.Password has invalid character')
       .withMessage('message.Password has invalid character')
-      .isLength({ min: 6 })
-      .withMessage('message.Password minimum character should be more than 6 characters')
       .not()
       .not()
       .isEmpty()
       .isEmpty()
       .withMessage('message.Password field is required'),
       .withMessage('message.Password field is required'),
@@ -21,7 +19,7 @@ export const loginRules = () => {
 };
 };
 
 
 // validation action
 // validation action
-export const loginValidation = (req, res, next) => {
+export const loginValidation = (req, res, next): ValidationChain[] => {
   const form = req.body;
   const form = req.body;
 
 
   const errors = validationResult(req);
   const errors = validationResult(req);

+ 6 - 6
apps/app/src/server/middlewares/register-form-validator.ts

@@ -1,8 +1,8 @@
-import { body, validationResult } from 'express-validator';
+import { ErrorV3 } from '@growi/core/dist/models';
+import { body, validationResult, type ValidationChain } from 'express-validator';
 
 
-const PASSOWRD_MINIMUM_NUMBER = 8;
 // form rules
 // form rules
-export const registerRules = () => {
+export const registerRules = (minPasswordLength: number): ValidationChain[] => {
   return [
   return [
     body('registerForm.username')
     body('registerForm.username')
       .matches(/^[\da-zA-Z\-_.]+$/)
       .matches(/^[\da-zA-Z\-_.]+$/)
@@ -19,8 +19,8 @@ export const registerRules = () => {
     body('registerForm.password')
     body('registerForm.password')
       .matches(/^[\x20-\x7F]*$/)
       .matches(/^[\x20-\x7F]*$/)
       .withMessage('message.Password has invalid character')
       .withMessage('message.Password has invalid character')
-      .isLength({ min: PASSOWRD_MINIMUM_NUMBER })
-      .withMessage('message.Password minimum character should be more than 8 characters')
+      .isLength({ min: minPasswordLength })
+      .withMessage(new ErrorV3('message.Password minimum character should be more than n characters', undefined, undefined, { number: minPasswordLength }))
       .not()
       .not()
       .isEmpty()
       .isEmpty()
       .withMessage('message.Password field is required'),
       .withMessage('message.Password field is required'),
@@ -29,7 +29,7 @@ export const registerRules = () => {
 };
 };
 
 
 // validation action
 // validation action
-export const registerValidation = (req, res, next) => {
+export const registerValidation = (req, res, next): ValidationChain[] => {
   const form = req.body;
   const form = req.body;
 
 
   const errors = validationResult(req);
   const errors = validationResult(req);

+ 3 - 2
apps/app/src/server/routes/apiv3/index.js

@@ -22,6 +22,7 @@ const routerForAuth = express.Router();
 
 
 module.exports = (crowi, app) => {
 module.exports = (crowi, app) => {
   const isInstalled = crowi.configManager.getConfig('crowi', 'app:installed');
   const isInstalled = crowi.configManager.getConfig('crowi', 'app:installed');
+  const minPasswordLength = crowi.configManager.getConfig('crowi', 'app:minPasswordLength');
 
 
   // add custom functions to express response
   // add custom functions to express response
   require('./response')(express, crowi);
   require('./response')(express, crowi);
@@ -62,9 +63,9 @@ module.exports = (crowi, app) => {
   routerForAuth.use('/logout', require('./logout')(crowi));
   routerForAuth.use('/logout', require('./logout')(crowi));
 
 
   routerForAuth.post('/register',
   routerForAuth.post('/register',
-    applicationInstalled, registerFormValidator.registerRules(), registerFormValidator.registerValidation, addActivity, login.register);
+    applicationInstalled, registerFormValidator.registerRules(minPasswordLength), registerFormValidator.registerValidation, addActivity, login.register);
 
 
-  routerForAuth.post('/user-activation/register', applicationInstalled, userActivation.registerRules(),
+  routerForAuth.post('/user-activation/register', applicationInstalled, userActivation.registerRules(minPasswordLength),
     userActivation.validateRegisterForm, userActivation.registerAction(crowi));
     userActivation.validateRegisterForm, userActivation.registerAction(crowi));
 
 
   // installer
   // installer

+ 4 - 1
apps/app/src/server/routes/apiv3/installer.ts

@@ -3,6 +3,7 @@ import type { Request, Router } from 'express';
 import express from 'express';
 import express from 'express';
 
 
 import { SupportedAction } from '~/interfaces/activity';
 import { SupportedAction } from '~/interfaces/activity';
+import { configManager } from '~/server/service/config-manager';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 import type Crowi from '../../crowi';
 import type Crowi from '../../crowi';
@@ -26,8 +27,10 @@ module.exports = (crowi: Crowi): Router => {
 
 
   const router = express.Router();
   const router = express.Router();
 
 
+  const minPasswordLength = configManager.getConfig('crowi', 'app:minPasswordLength');
+
   // eslint-disable-next-line max-len
   // eslint-disable-next-line max-len
-  router.post('/', registerRules(), registerValidation, addActivity, async(req: FormRequest, res: ApiV3Response) => {
+  router.post('/', registerRules(minPasswordLength), registerValidation, addActivity, async(req: FormRequest, res: ApiV3Response) => {
     const appService = crowi.appService;
     const appService = crowi.appService;
     if (appService == null) {
     if (appService == null) {
       return res.apiv3Err(new ErrorV3('GROWI cannot be installed due to an internal error', 'app_service_not_setup'), 500);
       return res.apiv3Err(new ErrorV3('GROWI cannot be installed due to an internal error', 'app_service_not_setup'), 500);

+ 19 - 0
apps/app/src/styles/_marker.scss

@@ -0,0 +1,19 @@
+@use '@growi/core-styles/scss/bootstrap/init' as bs;
+
+// Light mode color
+@include bs.color-mode(light) {
+  --grw-marker-bg-yellow: #FFFA90;
+  --grw-marker-bg-red: #FFAADD;
+  --grw-marker-bg-blue: #9AE0FF;
+  --grw-marker-bg-cyan: #88FFF0;
+  --grw-marker-bg-green: #B8FF9A;
+}
+
+// dark mode color
+@include bs.color-mode(dark) {
+  --grw-marker-bg-yellow: #888000;
+  --grw-marker-bg-red: #900066;
+  --grw-marker-bg-blue: #0A6A9A;
+  --grw-marker-bg-cyan: #008888;
+  --grw-marker-bg-green: #007000;
+}

+ 0 - 7
apps/app/src/styles/_variables.scss

@@ -1,10 +1,3 @@
-// == Marker Color
-$grw-marker-yellow: #ff6;
-$grw-marker-red: #f6c;
-$grw-marker-blue: #6cf;
-$grw-marker-cyan: #6ff;
-$grw-marker-green: #6f6;
-
 // == Layout
 // == Layout
 $grw-sidebar-nav-width: 48px;
 $grw-sidebar-nav-width: 48px;
 $grw-navbar-bottom-height: 62px;
 $grw-navbar-bottom-height: 62px;

+ 1 - 1
apps/app/src/styles/style-app.scss

@@ -25,7 +25,7 @@
 @import 'mirror_mode';
 @import 'mirror_mode';
 @import 'modal';
 @import 'modal';
 @import 'share-link';
 @import 'share-link';
-
+@import 'marker';
 
 
 /*
 /*
  * for Guest User Mode
  * for Guest User Mode

+ 0 - 176
apps/app/test/cypress/e2e/20-basic-features/20-basic-features--click-page-icons.cy.ts

@@ -1,176 +0,0 @@
-context('Click page icons button', () => {
-  const ssPrefix = 'click-page-icon-';
-
-  beforeEach(() => {
-    // login
-    cy.fixture("user-admin.json").then(user => {
-      cy.login(user.username, user.password);
-    });
-  });
-
-  it('Successfully subscribe/unsubscribe a page', () => {
-    cy.visit('/Sandbox');
-    cy.collapseSidebar(true, true);
-
-    // Subscribe
-    cy.get('#subscribe-button').click({force: true});
-    cy.get('#subscribe-button').should('have.class', 'active');
-
-    // position of the element is not fixed to be displayed, so the element is removed
-    cy.get('body').then($body => {
-      if ($body.find('[data-testid="subscribe-button-tooltip"]').length > 0) {
-        cy.getByTestid('subscribe-button-tooltip').invoke('remove');
-      }
-    })
-    cy.getByTestid('subscribe-button-tooltip').should('not.exist');
-
-    cy.waitUntilSkeletonDisappear();
-    cy.getByTestid('grw-contextual-sub-nav').within(() => { cy.screenshot(`${ssPrefix}1-subscribe-page`) })
-
-    // Unsubscribe
-    cy.get('#subscribe-button').click({force: true});
-    cy.get('#subscribe-button').should('not.have.class', 'active');
-
-    // position of the element is not fixed to be displayed, so the element is removed
-    cy.get('body').then($body => {
-      if ($body.find('[data-testid="subscribe-button-tooltip"]').length > 0) {
-        cy.getByTestid('subscribe-button-tooltip').invoke('remove');
-      }
-    })
-    cy.getByTestid('subscribe-button-tooltip').should('not.exist');
-
-    cy.waitUntilSkeletonDisappear();
-    cy.getByTestid('grw-contextual-sub-nav').within(() => { cy.screenshot(`${ssPrefix}2-unsubscribe-page`) })
-  });
-
-  it('Successfully Like / Dislike a page', () => {
-    cy.visit('/Sandbox');
-    cy.collapseSidebar(true);
-
-    // like
-    cy.get('#like-button').click({force: true});
-    cy.get('#like-button').should('have.class', 'active');
-
-    // position of the element is not fixed to be displayed, so the element is removed
-    cy.get('body').then($body => {
-      if ($body.find('[data-testid="like-button-tooltip"]').length > 0) {
-        cy.getByTestid('like-button-tooltip').invoke('remove');
-      }
-    })
-    cy.getByTestid('like-button-tooltip').should('not.exist');
-
-    cy.waitUntilSpinnerDisappear();
-    cy.getByTestid('grw-contextual-sub-nav').within(() => { cy.screenshot(`${ssPrefix}3-like-page`) });
-
-    // total liker (user-list-popover is commented out because it is sometimes displayed and sometimes not.)
-    // cy.get('#po-total-likes').click({force: true});
-    // cy.get('.user-list-popover').should('be.visible')
-    // cy.getByTestid('grw-contextual-sub-nav').within(() => { cy.screenshot(`${ssPrefix}4-likes-counter`) });
-
-    // unlike
-    cy.get('#like-button').click({force: true});
-    cy.get('#like-button').should('not.have.class', 'active');
-
-    // position of the element is not fixed to be displayed, so the element is removed
-    cy.get('body').then($body => {
-      if ($body.find('[data-testid="like-button-tooltip"]').length > 0) {
-        cy.getByTestid('like-button-tooltip').invoke('remove');
-      }
-    })
-    cy.getByTestid('like-button-tooltip').should('not.exist');
-
-    cy.waitUntilSpinnerDisappear();
-    cy.getByTestid('grw-contextual-sub-nav').within(() => { cy.screenshot(`${ssPrefix}5-dislike-page`) });
-
-    // total liker (user-list-popover is commented out because it is sometimes displayed and sometimes not.)
-    // cy.get('#po-total-likes').click({force: true});
-    // cy.get('.user-list-popover').should('be.visible');
-    // cy.getByTestid('grw-contextual-sub-nav').within(() => { cy.screenshot(`${ssPrefix}6-likes-counter`) });
-  });
-
-  it('Successfully Bookmark / Unbookmark a page', () => {
-    cy.visit('/Sandbox');
-    cy.collapseSidebar(true);
-
-    // bookmark
-    cy.get('#bookmark-dropdown-btn').click({force: true});
-    cy.get('#bookmark-dropdown-btn').should('have.class', 'active');
-
-    // position of the element is not fixed to be displayed, so the element is removed
-    cy.get('body').then($body => {
-      if ($body.find('[data-testid="bookmark-button-tooltip"]').length > 0) {
-        cy.getByTestid('bookmark-button-tooltip').invoke('remove');
-      }
-    })
-    cy.getByTestid('bookmark-button-tooltip').should('not.exist');
-
-    cy.waitUntilSpinnerDisappear();
-    cy.getByTestid('grw-contextual-sub-nav').within(() => { cy.screenshot(`${ssPrefix}7-bookmark-page`) });
-
-    // total bookmarker
-    cy.waitUntil(() => {
-      // do
-      cy.get('#po-total-bookmarks').click({force: true});
-      // wait until
-      return cy.get('body').within(() => {
-        return Cypress.$('.user-list-popover').is(':visible');
-      });
-    });
-    cy.waitUntilSpinnerDisappear();
-    cy.getByTestid('grw-contextual-sub-nav').within(() => { cy.screenshot(`${ssPrefix}8-bookmarks-counter`) });
-
-    // unbookmark
-    cy.get('#bookmark-dropdown-btn').click({force: true});
-    cy.get('.grw-bookmark-folder-menu').should('be.visible');
-    cy.get('.grw-bookmark-folder-menu-item').first().click({force: true});
-    cy.get('#bookmark-dropdown-btn').should('not.have.class', 'active');
-
-    // position of the element is not fixed to be displayed, so the element is removed
-    cy.get('body').then($body => {
-      if ($body.find('[data-testid="bookmark-button-tooltip"]').length > 0) {
-        cy.getByTestid('bookmark-button-tooltip').invoke('remove');
-      }
-    })
-    cy.getByTestid('bookmark-button-tooltip').should('not.exist');
-
-    cy.waitUntilSpinnerDisappear();
-    cy.getByTestid('grw-contextual-sub-nav').within(() => { cy.screenshot(`${ssPrefix}9-unbookmark-page`) });
-
-    // total bookmarker
-    cy.waitUntil(() => {
-      // do
-      cy.get('#po-total-bookmarks').click({force: true});
-      // wait until
-      return cy.get('body').within(() => {
-        return Cypress.$('.user-list-popover').is(':visible');
-      });
-    });
-    cy.waitUntilSpinnerDisappear();
-    cy.getByTestid('grw-contextual-sub-nav').within(() => { cy.screenshot(`${ssPrefix}10-bookmarks-counter`) });
-  });
-
-  // user-list-popover is commented out because it is sometimes displayed and sometimes not
-  // it('Successfully display list of "seen by user"', () => {
-  //   cy.visit('/Sandbox');
-  //   cy.waitUntilSkeletonDisappear();
-
-  //   cy.getByTestid('grw-contextual-sub-nav').within(() => {
-  //     cy.get('div.grw-seen-user-info').find('button#btn-seen-user').click({force: true});
-  //   });
-
-  //   // position of the element is not fixed to be displayed, so the element is removed
-  //   cy.get('body').then($body => {
-  //     if ($body.find('[data-testid="seen-user-info-tooltip"]').length > 0) {
-  //       cy.getByTestid('seen-user-info-tooltip').invoke('remove');
-  //     }
-  //   })
-  //   cy.getByTestid('seen-user-info-tooltip').should('not.exist');
-
-  //   cy.get('.user-list-popover').should('be.visible')
-
-  //   cy.getByTestid('grw-contextual-sub-nav').within(() => {
-  //     cy.screenshot(`${ssPrefix}11-seen-user-list`);
-  //   });
-  // });
-
-});

+ 1 - 0
apps/slackbot-proxy/tsconfig.json

@@ -3,6 +3,7 @@
   "extends": "../../tsconfig.base.json",
   "extends": "../../tsconfig.base.json",
   "compilerOptions": {
   "compilerOptions": {
     "module": "CommonJS",
     "module": "CommonJS",
+    "moduleResolution": "Node",
 
 
     "baseUrl": ".",
     "baseUrl": ".",
     "paths": {
     "paths": {

+ 3 - 0
packages/preset-themes/src/styles/antarctic.scss

@@ -29,6 +29,9 @@
   --grw-wiki-link-hover-color-rgb: var(--grw-primary-600-rgb);
   --grw-wiki-link-hover-color-rgb: var(--grw-primary-600-rgb);
   --grw-sidebar-nav-btn-color: var(--grw-highlight-600);
   --grw-sidebar-nav-btn-color: var(--grw-highlight-600);
 
 
+  // change marker color
+  --grw-marker-bg: var(--grw-marker-bg-cyan);
+
   &, body {
   &, body {
     background-image: url('../images/antarctic/antarctic.svg');
     background-image: url('../images/antarctic/antarctic.svg');
     background-attachment: fixed;
     background-attachment: fixed;

+ 3 - 0
packages/preset-themes/src/styles/blackboard.scss

@@ -28,6 +28,9 @@
   --grw-wiki-link-color-rgb: var(--grw-primary-400-rgb);
   --grw-wiki-link-color-rgb: var(--grw-primary-400-rgb);
   --grw-wiki-link-hover-color-rgb: var(--grw-primary-300-rgb);
   --grw-wiki-link-hover-color-rgb: var(--grw-primary-300-rgb);
 
 
+  // change marker color
+  --grw-marker-bg: var(--grw-marker-bg-red);
+
   &, body {
   &, body {
     background-image: url('../images/blackboard/blackboard.png');
     background-image: url('../images/blackboard/blackboard.png');
     background-attachment: fixed;
     background-attachment: fixed;

+ 3 - 0
packages/preset-themes/src/styles/classic.scss

@@ -54,4 +54,7 @@
   @import '@growi/core-styles/scss/bootstrap/theming/apply-dark';
   @import '@growi/core-styles/scss/bootstrap/theming/apply-dark';
 
 
   --grw-sidebar-nav-btn-color: rgba(var(--grw-highlight-400-rgb), 0.8);
   --grw-sidebar-nav-btn-color: rgba(var(--grw-highlight-400-rgb), 0.8);
+
+  // change marker color
+  --grw-marker-bg: var(--grw-marker-bg-red);
 }
 }

+ 1 - 0
packages/preset-themes/src/styles/default.scss

@@ -58,4 +58,5 @@
   --grw-wiki-link-color-rgb: var(--grw-highlight-600-rgb);
   --grw-wiki-link-color-rgb: var(--grw-highlight-600-rgb);
   --grw-wiki-link-hover-color-rgb: var(--grw-highlight-400-rgb);
   --grw-wiki-link-hover-color-rgb: var(--grw-highlight-400-rgb);
   --grw-sidebar-nav-btn-color: rgba(var(--grw-highlight-400-rgb), 0.8);
   --grw-sidebar-nav-btn-color: rgba(var(--grw-highlight-400-rgb), 0.8);
+
 }
 }

+ 6 - 0
packages/preset-themes/src/styles/fire-red.scss

@@ -27,6 +27,9 @@
 
 
   --grw-wiki-link-color-rgb: var(--grw-primary-500-rgb);
   --grw-wiki-link-color-rgb: var(--grw-primary-500-rgb);
   --grw-wiki-link-hover-color-rgb: var(--grw-primary-600-rgb);
   --grw-wiki-link-hover-color-rgb: var(--grw-primary-600-rgb);
+
+  // change marker color
+  --grw-marker-bg: var(--grw-marker-bg-cyan);
 }
 }
 
 
 :root[data-bs-theme='dark'] {
 :root[data-bs-theme='dark'] {
@@ -56,4 +59,7 @@
 
 
   --grw-wiki-link-color-rgb: var(--grw-primary-500-rgb);
   --grw-wiki-link-color-rgb: var(--grw-primary-500-rgb);
   --grw-wiki-link-hover-color-rgb: var(--grw-primary-400-rgb);
   --grw-wiki-link-hover-color-rgb: var(--grw-primary-400-rgb);
+
+    // change marker color
+    --grw-marker-bg: var(--grw-marker-bg-cyan);
 }
 }

+ 3 - 0
packages/preset-themes/src/styles/future.scss

@@ -27,4 +27,7 @@
 
 
   --grw-wiki-link-color-rgb: var(--grw-primary-400-rgb);
   --grw-wiki-link-color-rgb: var(--grw-primary-400-rgb);
   --grw-wiki-link-hover-color-rgb: var(--grw-primary-300-rgb);
   --grw-wiki-link-hover-color-rgb: var(--grw-primary-300-rgb);
+
+  // change marker color
+  --grw-marker-bg: var(--grw-marker-bg-cyan);
 }
 }

+ 2 - 0
packages/preset-themes/src/styles/halloween.scss

@@ -39,4 +39,6 @@
   --grw-wiki-link-color-rgb: var(--grw-primary-400-rgb);
   --grw-wiki-link-color-rgb: var(--grw-primary-400-rgb);
   --grw-wiki-link-hover-color-rgb: var(--grw-primary-300-rgb);
   --grw-wiki-link-hover-color-rgb: var(--grw-primary-300-rgb);
 
 
+  // change marker color
+  --grw-marker-bg: var(--grw-marker-bg-red);
 }
 }

+ 6 - 0
packages/preset-themes/src/styles/hufflepuff.scss

@@ -28,6 +28,9 @@
   --grw-wiki-link-color-rgb: var(--grw-primary-600-rgb);
   --grw-wiki-link-color-rgb: var(--grw-primary-600-rgb);
   --grw-wiki-link-hover-color-rgb: var(--grw-primary-800-rgb);
   --grw-wiki-link-hover-color-rgb: var(--grw-primary-800-rgb);
 
 
+  // change marker color
+  --grw-marker-bg: var(--grw-marker-bg-cyan);
+
   &, body {
   &, body {
     background-image: url('../images/hufflepuff/hufflepuff-light-bg.svg');
     background-image: url('../images/hufflepuff/hufflepuff-light-bg.svg');
     background-attachment: fixed;
     background-attachment: fixed;
@@ -65,6 +68,9 @@
   --grw-wiki-link-color-rgb: var(--grw-primary-500-rgb);
   --grw-wiki-link-color-rgb: var(--grw-primary-500-rgb);
   --grw-wiki-link-hover-color-rgb: var(--grw-primary-400-rgb);
   --grw-wiki-link-hover-color-rgb: var(--grw-primary-400-rgb);
 
 
+  // change marker color
+  --grw-marker-bg: var(--grw-marker-bg-cyan);
+
   &, body {
   &, body {
     background-image: url('../images/hufflepuff/hufflepuff-dark-bg.svg');
     background-image: url('../images/hufflepuff/hufflepuff-dark-bg.svg');
     background-attachment: fixed;
     background-attachment: fixed;

+ 6 - 0
packages/preset-themes/src/styles/jade-green.scss

@@ -29,6 +29,9 @@ $min-contrast-ratio: 2;
 
 
   --grw-wiki-link-color-rgb: var(--grw-primary-700-rgb);
   --grw-wiki-link-color-rgb: var(--grw-primary-700-rgb);
   --grw-wiki-link-hover-color-rgb: var(--grw-primary-800-rgb);
   --grw-wiki-link-hover-color-rgb: var(--grw-primary-800-rgb);
+
+    // change marker color
+    --grw-marker-bg: var(--grw-marker-bg-red);
 }
 }
 
 
 :root[data-bs-theme='dark'] {
 :root[data-bs-theme='dark'] {
@@ -58,4 +61,7 @@ $min-contrast-ratio: 2;
 
 
   --grw-wiki-link-color-rgb: var(--grw-primary-500-rgb);
   --grw-wiki-link-color-rgb: var(--grw-primary-500-rgb);
   --grw-wiki-link-hover-color-rgb: var(--grw-primary-400-rgb);
   --grw-wiki-link-hover-color-rgb: var(--grw-primary-400-rgb);
+
+  // change marker color
+  --grw-marker-bg: var(--grw-marker-bg-red);
 }
 }

+ 3 - 0
packages/preset-themes/src/styles/nature.scss

@@ -27,4 +27,7 @@
 
 
   --grw-wiki-link-color-rgb: var(--grw-primary-600-rgb);
   --grw-wiki-link-color-rgb: var(--grw-primary-600-rgb);
   --grw-wiki-link-hover-color-rgb: var(--grw-primary-700-rgb);
   --grw-wiki-link-hover-color-rgb: var(--grw-primary-700-rgb);
+
+  // change marker color
+  --grw-marker-bg: var(--grw-marker-bg-blue);
 }
 }

+ 3 - 0
packages/preset-themes/src/styles/wood.scss

@@ -28,6 +28,9 @@
   --grw-wiki-link-color-rgb: var(--grw-primary-500-rgb);
   --grw-wiki-link-color-rgb: var(--grw-primary-500-rgb);
   --grw-wiki-link-hover-color-rgb: var(--grw-primary-600-rgb);
   --grw-wiki-link-hover-color-rgb: var(--grw-primary-600-rgb);
 
 
+  // change marker color
+  --grw-marker-bg: var(--grw-marker-bg-green);
+
   &, body {
   &, body {
     background-image: url('../images/wood/wood.svg');
     background-image: url('../images/wood/wood.svg');
   }
   }