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

Merge branch 'dev/7.0.x' into support/138271-customtheme-spring

satof3 2 лет назад
Родитель
Сommit
b65d723c5c
25 измененных файлов с 948 добавлено и 845 удалено
  1. 21 100
      apps/app/src/components/PageEditor/PageEditor.tsx
  2. 4 8
      apps/app/src/components/PageEditor/Preview.tsx
  3. 0 206
      apps/app/src/components/PageEditor/ScrollSyncHelper.js
  4. 152 0
      apps/app/src/components/PageEditor/ScrollSyncHelper.ts
  5. 1 1
      apps/app/src/components/PageList/PageListItemL.tsx
  6. 9 4
      apps/app/src/components/Sidebar/PageCreateButton/PageCreateButton.tsx
  7. 14 5
      apps/app/src/components/Sidebar/PageCreateButton/hooks.tsx
  8. 7 3
      apps/app/src/components/Sidebar/Sidebar.tsx
  9. 1 0
      apps/app/src/components/Sidebar/SidebarHead/ToggleCollapseButton.tsx
  10. 7 3
      apps/app/src/server/routes/apiv3/pages.js
  11. 18 19
      apps/app/test/cypress/e2e/50-sidebar/50-sidebar--access-to-side-bar.cy.ts
  12. 39 10
      apps/app/test/cypress/e2e/50-sidebar/50-sidebar--switching-sidebar-mode.cy.ts
  13. 6 6
      apps/app/test/cypress/e2e/60-home/60-home--home.cy.ts
  14. 1 1
      apps/app/test/cypress/support/commands.ts
  15. 20 0
      packages/editor/src/components/CodeMirrorEditor/CodeMirrorEditor.tsx
  16. 9 3
      packages/editor/src/components/CodeMirrorEditorMain.tsx
  17. 1 0
      packages/editor/src/services/codemirror-editor/use-codemirror-editor/use-codemirror-editor.ts
  18. 48 0
      packages/editor/src/services/extensions/setDataLine.ts
  19. BIN
      packages/preset-themes/public/images/wood/wood-navbar.jpg
  20. BIN
      packages/preset-themes/public/images/wood/wood.jpg
  21. 1 0
      packages/preset-themes/public/images/wood/wood.svg
  22. 201 161
      packages/preset-themes/src/styles/island.scss
  23. 158 123
      packages/preset-themes/src/styles/nature.scss
  24. 227 188
      packages/preset-themes/src/styles/wood.scss
  25. 3 4
      packages/preset-themes/vite.themes.config.ts

+ 21 - 100
apps/app/src/components/PageEditor/PageEditor.tsx

@@ -60,7 +60,7 @@ import loggerFactory from '~/utils/logger';
 // import Editor from './Editor';
 import EditorNavbarBottom from './EditorNavbarBottom';
 import Preview from './Preview';
-import scrollSyncHelper from './ScrollSyncHelper';
+import { scrollEditor, scrollPreview } from './ScrollSyncHelper';
 
 import '@growi/editor/dist/style.css';
 
@@ -73,13 +73,10 @@ declare global {
   var globalEmitter: EventEmitter;
 }
 
-
 // for scrolling
-let lastScrolledDateWithCursor: Date | null = null;
 let isOriginOfScrollSyncEditor = false;
 let isOriginOfScrollSyncPreview = false;
 
-
 type Props = {
   visibility?: boolean,
 }
@@ -356,97 +353,38 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
     return AcceptedUploadFileType.IMAGE;
   }, [isUploadAllFileAllowed, isUploadEnabled]);
 
-  const scrollPreviewByEditorLine = useCallback((line: number) => {
-    if (previewRef.current == null) {
+
+  const scrollEditorHandler = useCallback(() => {
+    if (codeMirrorEditor?.view?.scrollDOM == null || previewRef.current == null) {
       return;
     }
 
-    // prevent circular invocation
     if (isOriginOfScrollSyncPreview) {
-      isOriginOfScrollSyncPreview = false; // turn off the flag
+      isOriginOfScrollSyncPreview = false;
       return;
     }
 
-    // turn on the flag
     isOriginOfScrollSyncEditor = true;
-    scrollSyncHelper.scrollPreview(previewRef.current, line);
-  }, []);
-  const scrollPreviewByEditorLineWithThrottle = useMemo(() => throttle(20, scrollPreviewByEditorLine), [scrollPreviewByEditorLine]);
-
-  /**
-   * the scroll event handler from codemirror
-   * @param {any} data {left, top, width, height, clientWidth, clientHeight} object that represents the current scroll position,
-   *                    the size of the scrollable area, and the size of the visible area (minus scrollbars).
-   *                    And data.line is also available that is added by Editor component
-   * @see https://codemirror.net/doc/manual.html#events
-   */
-  const editorScrolledHandler = useCallback(({ line }: { line: number }) => {
-    // prevent scrolling
-    //   if the elapsed time from last scroll with cursor is shorter than 40ms
-    const now = new Date();
-    if (lastScrolledDateWithCursor != null && now.getTime() - lastScrolledDateWithCursor.getTime() < 40) {
-      return;
-    }
+    scrollEditor(codeMirrorEditor.view.scrollDOM, previewRef.current);
+  }, [codeMirrorEditor, previewRef]);
 
-    scrollPreviewByEditorLineWithThrottle(line);
-  }, [scrollPreviewByEditorLineWithThrottle]);
+  const scrollEditorHandlerThrottle = useMemo(() => throttle(25, scrollEditorHandler), [scrollEditorHandler]);
 
-  /**
-   * scroll Preview element by cursor moving
-   * @param {number} line
-   */
-  const scrollPreviewByCursorMoving = useCallback((line: number) => {
-    if (previewRef.current == null) {
+  const scrollPreviewHandler = useCallback(() => {
+    if (codeMirrorEditor?.view?.scrollDOM == null || previewRef.current == null) {
       return;
     }
 
-    // prevent circular invocation
-    if (isOriginOfScrollSyncPreview) {
-      isOriginOfScrollSyncPreview = false; // turn off the flag
+    if (isOriginOfScrollSyncEditor) {
+      isOriginOfScrollSyncEditor = false;
       return;
     }
 
-    // turn on the flag
-    isOriginOfScrollSyncEditor = true;
-    if (previewRef.current != null) {
-      scrollSyncHelper.scrollPreviewToRevealOverflowing(previewRef.current, line);
-    }
-  }, []);
-  const scrollPreviewByCursorMovingWithThrottle = useMemo(() => throttle(20, scrollPreviewByCursorMoving), [scrollPreviewByCursorMoving]);
-
-  /**
-   * the scroll event handler from codemirror
-   * @param {number} line
-   * @see https://codemirror.net/doc/manual.html#events
-   */
-  const editorScrollCursorIntoViewHandler = useCallback((line: number) => {
-    // record date
-    lastScrolledDateWithCursor = new Date();
-    scrollPreviewByCursorMovingWithThrottle(line);
-  }, [scrollPreviewByCursorMovingWithThrottle]);
-
-  /**
-   * scroll Editor component by scroll event of Preview component
-   * @param {number} offset
-   */
-  // const scrollEditorByPreviewScroll = useCallback((offset: number) => {
-  //   if (editorRef.current == null || previewRef.current == null) {
-  //     return;
-  //   }
-
-  //   // prevent circular invocation
-  //   if (isOriginOfScrollSyncEditor) {
-  //     isOriginOfScrollSyncEditor = false; // turn off the flag
-  //     return;
-  //   }
-
-  //   // turn on the flag
-  //   // eslint-disable-next-line @typescript-eslint/no-unused-vars
-  //   isOriginOfScrollSyncPreview = true;
-
-  //   scrollSyncHelper.scrollEditor(editorRef.current, previewRef.current, offset);
-  // }, []);
-  // const scrollEditorByPreviewScrollWithThrottle = useMemo(() => throttle(20, scrollEditorByPreviewScroll), [scrollEditorByPreviewScroll]);
+    isOriginOfScrollSyncPreview = true;
+    scrollPreview(codeMirrorEditor.view.scrollDOM, previewRef.current);
+  }, [codeMirrorEditor, previewRef]);
+
+  const scrollPreviewHandlerThrottle = useMemo(() => throttle(25, scrollPreviewHandler), [scrollPreviewHandler]);
 
   const afterResolvedHandler = useCallback(async() => {
     // get page data from db
@@ -481,22 +419,6 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
     codeMirrorEditor?.setCaretLine();
   }, [codeMirrorEditor]);
 
-  // set handler to set caret line
-  useEffect(() => {
-    const handler = (line) => {
-      codeMirrorEditor?.setCaretLine(line);
-
-      if (previewRef.current != null) {
-        scrollSyncHelper.scrollPreview(previewRef.current, line);
-      }
-    };
-    globalEmitter.on('setCaretLine', handler);
-
-    return function cleanup() {
-      globalEmitter.removeListener('setCaretLine', handler);
-    };
-  }, [codeMirrorEditor]);
-
   // set handler to save and return to View
   useEffect(() => {
     globalEmitter.on('saveAndReturnToView', saveAndReturnToViewHandler);
@@ -575,20 +497,19 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
             onChange={markdownChangedHandler}
             onSave={saveWithShortcut}
             onUpload={uploadHandler}
+            onScroll={scrollEditorHandlerThrottle}
             indentSize={currentIndentSize ?? defaultIndentSize}
             acceptedFileType={acceptedFileType}
           />
         </div>
-        <div className="page-editor-preview-container flex-expand-vert d-none d-lg-flex">
+        <div ref={previewRef} onScroll={scrollPreviewHandlerThrottle} className="page-editor-preview-container flex-expand-vert d-none d-lg-flex">
           <Preview
-            ref={previewRef}
             rendererOptions={rendererOptions}
             markdown={markdownToPreview}
             pagePath={currentPagePath}
             expandContentWidth={shouldExpandContent}
-            // TODO: implement
-            // refs: https://redmine.weseek.co.jp/issues/126519
-            // onScroll={offset => scrollEditorByPreviewScrollWithThrottle(offset)}
+            // TODO: Dynamic changes by height or resizing the last element
+            pastEnd={500}
           />
         </div>
         {/*

+ 4 - 8
apps/app/src/components/PageEditor/Preview.tsx

@@ -17,14 +17,15 @@ type Props = {
   markdown?: string,
   pagePath?: string | null,
   expandContentWidth?: boolean,
+  pastEnd?: number,
   onScroll?: (scrollTop: number) => void,
 }
 
-const Preview = React.forwardRef((props: Props, ref: RefObject<HTMLDivElement>): JSX.Element => {
+const Preview = React.forwardRef((props: Props): JSX.Element => {
 
   const {
     rendererOptions,
-    markdown, pagePath,
+    markdown, pagePath, pastEnd,
     expandContentWidth,
   } = props;
 
@@ -33,12 +34,7 @@ const Preview = React.forwardRef((props: Props, ref: RefObject<HTMLDivElement>):
   return (
     <div
       className={`${moduleClass} ${fluidLayoutClass} ${pagePath === '/Sidebar' ? 'preview-sidebar' : ''}`}
-      ref={ref}
-      onScroll={(event: SyntheticEvent<HTMLDivElement>) => {
-        if (props.onScroll != null) {
-          props.onScroll(event.currentTarget.scrollTop);
-        }
-      }}
+      style={{ paddingBottom: pastEnd }}
     >
       { markdown != null && (
         <RevisionRenderer rendererOptions={rendererOptions} markdown={markdown}></RevisionRenderer>

+ 0 - 206
apps/app/src/components/PageEditor/ScrollSyncHelper.js

@@ -1,206 +0,0 @@
-/**
- * This class is copied from Microsoft/vscode repository
- * @see https://github.com/Microsoft/vscode/blob/0532a3429a18688a0c086a4212e7e5b4888b2a48/extensions/markdown/media/main.js
- */
-class ScrollSyncHelper {
-
-  /**
-   * @typedef {{ element: Element, line: number }} CodeLineElement
-   */
-
-  getCodeLineElements(parentElement) {
-    /** @type {CodeLineElement[]} */
-    let elements;
-    if (!elements) {
-      elements = Array.prototype.map.call(
-        parentElement.getElementsByClassName('has-data-line'),
-        (element) => {
-          const line = +element.getAttribute('data-line');
-          return { element, line };
-        },
-      )
-        .filter((x) => { return !Number.isNaN(x.line) });
-    }
-    return elements;
-  }
-
-  /**
-   * Find the html elements that map to a specific target line in the editor.
-   *
-   * If an exact match, returns a single element. If the line is between elements,
-   * returns the element prior to and the element after the given line.
-   *
-   * @param {Element} element
-   * @param {number} targetLine
-   *
-   * @returns {{ previous: CodeLineElement, next?: CodeLineElement }}
-   */
-  getElementsForSourceLine(element, targetLine) {
-    const lines = this.getCodeLineElements(element);
-    let previous = lines[0] || null;
-    for (const entry of lines) {
-      if (entry.line === targetLine) {
-        return { previous: entry, next: null };
-      }
-      if (entry.line > targetLine) {
-        return { previous, next: entry };
-      }
-      previous = entry;
-    }
-    return { previous };
-  }
-
-  /**
-   * Find the html elements that are at a specific pixel offset on the page.
-   *
-   * @param {Element} parentElement
-   * @param {number} offset
-   *
-   * @returns {{ previous: CodeLineElement, next?: CodeLineElement }}
-   */
-  getLineElementsAtPageOffset(parentElement, offset) {
-    const lines = this.getCodeLineElements(parentElement);
-
-    const position = offset - parentElement.scrollTop + this.getParentElementOffset(parentElement);
-
-    let lo = -1;
-    let hi = lines.length - 1;
-    while (lo + 1 < hi) {
-      const mid = Math.floor((lo + hi) / 2);
-      const bounds = lines[mid].element.getBoundingClientRect();
-      if (bounds.top + bounds.height >= position) {
-        hi = mid;
-      }
-      else {
-        lo = mid;
-      }
-    }
-
-    const hiElement = lines[hi];
-
-    if (hiElement == null) {
-      return {};
-    }
-
-    if (hi >= 1 && hiElement.element.getBoundingClientRect().top > position) {
-      const loElement = lines[lo];
-      const bounds = loElement.element.getBoundingClientRect();
-      const previous = { element: loElement.element, line: loElement.line };
-      if (bounds.height > 0) {
-        previous.line += (position - bounds.top) / (bounds.height);
-      }
-      const next = { element: hiElement.element, line: hiElement.line, fractional: 0 };
-      return { previous, next };
-    }
-
-    const bounds = hiElement.element.getBoundingClientRect();
-    const previous = { element: hiElement.element, line: hiElement.line + (position - bounds.top) / (bounds.height) };
-    return { previous };
-  }
-
-  getEditorLineNumberForPageOffset(parentElement, offset) {
-    const { previous, next } = this.getLineElementsAtPageOffset(parentElement, offset);
-    if (previous != null) {
-      if (next) {
-        const betweenProgress = (
-          offset - parentElement.scrollTop - previous.element.getBoundingClientRect().top)
-          / (next.element.getBoundingClientRect().top - previous.element.getBoundingClientRect().top);
-        return previous.line + betweenProgress * (next.line - previous.line);
-      }
-
-      return previous.line;
-
-    }
-    return null;
-  }
-
-  /**
-   * return the sum of the offset position of parent element and paddingTop
-   * @param {Element} parentElement
-   */
-  getParentElementOffset(parentElement) {
-    const offsetY = parentElement.getBoundingClientRect().top;
-    // get paddingTop
-    const style = window.getComputedStyle(parentElement, null);
-    const paddingTop = +(style.paddingTop.replace('px', ''));
-
-    return offsetY + paddingTop;
-  }
-
-  /**
-   * Attempt to scroll preview element for a source line in the editor.
-   *
-   * @param {Element} previewElement
-   * @param {number} line
-   */
-  scrollPreview(previewElement, line) {
-    const { previous, next } = this.getElementsForSourceLine(previewElement, line);
-    if (previous) {
-      let scrollTo = 0;
-      if (next) {
-        // Between two elements. Go to percentage offset between them.
-        const betweenProgress = (line - previous.line) / (next.line - previous.line);
-        const elementOffset = next.element.getBoundingClientRect().top - previous.element.getBoundingClientRect().top;
-        scrollTo = previous.element.getBoundingClientRect().top + betweenProgress * elementOffset;
-      }
-      else {
-        scrollTo = previous.element.getBoundingClientRect().top;
-      }
-
-      scrollTo -= this.getParentElementOffset(previewElement);
-
-      previewElement.scrollTop += scrollTo;
-    }
-  }
-
-  /**
-   * Attempt to reveal the element that is overflowing from previewElement.
-   *
-   * @param {Element} previewElement
-   * @param {number} line
-   */
-  scrollPreviewToRevealOverflowing(previewElement, line) {
-    // eslint-disable-next-line no-unused-vars
-    const { previous, next } = this.getElementsForSourceLine(previewElement, line);
-    if (previous) {
-      const parentElementOffset = this.getParentElementOffset(previewElement);
-      const prevElmTop = previous.element.getBoundingClientRect().top - parentElementOffset;
-      const prevElmBottom = previous.element.getBoundingClientRect().bottom - parentElementOffset;
-
-      let scrollTo = null;
-      if (prevElmTop < 0) {
-        // set the top of 'previous.element' to the top of 'previewElement'
-        scrollTo = previewElement.scrollTop + prevElmTop;
-      }
-      else if (prevElmBottom > previewElement.clientHeight) {
-        // set the bottom of 'previous.element' to the bottom of 'previewElement'
-        scrollTo = previewElement.scrollTop + prevElmBottom - previewElement.clientHeight + 20;
-      }
-
-      if (scrollTo == null) {
-        return;
-      }
-
-      previewElement.scrollTop = scrollTo;
-    }
-  }
-
-  /**
-   * Attempt to scroll Editor component for the offset of the element in the Preview component.
-   *
-   * @param {Editor} editor
-   * @param {Element} previewElement
-   * @param {number} offset
-   */
-  scrollEditor(editor, previewElement, offset) {
-    let line = this.getEditorLineNumberForPageOffset(previewElement, offset);
-    line = Math.floor(line);
-    editor.setScrollTopByLine(line);
-  }
-
-}
-
-// singleton pattern
-const instance = new ScrollSyncHelper();
-Object.freeze(instance);
-export default instance;

+ 152 - 0
apps/app/src/components/PageEditor/ScrollSyncHelper.ts

@@ -0,0 +1,152 @@
+let defaultTop = 0;
+const padding = 5;
+
+const setDefaultTop = (top: number): void => {
+  defaultTop = top;
+};
+const getDefaultTop = (): number => {
+  return defaultTop + padding;
+};
+
+
+const getDataLine = (element: Element | null): number => {
+  return element ? +(element.getAttribute('data-line') ?? '0') - 1 : 0;
+};
+
+const getEditorElements = (editorRootElement: HTMLElement): Array<Element> => {
+  return Array.from(editorRootElement.getElementsByClassName('cm-line'))
+    .filter((element) => { return !Number.isNaN(element.getAttribute('data-line')) });
+};
+
+const getPreviewElements = (previewRootElement: HTMLElement): Array<Element> => {
+  return Array.from(previewRootElement.getElementsByClassName('has-data-line'))
+    .filter((element) => { return !Number.isNaN(element.getAttribute('data-line')) });
+};
+
+// Ref: https://github.com/mikolalysenko/binary-search-bounds/blob/f436a2a8af11bf3208434e18bbac17e18e7a3a30/search-bounds.js
+const elementBinarySearch = (list: Array<Element>, fn: (index: number) => boolean): number => {
+  let ok = 0;
+  let ng = list.length;
+  while (ok + 1 < ng) {
+    const mid = Math.floor((ok + ng) / 2);
+    if (fn(mid)) {
+      ok = mid;
+    }
+    else {
+      ng = mid;
+    }
+  }
+  return ok;
+};
+
+const findTopElementIndex = (elements: Array<Element>): number => {
+
+  const find = (index: number): boolean => {
+    return elements[index].getBoundingClientRect().top < getDefaultTop();
+  };
+
+  return elementBinarySearch(elements, find);
+};
+
+const findElementIndexFromDataLine = (previewElements: Array<Element>, dataline: number): number => {
+
+  const find = (index: number): boolean => {
+    return getDataLine(previewElements[index]) <= dataline;
+  };
+
+  return elementBinarySearch(previewElements, find);
+};
+
+
+type SourceElement = {
+  start: DOMRect,
+  top: DOMRect,
+  next: DOMRect | undefined,
+}
+
+type TargetElement = {
+  start: DOMRect,
+  next: DOMRect | undefined,
+}
+
+const calcScrollElementToTop = (element: Element): number => {
+  return element.getBoundingClientRect().top - getDefaultTop();
+};
+
+const calcScorllElementByRatio = (sourceElement: SourceElement, targetElement: TargetElement): number => {
+  if (sourceElement.start === sourceElement.next || sourceElement.next == null || targetElement.next == null) {
+    return 0;
+  }
+  const sourceAllHeight = sourceElement.next.top - sourceElement.start.top;
+  const sourceOutHeight = sourceElement.top.top - sourceElement.start.top;
+  const sourceTopHeight = getDefaultTop() - sourceElement.top.top;
+  const sourceRaito = (sourceOutHeight + sourceTopHeight) / sourceAllHeight;
+
+  const targetAllHeight = targetElement.next.top - targetElement.start.top;
+
+  return targetAllHeight * sourceRaito;
+};
+
+
+export const scrollEditor = (editorRootElement: HTMLElement, previewRootElement: HTMLElement): void => {
+
+  setDefaultTop(editorRootElement.getBoundingClientRect().top);
+
+  const editorElements = getEditorElements(editorRootElement);
+  const previewElements = getPreviewElements(previewRootElement);
+
+  const topEditorElementIndex = findTopElementIndex(editorElements);
+  const topPreviewElementIndex = findElementIndexFromDataLine(previewElements, getDataLine(editorElements[topEditorElementIndex]));
+
+  const startEditorElementIndex = findElementIndexFromDataLine(editorElements, getDataLine(previewElements[topPreviewElementIndex]));
+  const nextEditorElementIndex = findElementIndexFromDataLine(editorElements, getDataLine(previewElements[topPreviewElementIndex + 1]));
+
+  let newScrollTop = previewRootElement.scrollTop;
+
+  newScrollTop += calcScrollElementToTop(previewElements[topPreviewElementIndex]);
+  newScrollTop += calcScorllElementByRatio(
+    {
+      start: editorElements[startEditorElementIndex].getBoundingClientRect(),
+      top: editorElements[topEditorElementIndex].getBoundingClientRect(),
+      next: editorElements[nextEditorElementIndex]?.getBoundingClientRect(),
+    },
+    {
+      start: previewElements[topPreviewElementIndex].getBoundingClientRect(),
+      next: previewElements[topPreviewElementIndex + 1]?.getBoundingClientRect(),
+    },
+  );
+
+  previewRootElement.scrollTop = newScrollTop;
+
+};
+
+export const scrollPreview = (editorRootElement: HTMLElement, previewRootElement: HTMLElement): void => {
+
+  setDefaultTop(previewRootElement.getBoundingClientRect().y);
+
+  const previewElements = getPreviewElements(previewRootElement);
+  const editorElements = getEditorElements(editorRootElement);
+
+  const topPreviewElementIndex = findTopElementIndex(previewElements);
+
+  const startEditorElementIndex = findElementIndexFromDataLine(editorElements, getDataLine(previewElements[topPreviewElementIndex]));
+  const nextEditorElementIndex = findElementIndexFromDataLine(editorElements, getDataLine(previewElements[topPreviewElementIndex + 1]));
+
+  let newScrollTop = editorRootElement.scrollTop;
+
+  newScrollTop += calcScrollElementToTop(editorElements[startEditorElementIndex]);
+  newScrollTop += calcScorllElementByRatio(
+    {
+      start: previewElements[topPreviewElementIndex].getBoundingClientRect(),
+      top: previewElements[topPreviewElementIndex].getBoundingClientRect(),
+      next: previewElements[topPreviewElementIndex + 1]?.getBoundingClientRect(),
+    },
+    {
+      start: editorElements[startEditorElementIndex].getBoundingClientRect(),
+      next: editorElements[nextEditorElementIndex]?.getBoundingClientRect(),
+    },
+  );
+
+  editorRootElement.scrollTop = newScrollTop;
+
+};

+ 1 - 1
apps/app/src/components/PageList/PageListItemL.tsx

@@ -226,7 +226,7 @@ const PageListItemLSubstance: ForwardRefRenderFunction<ISelectable, Props> = (pr
               <Clamp lines={1}>
                 <span className="h5 mb-0">
                   {/* Use permanent links to care for pages with the same name (Cannot use page path url) */}
-                  <span className="grw-page-path-hierarchical-link text-break">
+                  <span className="text-break">
                     <Link
                       legacyBehavior
                       href={returnPathForURL(pageData.path, pageData._id)}

+ 9 - 4
apps/app/src/components/Sidebar/PageCreateButton/PageCreateButton.tsx

@@ -9,7 +9,7 @@ import { useOnTemplateButtonClicked } from '~/client/services/use-on-template-bu
 import { toastError } from '~/client/util/toastr';
 import { LabelType } from '~/interfaces/template';
 import { useCurrentUser } from '~/stores/context';
-import { useCurrentPagePath } from '~/stores/page';
+import { useCurrentPagePath, useSWRxCurrentPage } from '~/stores/page';
 
 import { CreateButton } from './CreateButton';
 import { DropendMenu } from './DropendMenu';
@@ -25,7 +25,8 @@ const generateTodaysPath = (currentUser: IUserHasId, parentDirName: string) => {
 export const PageCreateButton = React.memo((): JSX.Element => {
   const { t } = useTranslation('commons');
 
-  const { data: currentPagePath, isLoading } = useCurrentPagePath();
+  const { data: currentPagePath, isLoading: isLoadingPagePath } = useCurrentPagePath();
+  const { data: currentPage, isLoading } = useSWRxCurrentPage();
   const { data: currentUser } = useCurrentUser();
 
   const [isHovered, setIsHovered] = useState(false);
@@ -34,9 +35,13 @@ export const PageCreateButton = React.memo((): JSX.Element => {
     ? null
     : generateTodaysPath(currentUser, t('create_page_dropdown.todays.memo'));
 
-  const { onClickHandler: onClickNewButton, isPageCreating: isNewPageCreating } = useOnNewButtonClicked(currentPagePath, isLoading);
+  const { onClickHandler: onClickNewButton, isPageCreating: isNewPageCreating } = useOnNewButtonClicked(
+    currentPage?.path, currentPage?.grant, currentPage?.grantedGroups, isLoading,
+  );
+  // TODO: https://redmine.weseek.co.jp/issues/138806
   const { onClickHandler: onClickTodaysButton, isPageCreating: isTodaysPageCreating } = useOnTodaysButtonClicked(todaysPath);
-  const { onClickHandler: onClickTemplateButton, isPageCreating: isTemplatePageCreating } = useOnTemplateButtonClicked(currentPagePath, isLoading);
+  // TODO: https://redmine.weseek.co.jp/issues/138805
+  const { onClickHandler: onClickTemplateButton, isPageCreating: isTemplatePageCreating } = useOnTemplateButtonClicked(currentPagePath, isLoadingPagePath);
 
   const onClickTemplateButtonHandler = useCallback(async(label: LabelType) => {
     try {

+ 14 - 5
apps/app/src/components/Sidebar/PageCreateButton/hooks.tsx

@@ -1,5 +1,6 @@
 import { useCallback, useState } from 'react';
 
+import type { PageGrant, IGrantedGroup } from '@growi/core';
 import { useRouter } from 'next/router';
 
 import { createPage, exist } from '~/client/services/page-operation';
@@ -7,6 +8,8 @@ import { toastError } from '~/client/util/toastr';
 
 export const useOnNewButtonClicked = (
     currentPagePath?: string,
+    currentPageGrant?: PageGrant,
+    currentPageGrantedGroups?: IGrantedGroup[],
     isLoading?: boolean,
 ): {
   onClickHandler: () => Promise<void>,
@@ -21,19 +24,25 @@ export const useOnNewButtonClicked = (
     try {
       setIsPageCreating(true);
 
-      const parentPath = currentPagePath == null
+      /**
+       * !! NOTICE !! - Verification of page createable or not is checked on the server side.
+       * since the new page path is not generated on the client side.
+       * need shouldGeneratePath flag.
+       */
+      const shouldUseRootPath = currentPagePath == null || currentPageGrant == null;
+      const parentPath = shouldUseRootPath
         ? '/'
         : currentPagePath;
 
       const params = {
         isSlackEnabled: false,
         slackChannels: '',
-        grant: 4,
-        // grant: currentPage?.grant || 1,
-        // grantUserGroupId: currentPage?.grantedGroup?._id,
+        grant: shouldUseRootPath ? 1 : currentPageGrant,
+        grantUserGroupIds: shouldUseRootPath ? undefined : currentPageGrantedGroups,
         shouldGeneratePath: true,
       };
 
+      // !! NOTICE !! - if shouldGeneratePath is flagged, send the parent page path
       const response = await createPage(parentPath, '', params);
 
       router.push(`/${response.page.id}#edit`);
@@ -44,7 +53,7 @@ export const useOnNewButtonClicked = (
     finally {
       setIsPageCreating(false);
     }
-  }, [currentPagePath, isLoading, router]);
+  }, [currentPageGrant, currentPageGrantedGroups, currentPagePath, isLoading, router]);
 
   return { onClickHandler, isPageCreating };
 };

+ 7 - 3
apps/app/src/components/Sidebar/Sidebar.tsx

@@ -143,15 +143,19 @@ const CollapsibleContainer = memo((props: CollapsibleContainerProps): JSX.Elemen
 
 });
 
+// for data-* attributes
+type HTMLElementProps = JSX.IntrinsicElements &
+  Record<keyof JSX.IntrinsicElements, { [p: `data-${string}`]: string | number }>;
 
 type DrawableContainerProps = {
+  divProps?: HTMLElementProps['div'],
   className?: string,
   children?: React.ReactNode,
 }
 
 const DrawableContainer = memo((props: DrawableContainerProps): JSX.Element => {
 
-  const { className, children } = props;
+  const { divProps, className, children } = props;
 
   const { data: isDrawerOpened, mutate } = useDrawerOpened();
 
@@ -159,7 +163,7 @@ const DrawableContainer = memo((props: DrawableContainerProps): JSX.Element => {
 
   return (
     <>
-      <div className={`${className} ${openClass}`}>
+      <div {...divProps} className={`${className} ${openClass}`}>
         {children}
       </div>
       { isDrawerOpened && (
@@ -201,7 +205,7 @@ export const Sidebar = (): JSX.Element => {
         </DrawerToggler>
       ) }
       { sidebarMode != null && !isDockMode() && <AppTitleOnSubnavigation /> }
-      <DrawableContainer className={`${grwSidebarClass} ${modeClass} border-end flex-expand-vh-100`} data-testid="grw-sidebar">
+      <DrawableContainer className={`${grwSidebarClass} ${modeClass} border-end flex-expand-vh-100`} divProps={{ 'data-testid': 'grw-sidebar' }}>
         <ResizableContainer>
           { sidebarMode != null && !isCollapsedMode() && <AppTitleOnSidebarHead /> }
           <SidebarHead />

+ 1 - 0
apps/app/src/components/Sidebar/SidebarHead/ToggleCollapseButton.tsx

@@ -37,6 +37,7 @@ export const ToggleCollapseButton = memo((): JSX.Element => {
       type="button"
       className={`btn btn-primary ${styles['btn-toggle-collapse']} p-2`}
       onClick={isDrawerMode() ? toggleDrawer : toggleCollapsed}
+      data-testid="btn-toggle-collapse"
     >
       <span className={`material-symbols-outlined fs-2 ${rotationClass}`}>{icon}</span>
     </button>

+ 7 - 3
apps/app/src/server/routes/apiv3/pages.js

@@ -307,16 +307,16 @@ module.exports = (crowi) => {
   router.post('/', accessTokenParser, loginRequiredStrictly, excludeReadOnlyUser, addActivity, validator.createPage, apiV3FormValidator, async(req, res) => {
     const {
       // body, grant, grantUserGroupId, overwriteScopesOfDescendants, isSlackEnabled, slackChannels, pageTags, shouldGeneratePath,
-      body, grant, grantUserGroupIds, overwriteScopesOfDescendants, isSlackEnabled, slackChannels, pageTags, shouldGeneratePath,
+      body, overwriteScopesOfDescendants, isSlackEnabled, slackChannels, pageTags, shouldGeneratePath,
     } = req.body;
 
+    let { path, grant, grantUserGroupIds } = req.body;
+
     // TODO: remove in https://redmine.weseek.co.jp/issues/136136
     if (grantUserGroupIds != null && grantUserGroupIds.length > 1) {
       return res.apiv3Err('Cannot grant multiple groups to page at the moment');
     }
 
-    let { path } = req.body;
-
     // check whether path starts slash
     path = addHeadingSlash(path);
 
@@ -327,8 +327,12 @@ module.exports = (crowi) => {
         const basePath = path === rootPath ? defaultTitle : path + defaultTitle;
         path = await generateUniquePath(basePath);
 
+        // if the generated path is not creatable, create the path under the root path
         if (!isCreatablePage(path)) {
           path = await generateUniquePath(defaultTitle);
+          // initialize grant data
+          grant = 1;
+          grantUserGroupIds = undefined;
         }
       }
       catch (err) {

+ 18 - 19
apps/app/test/cypress/e2e/50-sidebar/50-sidebar--access-to-side-bar.cy.ts

@@ -36,9 +36,8 @@ describe('Access to sidebar', () => {
           });
         });
 
-        // TODO: rewrite test case with grw-switch-collapse-button
         it('Successfully collapse sidebar', () => {
-          cy.getByTestid('grw-switch-collapse-button').click({force: true});
+          cy.getByTestid('btn-toggle-collapse').click({force: true});
 
           cy.getByTestid('grw-sidebar-contents').should('not.be.visible');
 
@@ -205,27 +204,28 @@ describe('Access to sidebar', () => {
           });
         });
 
-        it('Successfully redirect to editor', () => {
-          const content = '# HELLO \n ## Hello\n ### Hello';
+        // TODO: fix by https://redmine.weseek.co.jp/issues/138562
+        // it('Successfully redirect to editor', () => {
+        //   const content = '# HELLO \n ## Hello\n ### Hello';
 
-          cy.get('.grw-sidebar-content-header > h3 > a').should('be.visible').click();
+        //   cy.get('.grw-sidebar-content-header > h3 > a').should('be.visible').click();
 
-          cy.get('.layout-root').should('have.class', 'editing');
-          cy.get('.CodeMirror textarea').type(content, {force: true});
+        //   cy.get('.layout-root').should('have.class', 'editing');
+        //   cy.get('.CodeMirror textarea').type(content, {force: true});
 
-          cy.screenshot(`${ssPrefix}custom-sidebar-2-redirect-to-editor`, { blackout: blackoutOverride });
+        //   cy.screenshot(`${ssPrefix}custom-sidebar-2-redirect-to-editor`, { blackout: blackoutOverride });
 
-          cy.getByTestid('save-page-btn').click();
-        });
+        //   cy.getByTestid('save-page-btn').click();
+        // });
 
-        it('Successfully create custom sidebar content', () => {
-          cy.getByTestid('grw-sidebar-nav-primary-custom-sidebar')
-            .should('be.visible')
-            .should('have.class', 'active');
+        // it('Successfully create custom sidebar content', () => {
+        //   cy.getByTestid('grw-sidebar-nav-primary-custom-sidebar')
+        //     .should('be.visible')
+        //     .should('have.class', 'active');
 
-          cy.waitUntilSkeletonDisappear();
-          cy.screenshot(`${ssPrefix}custom-sidebar-3-content-created`, { blackout: blackoutOverride });
-        });
+        //   cy.waitUntilSkeletonDisappear();
+        //   cy.screenshot(`${ssPrefix}custom-sidebar-3-content-created`, { blackout: blackoutOverride });
+        // });
       });
 
       describe('Test recent changes tab', () => {
@@ -307,8 +307,7 @@ describe('Access to sidebar', () => {
             cy.get('a[href*="/trash"]').click();
           });
 
-          cy.get('.grw-page-path-hierarchical-link').should('be.visible');
-          cy.get('.grw-custom-nav-tab').should('be.visible');
+          cy.getByTestid('trash-page-list').should('be.visible');
 
           cy.screenshot(`${ssPrefix}access-to-trash-page`, { blackout: blackoutOverride });
         });

+ 39 - 10
apps/app/test/cypress/e2e/50-sidebar/50-sidebar--switching-sidebar-mode.cy.ts

@@ -9,28 +9,57 @@ const blackoutOverride = [
 context('Switch sidebar mode', () => {
   const ssPrefix = 'switch-sidebar-mode-';
 
-  before(() => {
+  beforeEach(() => {
     // login
     cy.fixture("user-admin.json").then(user => {
       cy.login(user.username, user.password);
     });
+    cy.visit('/');
   });
 
   it('Switching sidebar mode', () => {
-    cy.visit('/');
-    cy.get('.grw-apperance-mode-dropdown').first().click();
-
-    cy.get('[for="swSidebarMode"]').click({force: true});
-    cy.get('.grw-sidebar-nav').should('not.be.visible');
-    cy.screenshot(`${ssPrefix}-switch-sidebar-mode`, {
+    cy.collapseSidebar(false);
+    cy.screenshot(`${ssPrefix}-doc-mode-opened`, {
       blackout: blackoutOverride,
     });
 
-    cy.get('[for="swSidebarMode"]').click({force: true});
-    cy.get('.grw-sidebar-nav').should('be.visible');
-    cy.screenshot(`${ssPrefix}-switch-sidebar-mode-back`, {
+    cy.collapseSidebar(true);
+    cy.screenshot(`${ssPrefix}-doc-mode-closed`, {
       blackout: blackoutOverride,
     });
   });
 
 });
+
+context('Switch viewport size', () => {
+  const ssPrefix = 'switch-viewport-size-';
+
+  const sizes = {
+    'xl': [1200, 1024],
+    'lg': [992, 1024],
+    'md': [768, 1024],
+    'sm': [576, 1024],
+    'xs': [575, 1024],
+    'iphone-x': [375, 812],
+  };
+
+  Object.entries(sizes).forEach(([screenLabel, size]) => {
+    it(`on ${screenLabel} screen`, () => {
+      cy.viewport(size[0], size[1]);
+
+      // login
+      cy.fixture("user-admin.json").then(user => {
+        cy.login(user.username, user.password);
+      });
+      cy.visit('/');
+
+      cy.get('.layout-root').should('be.visible');
+
+      cy.screenshot(`${ssPrefix}-${screenLabel}`, {
+        blackout: blackoutOverride,
+      });
+    });
+  });
+
+});
+

+ 6 - 6
apps/app/test/cypress/e2e/60-home/60-home--home.cy.ts

@@ -64,7 +64,7 @@ context('Access User settings', () => {
 
   it('Access External account', () => {
     cy.getByTestid('grw-personal-settings').find('.nav-title.nav li:eq(1) a').click();
-    cy.scrollTo('top');
+    cy.scrollTo('top', {ensureScrollable: false});
     cy.screenshot(`${ssPrefix}-external-account-1`);
     cy.getByTestid('grw-external-account-add-button').click();
     cy.getByTestid('grw-associate-modal').should('be.visible');
@@ -76,7 +76,7 @@ context('Access User settings', () => {
       cy.get('.Toastify__close-button').should('be.visible').click();
       cy.get('.Toastify__progress-bar').invoke('attr', 'style', 'display: none')
     });
-    cy.getByTestid('grw-associate-modal').find('.close').click();
+    cy.getByTestid('grw-associate-modal').find('[aria-label="Close"]').click();
     cy.screenshot(`${ssPrefix}-external-account-4`);
 
       cy.get('.Toastify__toast').should('not.be.visible');
@@ -84,7 +84,7 @@ context('Access User settings', () => {
 
   it('Access Password setting', () => {
     cy.getByTestid('grw-personal-settings').find('.nav-title.nav li:eq(2) a').click();
-    cy.scrollTo('top');
+    cy.scrollTo('top', {ensureScrollable: false});
     cy.screenshot(`${ssPrefix}-password-settings-1`);
     cy.getByTestid('grw-password-settings-update-button').click();
     cy.get('.Toastify__toast').should('be.visible');
@@ -100,7 +100,7 @@ context('Access User settings', () => {
 
   it('Access API setting', () => {
     cy.getByTestid('grw-personal-settings').find('.nav-title.nav li:eq(3) a').click();
-    cy.scrollTo('top');
+    cy.scrollTo('top', {ensureScrollable: false});
     cy.screenshot(`${ssPrefix}-api-setting-1`);
     cy.getByTestid('grw-api-settings-update-button').click();
     cy.getByTestid('grw-api-settings-input').should('be.visible');
@@ -115,7 +115,7 @@ context('Access User settings', () => {
 
   it('Access In-app notification setting', () => {
     cy.getByTestid('grw-personal-settings').find('.nav-title.nav li:eq(4) a').click();
-    cy.scrollTo('top');
+    cy.scrollTo('top', {ensureScrollable: false});
     cy.screenshot(`${ssPrefix}-in-app-notification-setting-1`);
     cy.getByTestid('grw-in-app-notification-settings-update-button').click();
     cy.get('.Toastify__toast').should('be.visible');
@@ -124,7 +124,7 @@ context('Access User settings', () => {
 
   it('Access Other setting', () => {
     cy.getByTestid('grw-personal-settings').find('.nav-title.nav li:eq(5) a').click();
-    cy.scrollTo('top');
+    cy.scrollTo('top', {ensureScrollable: false});
     cy.screenshot(`${ssPrefix}-other-setting-1`);
     cy.getByTestid('grw-questionnaire-settings-update-btn').click();
     cy.get('.Toastify__toast').should('be.visible').invoke('attr', 'style', 'display: none');

+ 1 - 1
apps/app/test/cypress/support/commands.ts

@@ -88,7 +88,7 @@ Cypress.Commands.add('collapseSidebar', (isCollapsed: boolean, waitUntilSaving =
 
     cy.waitUntil(() => {
       // do
-      cy.getByTestid("grw-switch-collapse-button").click({force: true});
+      cy.getByTestid("btn-toggle-collapse").click({force: true});
       // wait until saving UserUISettings
       if (waitUntilSaving) {
         // eslint-disable-next-line cypress/no-unnecessary-waiting

+ 20 - 0
packages/editor/src/components/CodeMirrorEditor/CodeMirrorEditor.tsx

@@ -25,6 +25,7 @@ type Props = {
   acceptedFileType: AcceptedUploadFileType,
   onChange?: (value: string) => void,
   onUpload?: (files: File[]) => void,
+  onScroll?: () => void,
   indentSize?: number,
 }
 
@@ -34,6 +35,7 @@ export const CodeMirrorEditor = (props: Props): JSX.Element => {
     acceptedFileType,
     onChange,
     onUpload,
+    onScroll,
     indentSize,
   } = props;
 
@@ -100,6 +102,24 @@ export const CodeMirrorEditor = (props: Props): JSX.Element => {
 
   }, [codeMirrorEditor]);
 
+  useEffect(() => {
+
+    const handleScroll = (event: Event) => {
+      event.preventDefault();
+      if (onScroll != null) {
+        onScroll();
+      }
+    };
+
+    const extension = EditorView.domEventHandlers({
+      scroll: handleScroll,
+    });
+
+    const cleanupFunction = codeMirrorEditor?.appendExtensions(extension);
+    return cleanupFunction;
+
+  }, [onScroll, codeMirrorEditor]);
+
   const {
     getRootProps,
     isDragActive,

+ 9 - 3
packages/editor/src/components/CodeMirrorEditorMain.tsx

@@ -1,29 +1,34 @@
 import { useEffect } from 'react';
 
-import type { Extension } from '@codemirror/state';
+import { type Extension } from '@codemirror/state';
 import { keymap, scrollPastEnd } from '@codemirror/view';
 
 import { GlobalCodeMirrorEditorKey, AcceptedUploadFileType } from '../consts';
+import { setDataLine } from '../services/extensions/setDataLine';
 import { useCodeMirrorEditorIsolated } from '../stores';
 
 import { CodeMirrorEditor } from '.';
 
 
 const additionalExtensions: Extension[] = [
-  scrollPastEnd(),
+  [
+    scrollPastEnd(),
+    setDataLine,
+  ],
 ];
 
 type Props = {
   onChange?: (value: string) => void,
   onSave?: () => void,
   onUpload?: (files: File[]) => void,
+  onScroll?: () => void,
   acceptedFileType?: AcceptedUploadFileType,
   indentSize?: number,
 }
 
 export const CodeMirrorEditorMain = (props: Props): JSX.Element => {
   const {
-    onSave, onChange, onUpload, acceptedFileType, indentSize,
+    onSave, onChange, onUpload, onScroll, acceptedFileType, indentSize,
   } = props;
 
   const { data: codeMirrorEditor } = useCodeMirrorEditorIsolated(GlobalCodeMirrorEditorKey.MAIN);
@@ -65,6 +70,7 @@ export const CodeMirrorEditorMain = (props: Props): JSX.Element => {
       editorKey={GlobalCodeMirrorEditorKey.MAIN}
       onChange={onChange}
       onUpload={onUpload}
+      onScroll={onScroll}
       acceptedFileType={acceptedFileTypeNoOpt}
       indentSize={indentSize}
     />

+ 1 - 0
packages/editor/src/services/codemirror-editor/use-codemirror-editor/use-codemirror-editor.ts

@@ -23,6 +23,7 @@ import { useInsertText, type InsertText } from './utils/insert-text';
 import { useReplaceText, type ReplaceText } from './utils/replace-text';
 import { useSetCaretLine, type SetCaretLine } from './utils/set-caret-line';
 
+
 const markdownHighlighting = HighlightStyle.define([
   { tag: tags.heading1, class: 'cm-header-1 cm-header' },
   { tag: tags.heading2, class: 'cm-header-2 cm-header' },

+ 48 - 0
packages/editor/src/services/extensions/setDataLine.ts

@@ -0,0 +1,48 @@
+
+// Ref: https://github.com/uiwjs/react-codemirror/blob/bf3b862923d0cb04ccf4bb9da0791bdc7fd6d29b/extensions/classname/src/index.ts
+
+
+import { RangeSetBuilder } from '@codemirror/state';
+import {
+  EditorView, Decoration, ViewPlugin, DecorationSet, ViewUpdate,
+} from '@codemirror/view';
+
+const stripeDeco = (view: EditorView) => {
+  const builder = new RangeSetBuilder<Decoration>();
+  for (const { from, to } of view.visibleRanges) {
+    for (let pos = from; pos <= to;) {
+      const line = view.state.doc.lineAt(pos);
+      const cls = line.number.toString();
+      builder.add(
+        line.from,
+        line.from,
+        Decoration.line({
+          attributes: { 'data-line': cls },
+        }),
+      );
+      pos = line.to + 1;
+    }
+  }
+  return builder.finish();
+};
+
+export const setDataLine = ViewPlugin.fromClass(
+  class {
+
+    decorations: DecorationSet;
+
+    constructor(view: EditorView) {
+      this.decorations = stripeDeco(view);
+    }
+
+    update(update: ViewUpdate) {
+      if (update.docChanged || update.viewportChanged) {
+        this.decorations = stripeDeco(update.view);
+      }
+    }
+
+  },
+  {
+    decorations: v => v.decorations,
+  },
+);

BIN
packages/preset-themes/public/images/wood/wood-navbar.jpg


BIN
packages/preset-themes/public/images/wood/wood.jpg


Разница между файлами не показана из-за своего большого размера
+ 1 - 0
packages/preset-themes/public/images/wood/wood.svg


+ 201 - 161
packages/preset-themes/src/styles/island.scss

@@ -1,170 +1,210 @@
-@use '@growi/core/scss/bootstrap/init' as bs;
-
-@use './variables' as var;
-@use './theme/mixins/page-editor-mode-manager';
-@use './atoms/mixins/buttons' as mixins-buttons;
-@use './theme/hsl-functions' as hsl;
-
-//== Light Mode
-//
-:root[data-bs-theme='light'] {
-  --primary: hsl(var(--primary-hs),var(--primary-l)) !important;
-  --primary-hs: 171,33%;
-  --primary-l: 69%;
-  --secondary: hsl(var(--secondary-hs),var(--secondary-l)) !important;
-  --secondary-hs: 208,7%;
-  --secondary-l: 46%;
-
-  // Background colors
-  --bgcolor-global: hsl(var(--bgcolor-global-hs),var(--bgcolor-global-l));
-  --bgcolor-global-hs: 170,43%;
-  --bgcolor-global-l: 90%;
-  --bgcolor-card: #{bs.$gray-100};
-  --bgcolor-inline-code: #{bs.$gray-100}; //optional
-  --bgcolor-blinked-section: #{hsl.alpha(var(--primary), 30%)};
-  //--bgcolor-keyword-highlighted: #{$grw-marker-yellow};
-
-  // Font colors
-  --color-global: hsl(var(--color-global-hs),var(--color-global-l));
-  --color-global-hs: 214,60%;
-  --color-global-l: 17%;
-  --color-reversal: #eeeeee;
-  // --color-header: #2b2b2b;
-  --color-link: hsl(var(--color-link-hs),var(--color-link-l));
-  --color-link-hs: 186,31%;
-  --color-link-l: 34%;
-  --color-link-hover: #{hsl.lighten(var(--color-link), 20%)};
-  --color-link-wiki: var(--color-link);
-  --color-link-wiki-hs: var(--color-link-hs);
-  --color-link-wiki-l: var(--color-link-l);
-  --color-link-wiki-hover: #{hsl.lighten(var(--color-link), 20%)};
-  --color-link-nabvar: var(--color-reversal);
-  --color-inline-code: #c7254e; // optional
-
-  // List Group colors
-  // --color-list: var(--color-global);
-  // --bgcolor-list: var(--bgcolor-global);
-  // --color-list-hover: ;
-  --bgcolor-list-hover: #{hsl.darken(var(--bgcolor-global),10%)};
-  --color-list-active: var(--color-global);
-  --bgcolor-list-active: var(--primary);
-
-  // Table colors
-  // --color-table: #; // optional
-  // --bgcolor-table: #; // optional
-  --border-color-table: var(--primary); // optional
-  // --color-table-hover: #; // optional
-  // --bgcolor-table-hover: #; // optional
-
-  // Navbar
-  --bgcolor-navbar: hsl(var(--bgcolor-navbar-hs),var(--bgcolor-navbar-l));
-  --bgcolor-navbar-hs: 0,2%;
-  --bgcolor-navbar-l: 18%;
-  --bgcolor-search-top-dropdown: var(--primary);
-  --bgcolor-search-top-dropdown-hs: var(--primary-hs);
-  --bgcolor-search-top-dropdown-l: var(--primary-l);
-  --border-image-navbar: linear-gradient(to right, #5ce4ef 0%, #5953eb 100%);
-
-  // Logo colors
-  --bgcolor-logo: #0d3955;
-  --fillcolor-logo-mark: #{lighten(desaturate(bs.$gray-100, 10%), 15%)};
-
-  // Sidebar
-  --bgcolor-sidebar: hsl(var(--bgcolor-sidebar-hs),var(--bgcolor-sidebar-l));
-  --bgcolor-sidebar-hs: 203,73%;
-  --bgcolor-sidebar-l: 19%;
-  --bgcolor-sidebar-nav-item-active: rgba(black, 0.37);
-  // --bgcolor-sidebar-nav-item-active: rgba(#969494, 0.3); // optional
-  --text-shadow-sidebar-nav-item-active: 0px 0px 10px #0099ff; // optional
-
-  // Sidebar resize button
-  --color-resize-button: white;
-  --bgcolor-resize-button: var(--primary);
-  --bgcolor-resize-button-hs: var(--primary-hs);
-  --bgcolor-resize-button-l: var(--primary-l);
-  --color-resize-button-hover: white;
-  --bgcolor-resize-button-hover: #{hsl.darken(var(--primary),5%)};
-
-  // Sidebar contents
-  --bgcolor-sidebar-context: hsl(var(--bgcolor-sidebar-context-hs),var(--bgcolor-sidebar-context-l));
-  --bgcolor-sidebar-context-hs: 173,41%;
-  --bgcolor-sidebar-context-l: 92%;
-  --color-sidebar-context: var(--color-link);
-  --color-sidebar-context-hs: var(--color-link-hs);
-  --color-sidebar-context-l: var(--color-link-l);
-
-  // Sidebar list group
-  --bgcolor-sidebar-list-group: #eff8f7; // optional
-
-  // Subnavigation
-  --bgcolor-subnav: hsl(var(--bgcolor-subnav-hs),var(--bgcolor-subnav-l));
-  --bgcolor-subnav-hs: var(--bgcolor-global-hs);
-  --bgcolor-subnav-l: calc(var(--bgcolor-global-l) + 4%);
-
-  // Tabs
-  --bordercolor-nav-tabs: #{bs.$gray-300}; // optional
-
-  // Icon colors
-  --color-editor-icons: var(--color-global);
-
-  // Border colors
-  --border-color-theme: #{bs.$gray-300};
-  --bordercolor-inline-code: #ccc8c8; // optional
-
-  // Dropdown colors
-  --bgcolor-dropdown-link-active: #{var.$growi-blue};
-
-  // admin theme box
-  --color-theme-color-box: #{hsl.darken(var(--primary), 15%)};
+:root[data-bs-theme] {
+  @import '@growi/core/scss/bootstrap/init-stage-1';
+  @import '@growi/core/scss/bootstrap/theming/variables';
+  @import '@growi/core/scss/bootstrap/theming/utils/color-palette';
 
-  &, body {
-    background-image: url('../images/island/island.png');
-    background-attachment: fixed;
-  }
+  $primary: #00A2B7;
+  $highlight: #C6AB62;
 
-  .rbt-menu {
-    background: #{hsl.darken(var(--bgcolor-global), 5%)};
-  }
+  @include generate-color-palette('primary', $primary, black, white);
+  @include generate-color-palette('highlight', $highlight, black, white);
 
-  .page-editor-preview-container {
-    background-image: url('../images/island/island.png');
-    background-attachment: fixed;
-  }
+  $body-color:                mix(#00A2B7, black, 40%);
+  $body-bg:                   white;
 
-  // login page
-  .nologin {
-    background-image: unset !important;
-  }
+  $body-secondary-color:      rgba($body-color, .75);
+  $body-secondary-bg:         $gray-200;
 
-  // Button
-  .btn-group.grw-page-editor-mode-manager {
-    .btn.btn-outline-primary {
-      @include page-editor-mode-manager.btn-page-editor-mode-manager(#{hsl.darken(var(--primary), 50%)}, #{hsl.lighten(var(--primary), 5%)}, #{hsl.darken(var(--primary), 5%)});
-    }
-  }
+  $body-tertiary-color:       rgba($body-color, .5);
+  $body-tertiary-bg:          $gray-100;
 
-  // Cards
-  .admin-bot-card {
-    .grw-botcard-title-active {
-      color: var(--color-reversal);
-    }
-  }
+  $border-color:              $gray-300;
 
-  /*
-   * GROWI Sidebar
-  */
-  .grw-sidebar {
-    // Pagetree
-    .grw-pagetree {
-      .grw-pagetree-triangle-btn {
-        @include mixins-buttons.button-outline-svg-icon-variant(bs.$gray-400, #0d3955);
-      }
-    }
-    // Foldertree
-    .grw-foldertree {
-      .grw-foldertree-triangle-btn {
-        @include mixins-buttons.button-outline-svg-icon-variant(bs.$gray-400, var(--bgcolor-sidebar));
-      }
-    }
+  $link-color:                $gray-800;
+
+  @import 'bootstrap/scss/variables';
+  @import 'bootstrap/scss/variables-dark';
+
+  @import '@growi/core/scss/bootstrap/init-stage-2';
+
+  @import '@growi/core/scss/bootstrap/theming/apply-light';
+
+  --grw-wiki-link-color-rgb: var(--grw-primary-600-rgb);
+  --grw-wiki-link-hover-color-rgb: var(--grw-primary-700-rgb);
+
+  &, body {
+    background: linear-gradient(to bottom, white 20%, #D8F9FD);
+    background-attachment: fixed;
   }
 }
+
+// @use '@growi/core/scss/bootstrap/init' as bs;
+
+// @use './variables' as var;
+// @use './theme/mixins/page-editor-mode-manager';
+// @use './atoms/mixins/buttons' as mixins-buttons;
+// @use './theme/hsl-functions' as hsl;
+
+// //== Light Mode
+// //
+// :root[data-bs-theme='light'] {
+//   --primary: hsl(var(--primary-hs),var(--primary-l)) !important;
+//   --primary-hs: 171,33%;
+//   --primary-l: 69%;
+//   --secondary: hsl(var(--secondary-hs),var(--secondary-l)) !important;
+//   --secondary-hs: 208,7%;
+//   --secondary-l: 46%;
+
+//   // Background colors
+//   --bgcolor-global: hsl(var(--bgcolor-global-hs),var(--bgcolor-global-l));
+//   --bgcolor-global-hs: 170,43%;
+//   --bgcolor-global-l: 90%;
+//   --bgcolor-card: #{bs.$gray-100};
+//   --bgcolor-inline-code: #{bs.$gray-100}; //optional
+//   --bgcolor-blinked-section: #{hsl.alpha(var(--primary), 30%)};
+//   //--bgcolor-keyword-highlighted: #{$grw-marker-yellow};
+
+//   // Font colors
+//   --color-global: hsl(var(--color-global-hs),var(--color-global-l));
+//   --color-global-hs: 214,60%;
+//   --color-global-l: 17%;
+//   --color-reversal: #eeeeee;
+//   // --color-header: #2b2b2b;
+//   --color-link: hsl(var(--color-link-hs),var(--color-link-l));
+//   --color-link-hs: 186,31%;
+//   --color-link-l: 34%;
+//   --color-link-hover: #{hsl.lighten(var(--color-link), 20%)};
+//   --color-link-wiki: var(--color-link);
+//   --color-link-wiki-hs: var(--color-link-hs);
+//   --color-link-wiki-l: var(--color-link-l);
+//   --color-link-wiki-hover: #{hsl.lighten(var(--color-link), 20%)};
+//   --color-link-nabvar: var(--color-reversal);
+//   --color-inline-code: #c7254e; // optional
+
+//   // List Group colors
+//   // --color-list: var(--color-global);
+//   // --bgcolor-list: var(--bgcolor-global);
+//   // --color-list-hover: ;
+//   --bgcolor-list-hover: #{hsl.darken(var(--bgcolor-global),10%)};
+//   --color-list-active: var(--color-global);
+//   --bgcolor-list-active: var(--primary);
+
+//   // Table colors
+//   // --color-table: #; // optional
+//   // --bgcolor-table: #; // optional
+//   --border-color-table: var(--primary); // optional
+//   // --color-table-hover: #; // optional
+//   // --bgcolor-table-hover: #; // optional
+
+//   // Navbar
+//   --bgcolor-navbar: hsl(var(--bgcolor-navbar-hs),var(--bgcolor-navbar-l));
+//   --bgcolor-navbar-hs: 0,2%;
+//   --bgcolor-navbar-l: 18%;
+//   --bgcolor-search-top-dropdown: var(--primary);
+//   --bgcolor-search-top-dropdown-hs: var(--primary-hs);
+//   --bgcolor-search-top-dropdown-l: var(--primary-l);
+//   --border-image-navbar: linear-gradient(to right, #5ce4ef 0%, #5953eb 100%);
+
+//   // Logo colors
+//   --bgcolor-logo: #0d3955;
+//   --fillcolor-logo-mark: #{lighten(desaturate(bs.$gray-100, 10%), 15%)};
+
+//   // Sidebar
+//   --bgcolor-sidebar: hsl(var(--bgcolor-sidebar-hs),var(--bgcolor-sidebar-l));
+//   --bgcolor-sidebar-hs: 203,73%;
+//   --bgcolor-sidebar-l: 19%;
+//   --bgcolor-sidebar-nav-item-active: rgba(black, 0.37);
+//   // --bgcolor-sidebar-nav-item-active: rgba(#969494, 0.3); // optional
+//   --text-shadow-sidebar-nav-item-active: 0px 0px 10px #0099ff; // optional
+
+//   // Sidebar resize button
+//   --color-resize-button: white;
+//   --bgcolor-resize-button: var(--primary);
+//   --bgcolor-resize-button-hs: var(--primary-hs);
+//   --bgcolor-resize-button-l: var(--primary-l);
+//   --color-resize-button-hover: white;
+//   --bgcolor-resize-button-hover: #{hsl.darken(var(--primary),5%)};
+
+//   // Sidebar contents
+//   --bgcolor-sidebar-context: hsl(var(--bgcolor-sidebar-context-hs),var(--bgcolor-sidebar-context-l));
+//   --bgcolor-sidebar-context-hs: 173,41%;
+//   --bgcolor-sidebar-context-l: 92%;
+//   --color-sidebar-context: var(--color-link);
+//   --color-sidebar-context-hs: var(--color-link-hs);
+//   --color-sidebar-context-l: var(--color-link-l);
+
+//   // Sidebar list group
+//   --bgcolor-sidebar-list-group: #eff8f7; // optional
+
+//   // Subnavigation
+//   --bgcolor-subnav: hsl(var(--bgcolor-subnav-hs),var(--bgcolor-subnav-l));
+//   --bgcolor-subnav-hs: var(--bgcolor-global-hs);
+//   --bgcolor-subnav-l: calc(var(--bgcolor-global-l) + 4%);
+
+//   // Tabs
+//   --bordercolor-nav-tabs: #{bs.$gray-300}; // optional
+
+//   // Icon colors
+//   --color-editor-icons: var(--color-global);
+
+//   // Border colors
+//   --border-color-theme: #{bs.$gray-300};
+//   --bordercolor-inline-code: #ccc8c8; // optional
+
+//   // Dropdown colors
+//   --bgcolor-dropdown-link-active: #{var.$growi-blue};
+
+//   // admin theme box
+//   --color-theme-color-box: #{hsl.darken(var(--primary), 15%)};
+
+//   &, body {
+//     background-image: url('../images/island/island.png');
+//     background-attachment: fixed;
+//   }
+
+//   .rbt-menu {
+//     background: #{hsl.darken(var(--bgcolor-global), 5%)};
+//   }
+
+//   .page-editor-preview-container {
+//     background-image: url('../images/island/island.png');
+//     background-attachment: fixed;
+//   }
+
+//   // login page
+//   .nologin {
+//     background-image: unset !important;
+//   }
+
+//   // Button
+//   .btn-group.grw-page-editor-mode-manager {
+//     .btn.btn-outline-primary {
+//       @include page-editor-mode-manager.btn-page-editor-mode-manager(#{hsl.darken(var(--primary), 50%)}, #{hsl.lighten(var(--primary), 5%)}, #{hsl.darken(var(--primary), 5%)});
+//     }
+//   }
+
+//   // Cards
+//   .admin-bot-card {
+//     .grw-botcard-title-active {
+//       color: var(--color-reversal);
+//     }
+//   }
+
+//   /*
+//    * GROWI Sidebar
+//   */
+//   .grw-sidebar {
+//     // Pagetree
+//     .grw-pagetree {
+//       .grw-pagetree-triangle-btn {
+//         @include mixins-buttons.button-outline-svg-icon-variant(bs.$gray-400, #0d3955);
+//       }
+//     }
+//     // Foldertree
+//     .grw-foldertree {
+//       .grw-foldertree-triangle-btn {
+//         @include mixins-buttons.button-outline-svg-icon-variant(bs.$gray-400, var(--bgcolor-sidebar));
+//       }
+//     }
+//   }
+// }

+ 158 - 123
packages/preset-themes/src/styles/nature.scss

@@ -1,125 +1,160 @@
-@use '@growi/core/scss/bootstrap/init' as bs;
-
-@use './variables' as var;
-@use './theme/mixins/page-editor-mode-manager';
-@use './theme/hsl-functions' as hsl;
-
-.growi:not(.login-page) {
-  // add background-image
-  .page-editor-preview-container {
-    background-attachment: fixed;
-    background-position: center center;
-    background-size: cover;
-  }
-}
+:root[data-bs-theme] {
+  @import '@growi/core/scss/bootstrap/init-stage-1';
+  @import '@growi/core/scss/bootstrap/theming/variables';
+  @import '@growi/core/scss/bootstrap/theming/utils/color-palette';
+
+  $primary: #4FA529;
+  $highlight: #9DE201;
+
+  @include generate-color-palette('primary', $primary, black, white);
+  @include generate-color-palette('highlight', $highlight, black, white);
+
+  $body-color:                $gray-800;
+  $body-bg:                   mix(#9DE201, white, 5%);
+
+  $body-secondary-color:      rgba($body-color, .75);
+  $body-secondary-bg:         $gray-200;
+
+  $body-tertiary-color:       rgba($body-color, .5);
+  $body-tertiary-bg:          $gray-100;
 
-//== Light Mode
-//
-:root[data-bs-theme='light'] {
-  --primary: hsl(var(--primary-hs),var(--primary-l)) !important;
-  --primary-hs: 311,100%;
-  --primary-l: 14%;
-  --secondary: hsl(var(--secondary-hs),var(--secondary-l)) !important;
-  --secondary-hs: 208,7%;
-  --secondary-l: 46%;
-
-  // Background colors
-  --bgcolor-global: hsl(var(--bgcolor-global-hs),var(--bgcolor-global-l));
-  --bgcolor-global-hs: 0,0%;
-  --bgcolor-global-l: 99%;
-  --bgcolor-inline-code: #{bs.$gray-100}; //optional
-  --bgcolor-card: #f1ffe4;
-  --bgcolor-blinked-section: #{hsl.alpha(var(--primary), 10%)};
-  //--bgcolor-keyword-highlighted: #{$grw-marker-yellow};
-
-  // Font colors
-  --color-global: hsl(var(--color-global-hs),var(--color-global-l));
-  --color-global-hs: 311,100%;
-  --color-global-l: 14%;
-  --color-reversal: #eeeeee;
-  --color-link: hsl(var(--color-link-hs),var(--color-link-l));
-  --color-link-hs: 328,100%;
-  --color-link-l: 25%;
-  --color-link-hover: #{hsl.lighten(var(--color-link), 20%)};
-  --color-link-wiki: #{hsl.lighten(var(--primary), 20%)};
-  --color-link-wiki-hs: var(--primary-hs);
-  --color-link-wiki-l: calc(var(--primary-l) + 20%);
-  --color-link-wiki-hover: #{hsl.lighten(var(--primary), 40%)};
-  --color-link-nabvar: #a7a7a7;
-  --color-inline-code: #c7254e; // optional
-  --color-search: white;
-
-  // Navbar
-  --bgcolor-navbar: hsl(var(--bgcolor-navbar-hs),var(--bgcolor-navbar-l));
-  --bgcolor-navbar-hs: 158,30%;
-  --bgcolor-navbar-l: 20%;
-  --bgcolor-search-top-dropdown: hsl(var(--bgcolor-search-top-dropdown-hs),var(--bgcolor-search-top-dropdown-l));
-  --bgcolor-search-top-dropdown-hs: 115,95%;
-  --bgcolor-search-top-dropdown-l: 36%;
-  --border-image-navbar: linear-gradient(to right, #5c78ef 0%, #16bc42 50%, #5c78ef 100%);
-
-  // Logo colors
-  --bgcolor-logo: var(--bgcolor-navbar);
-  --fillcolor-logo-mark: #{lighten(desaturate(bs.$gray-100, 10%), 15%)};
-
-  // Sidebar
-  --bgcolor-sidebar: hsl(var(--bgcolor-sidebar-hs),var(--bgcolor-sidebar-l));
-  --bgcolor-sidebar-hs: 158,71%;
-  --bgcolor-sidebar-l: 33%;
-
-  // Sidebar contents
-  --color-sidebar-context: hsl(var(--color-sidebar-context-hs),var(--color-sidebar-context-l));
-  --color-sidebar-context-hs: 328,100%;
-  --color-sidebar-context-l: 25%;
-  --bgcolor-sidebar-context: hsl(var(--bgcolor-sidebar-context-hs),var(--bgcolor-sidebar-context-l));
-  --bgcolor-sidebar-context-hs: 67,31%;
-  --bgcolor-sidebar-context-l: 94%;
-
-  // Sidebar resize button
-  --color-resize-button: white;
-  --bgcolor-resize-button: hsl(var(--bgcolor-resize-button-hs),var(--bgcolor-resize-button-l));
-  --bgcolor-resize-button-hs: 115,95%;
-  --bgcolor-resize-button-l: 36%;
-  --color-resize-button-hover: var(--color-reversal);
-  --bgcolor-resize-button-hover: #{hsl.lighten(var(--bgcolor-resize-button), 5%)};
-
-  // Subnavigation
-  --bgcolor-subnav: hsl(var(--bgcolor-subnav-hs),var(--bgcolor-subnav-l));
-  --bgcolor-subnav-hs: 0,0%;
-  --bgcolor-subnav-l: 98%;
-
-  // Icon colors
-  --color-editor-icons: var(--color-global);
-
-  // Border colors
-  --border-color-theme: #{bs.$gray-300};
-  --bordercolor-inline-code: #ccc8c8; // optional
-
-  // Table colors
-  --border-color-table: #{bs.$gray-400}; // optional
-
-  // admin theme box
-  --color-theme-color-box: #{hsl.lighten(var(--primary), 20%)};
-
-  // Search Top
-  .grw-global-search {
-    .btn-secondary.dropdown-toggle {
-      color: var(--color-search);
-    }
-  }
-
-  // Navs
-  .nav-tabs .nav-link.active {
-    color: var(--color-link) !important;
-    &:hover {
-      color: var(--color-link-hover) !important;
-    }
-  }
-
-  // Button
-  .btn-group.grw-page-editor-mode-manager {
-    .btn.btn-outline-primary {
-      @include page-editor-mode-manager.btn-page-editor-mode-manager(var(--bgcolor-navbar), #{hsl.lighten(var(--bgcolor-navbar), 65%)}, #{hsl.lighten(var(--bgcolor-navbar), 70%)});
-    }
-  }
+  $border-color:              $gray-300;
+
+  $link-color:                $gray-800;
+
+  @import 'bootstrap/scss/variables';
+  @import 'bootstrap/scss/variables-dark';
+
+  @import '@growi/core/scss/bootstrap/init-stage-2';
+
+  @import '@growi/core/scss/bootstrap/theming/apply-light';
+
+  --grw-wiki-link-color-rgb: var(--grw-primary-600-rgb);
+  --grw-wiki-link-hover-color-rgb: var(--grw-primary-700-rgb);
 }
+
+// @use '@growi/core/scss/bootstrap/init' as bs;
+
+// @use './variables' as var;
+// @use './theme/mixins/page-editor-mode-manager';
+// @use './theme/hsl-functions' as hsl;
+
+// .growi:not(.login-page) {
+//   // add background-image
+//   .page-editor-preview-container {
+//     background-attachment: fixed;
+//     background-position: center center;
+//     background-size: cover;
+//   }
+// }
+
+// //== Light Mode
+// //
+// :root[data-bs-theme='light'] {
+//   --primary: hsl(var(--primary-hs),var(--primary-l)) !important;
+//   --primary-hs: 311,100%;
+//   --primary-l: 14%;
+//   --secondary: hsl(var(--secondary-hs),var(--secondary-l)) !important;
+//   --secondary-hs: 208,7%;
+//   --secondary-l: 46%;
+
+//   // Background colors
+//   --bgcolor-global: hsl(var(--bgcolor-global-hs),var(--bgcolor-global-l));
+//   --bgcolor-global-hs: 0,0%;
+//   --bgcolor-global-l: 99%;
+//   --bgcolor-inline-code: #{bs.$gray-100}; //optional
+//   --bgcolor-card: #f1ffe4;
+//   --bgcolor-blinked-section: #{hsl.alpha(var(--primary), 10%)};
+//   //--bgcolor-keyword-highlighted: #{$grw-marker-yellow};
+
+//   // Font colors
+//   --color-global: hsl(var(--color-global-hs),var(--color-global-l));
+//   --color-global-hs: 311,100%;
+//   --color-global-l: 14%;
+//   --color-reversal: #eeeeee;
+//   --color-link: hsl(var(--color-link-hs),var(--color-link-l));
+//   --color-link-hs: 328,100%;
+//   --color-link-l: 25%;
+//   --color-link-hover: #{hsl.lighten(var(--color-link), 20%)};
+//   --color-link-wiki: #{hsl.lighten(var(--primary), 20%)};
+//   --color-link-wiki-hs: var(--primary-hs);
+//   --color-link-wiki-l: calc(var(--primary-l) + 20%);
+//   --color-link-wiki-hover: #{hsl.lighten(var(--primary), 40%)};
+//   --color-link-nabvar: #a7a7a7;
+//   --color-inline-code: #c7254e; // optional
+//   --color-search: white;
+
+//   // Navbar
+//   --bgcolor-navbar: hsl(var(--bgcolor-navbar-hs),var(--bgcolor-navbar-l));
+//   --bgcolor-navbar-hs: 158,30%;
+//   --bgcolor-navbar-l: 20%;
+//   --bgcolor-search-top-dropdown: hsl(var(--bgcolor-search-top-dropdown-hs),var(--bgcolor-search-top-dropdown-l));
+//   --bgcolor-search-top-dropdown-hs: 115,95%;
+//   --bgcolor-search-top-dropdown-l: 36%;
+//   --border-image-navbar: linear-gradient(to right, #5c78ef 0%, #16bc42 50%, #5c78ef 100%);
+
+//   // Logo colors
+//   --bgcolor-logo: var(--bgcolor-navbar);
+//   --fillcolor-logo-mark: #{lighten(desaturate(bs.$gray-100, 10%), 15%)};
+
+//   // Sidebar
+//   --bgcolor-sidebar: hsl(var(--bgcolor-sidebar-hs),var(--bgcolor-sidebar-l));
+//   --bgcolor-sidebar-hs: 158,71%;
+//   --bgcolor-sidebar-l: 33%;
+
+//   // Sidebar contents
+//   --color-sidebar-context: hsl(var(--color-sidebar-context-hs),var(--color-sidebar-context-l));
+//   --color-sidebar-context-hs: 328,100%;
+//   --color-sidebar-context-l: 25%;
+//   --bgcolor-sidebar-context: hsl(var(--bgcolor-sidebar-context-hs),var(--bgcolor-sidebar-context-l));
+//   --bgcolor-sidebar-context-hs: 67,31%;
+//   --bgcolor-sidebar-context-l: 94%;
+
+//   // Sidebar resize button
+//   --color-resize-button: white;
+//   --bgcolor-resize-button: hsl(var(--bgcolor-resize-button-hs),var(--bgcolor-resize-button-l));
+//   --bgcolor-resize-button-hs: 115,95%;
+//   --bgcolor-resize-button-l: 36%;
+//   --color-resize-button-hover: var(--color-reversal);
+//   --bgcolor-resize-button-hover: #{hsl.lighten(var(--bgcolor-resize-button), 5%)};
+
+//   // Subnavigation
+//   --bgcolor-subnav: hsl(var(--bgcolor-subnav-hs),var(--bgcolor-subnav-l));
+//   --bgcolor-subnav-hs: 0,0%;
+//   --bgcolor-subnav-l: 98%;
+
+//   // Icon colors
+//   --color-editor-icons: var(--color-global);
+
+//   // Border colors
+//   --border-color-theme: #{bs.$gray-300};
+//   --bordercolor-inline-code: #ccc8c8; // optional
+
+//   // Table colors
+//   --border-color-table: #{bs.$gray-400}; // optional
+
+//   // admin theme box
+//   --color-theme-color-box: #{hsl.lighten(var(--primary), 20%)};
+
+//   // Search Top
+//   .grw-global-search {
+//     .btn-secondary.dropdown-toggle {
+//       color: var(--color-search);
+//     }
+//   }
+
+//   // Navs
+//   .nav-tabs .nav-link.active {
+//     color: var(--color-link) !important;
+//     &:hover {
+//       color: var(--color-link-hover) !important;
+//     }
+//   }
+
+//   // Button
+//   .btn-group.grw-page-editor-mode-manager {
+//     .btn.btn-outline-primary {
+//       @include page-editor-mode-manager.btn-page-editor-mode-manager(var(--bgcolor-navbar), #{hsl.lighten(var(--bgcolor-navbar), 65%)}, #{hsl.lighten(var(--bgcolor-navbar), 70%)});
+//     }
+//   }
+// }

+ 227 - 188
packages/preset-themes/src/styles/wood.scss

@@ -1,197 +1,236 @@
-@use '@growi/core/scss/bootstrap/init' as bs;
-
-@use './variables' as var;
-@use './theme/mixins/page-editor-mode-manager';
-@use './theme/hsl-functions' as hsl;
-
-.growi:not(.login-page) {
-  // add background-image
-  .page-editor-preview-container {
-    background-image: url('../images/wood/wood.jpg');
-    background-attachment: fixed;
-    background-position: center center;
-    background-size: cover;
-  }
-}
+:root[data-bs-theme] {
+  @import '@growi/core/scss/bootstrap/init-stage-1';
+  @import '@growi/core/scss/bootstrap/theming/variables';
+  @import '@growi/core/scss/bootstrap/theming/utils/color-palette';
 
-.growi.login-page {
-  .page-wrapper {
-    background-image: url('../images/wood/wood.jpg');
-    background-attachment: fixed;
-    background-position: center center;
-    background-size: cover;
-  }
-}
+  $primary: #A77E21;
+  $highlight: #967428;
 
-//== Light Mode
-//
-:root[data-bs-theme='light'] {
-  --primary: hsl(var(--primary-hs),var(--primary-l)) !important;
-  --primary-hs: 55,31%;
-  --primary-l: 52%;
-  --secondary: hsl(var(--secondary-hs),var(--secondary-l)) !important;
-  --secondary-hs: 208,7%;
-  --secondary-l: 46%;
-  --themecolor: hsl(var(--themecolor-hs),var(--themecolor-l));
-  --themecolor-hs: 53,32%;
-  --themecolor-l: 60%;
-
-  // Background colors
-  --bgcolor-global: hsl(var(--bgcolor-global-hs),var(--bgcolor-global-l));
-  --bgcolor-global-hs: 0,0%;
-  --bgcolor-global-l: 100%;
-  --bgcolor-card: #ece8de;
-  --bgcolor-blinked-section: #{hsl.alpha(var(--primary),30%)};
-  --bgcolor-keyword-highlighted: #{var.$grw-marker-blue};
-
-  // Font colors
-  --color-global: hsl(var(--color-global-hs),var(--color-global-l));
-  --color-global-hs: 42,86%;
-  --color-global-l: 14%;
-  --color-reversal: #fffffc;
-  --color-link: hsl(var(--color-link-hs),var(--color-link-l));
-  --color-link-hs: 44,93%;
-  --color-link-l: 32%;
-  --color-link-hover: #{hsl.lighten(var(--color-link), 10%)};
-  --color-link-wiki: var(--color-link);
-  --color-link-wiki-hs: var(--color-link-hs);
-  --color-link-wiki-l: var(--color-link-l);
-  --color-link-wiki-hover: #{hsl.lighten(var(--color-link), 10%)};
-  --color-link-nabvar: #a7a7a7;
-  --color-search: white;
-
-  // Inline code
-  --bgcolor-inline-code: #f5f3ee;; //optional
-  // --color-inline-code: # !default;
-  --bordercolor-inline-code: var(--themecolor); //optional
-
-  // List Group colors
-  // --color-list: var(--color-global);
-  --bgcolor-list: transparent;
-  --color-list-hover: #{bs.$gray-100};
-  --bgcolor-list-hover: #{hsl.lighten(var(--primary), 40%)};
-  // --color-list-active: var(--color-reversal);
-  --bgcolor-list-active: #{hsl.lighten(var(--primary), 30%)};
-
-  // Table colors
-  // --color-table: #; // optional
-  // --bgcolor-table: #; // optional
-  --border-color-table: #{bs.$gray-400}; // optional
-  // --color-table-hover: #; // optional
-  // --bgcolor-table-hover: #; // optional
-
-  // Navbar
-  --bgcolor-navbar: hsl(var(--bgcolor-navbar-hs),var(--bgcolor-navbar-l));
-  --bgcolor-navbar-hs: 0,1%;
-  --bgcolor-navbar-l: 16%;
-  --bgcolor-search-top-dropdown: var(--themecolor);
-  --bgcolor-search-top-dropdown-hs: var(--themecolor-hs);
-  --bgcolor-search-top-dropdown-l: var(--themecolor-l);
-  --border-image-navbar: linear-gradient(to right, var(--themecolor) 0%, #{hsl.darken(var(--themecolor), 20%)} 100%);
-
-  // Logo colors
-  --bgcolor-logo: #{hsl.darken(var(--themecolor), 10%)};
-  --fillcolor-logo-mark: #{lighten(desaturate(#b9b177, 50%), 50%)}; // Icon colors
-  --color-editor-icons: var(--color-global);
-
-  // Sidebar
-  --bgcolor-sidebar: var(--themecolor);
-  --bgcolor-sidebar-hs: var(--themecolor-hs);
-  --bgcolor-sidebar-l: var(--themecolor-l);
-
-  // Sidebar contents
-  --color-sidebar-context: hsl(var(--color-sidebar-context-hs),var(--color-sidebar-context-l));
-  --color-sidebar-context-hs: 44,93%;
-  --color-sidebar-context-l: 32%;
-  --bgcolor-sidebar-context: #{hsl.lighten(var(--themecolor), 38%)};
-  --bgcolor-sidebar-context-hs: var(--themecolor-hs);
-  --bgcolor-sidebar-context-l: calc(var(--themecolor-l) + 38%);
-
-  // Sidebar list group
-  --bgcolor-sidebar-list-group: rgba(#f7f5f1, 0.5);
-
-  // Sidebar resize button
-  --color-resize-button: white;
-  --bgcolor-resize-button: var(--themecolor);
-  --bgcolor-resize-button-hs: var(--themecolor-hs);
-  --bgcolor-resize-button-l: var(--themecolor-l);
-  --color-resize-button-hover: var(--color-reversal);
-  --bgcolor-resize-button-hover: #{hsl.lighten(var(--bgcolor-resize-button), 5%)};
-
-  // Border colors
-  --border-color-theme: #{bs.$gray-300}; // former: `$navbar-border: $gray-300;`
-  --bordercolor-inline-code: #ccc8c8; // optional
-
-  // Dropdown colors
-  --bgcolor-dropdown-link-active: #{var.$growi-blue};
-
-  // admin theme box
-  --color-theme-color-box: #{hsl.lighten(var(--primary), 20%)};
-
-  // alert
-  --color-alert: var(--color-reversal);
-
-  // Subnavigation
-  --bgcolor-subnav: hsl(var(--bgcolor-subnav-hs),var(--bgcolor-subnav-l));
-  --bgcolor-subnav-hs: var(--bgcolor-global-hs);
-  --bgcolor-subnav-l: calc(var(--bgcolor-global-l) - 3%);
+  @include generate-color-palette('primary', $primary, black, white);
+  @include generate-color-palette('highlight', $highlight, black, white);
 
-  &, body {
-    background-image: url('../images/wood/wood.jpg');
-    background-attachment: fixed;
-    background-position: 50%;
-    background-size: cover;
-  }
+  $body-color:                mix(#967428, black, 60%);
+  $body-bg:                   #FFFFF5;
 
-  /*
-   * Modal
-   */
-  .modal-dialog .modal-header.bg-primary {
-    background-image: url('../images/wood/wood-navbar.jpg');
-  }
+  $body-secondary-color:      rgba($body-color, .75);
+  $body-secondary-bg:         $gray-200;
 
-  // Sidebar
-  .grw-sidebar {
-    div[data-testid='GlobalNavigation'] {
-      background-image: url('../images/wood/wood-navbar.jpg');
-
-      button,
-      .btn {
-        border: none;
-      }
-    }
-    div[data-testid='ContextualNavigation'] {
-      > div {
-        background-color: rgba(white, 0.6);
-        background-image: url('../images/wood/wood-navbar.jpg');
-        background-blend-mode: lighten;
-      }
-    }
-  }
+  $body-tertiary-color:       rgba($body-color, .5);
+  $body-tertiary-bg:          $gray-100;
 
-  // login and register
-  .nologin {
-    background: unset !important;
-
-    .page-wrapper{
-      .nologin-header,
-      .nologin-dialog {
-        background-color: rgba(black, 0.1);
-        a.link-switch {
-          color: rgba(black, 0.5);
-        }
-      }
-
-      .grw-external-auth-form {
-        border-color: #aaa;
-      }
-    }
-  }
+  $border-color:              $gray-300;
+
+  $link-color:                $gray-800;
+
+  @import 'bootstrap/scss/variables';
+  @import 'bootstrap/scss/variables-dark';
+
+  @import '@growi/core/scss/bootstrap/init-stage-2';
 
-  // Button
-  .btn-group.grw-page-editor-mode-manager {
-    .btn.btn-outline-primary {
-      @include page-editor-mode-manager.btn-page-editor-mode-manager(#{hsl.darken(var(--primary), 30%)}, #{hsl.lighten(var(--primary), 15%)}, #{hsl.lighten(var(--primary), 25%)});
-    }
+  @import '@growi/core/scss/bootstrap/theming/apply-light';
+
+  --grw-wiki-link-color-rgb: var(--grw-primary-500-rgb);
+  --grw-wiki-link-hover-color-rgb: var(--grw-primary-600-rgb);
+
+  &, body {
+    background-image: url('../images/wood/wood.svg');
   }
 }
+
+// @use '@growi/core/scss/bootstrap/init' as bs;
+
+// @use './variables' as var;
+// @use './theme/mixins/page-editor-mode-manager';
+// @use './theme/hsl-functions' as hsl;
+
+// .growi:not(.login-page) {
+//   // add background-image
+//   .page-editor-preview-container {
+//     background-image: url('../images/wood/wood.jpg');
+//     background-attachment: fixed;
+//     background-position: center center;
+//     background-size: cover;
+//   }
+// }
+
+// .growi.login-page {
+//   .page-wrapper {
+//     background-image: url('../images/wood/wood.jpg');
+//     background-attachment: fixed;
+//     background-position: center center;
+//     background-size: cover;
+//   }
+// }
+
+// //== Light Mode
+// //
+// :root[data-bs-theme='light'] {
+//   --primary: hsl(var(--primary-hs),var(--primary-l)) !important;
+//   --primary-hs: 55,31%;
+//   --primary-l: 52%;
+//   --secondary: hsl(var(--secondary-hs),var(--secondary-l)) !important;
+//   --secondary-hs: 208,7%;
+//   --secondary-l: 46%;
+//   --themecolor: hsl(var(--themecolor-hs),var(--themecolor-l));
+//   --themecolor-hs: 53,32%;
+//   --themecolor-l: 60%;
+
+//   // Background colors
+//   --bgcolor-global: hsl(var(--bgcolor-global-hs),var(--bgcolor-global-l));
+//   --bgcolor-global-hs: 0,0%;
+//   --bgcolor-global-l: 100%;
+//   --bgcolor-card: #ece8de;
+//   --bgcolor-blinked-section: #{hsl.alpha(var(--primary),30%)};
+//   --bgcolor-keyword-highlighted: #{var.$grw-marker-blue};
+
+//   // Font colors
+//   --color-global: hsl(var(--color-global-hs),var(--color-global-l));
+//   --color-global-hs: 42,86%;
+//   --color-global-l: 14%;
+//   --color-reversal: #fffffc;
+//   --color-link: hsl(var(--color-link-hs),var(--color-link-l));
+//   --color-link-hs: 44,93%;
+//   --color-link-l: 32%;
+//   --color-link-hover: #{hsl.lighten(var(--color-link), 10%)};
+//   --color-link-wiki: var(--color-link);
+//   --color-link-wiki-hs: var(--color-link-hs);
+//   --color-link-wiki-l: var(--color-link-l);
+//   --color-link-wiki-hover: #{hsl.lighten(var(--color-link), 10%)};
+//   --color-link-nabvar: #a7a7a7;
+//   --color-search: white;
+
+//   // Inline code
+//   --bgcolor-inline-code: #f5f3ee;; //optional
+//   // --color-inline-code: # !default;
+//   --bordercolor-inline-code: var(--themecolor); //optional
+
+//   // List Group colors
+//   // --color-list: var(--color-global);
+//   --bgcolor-list: transparent;
+//   --color-list-hover: #{bs.$gray-100};
+//   --bgcolor-list-hover: #{hsl.lighten(var(--primary), 40%)};
+//   // --color-list-active: var(--color-reversal);
+//   --bgcolor-list-active: #{hsl.lighten(var(--primary), 30%)};
+
+//   // Table colors
+//   // --color-table: #; // optional
+//   // --bgcolor-table: #; // optional
+//   --border-color-table: #{bs.$gray-400}; // optional
+//   // --color-table-hover: #; // optional
+//   // --bgcolor-table-hover: #; // optional
+
+//   // Navbar
+//   --bgcolor-navbar: hsl(var(--bgcolor-navbar-hs),var(--bgcolor-navbar-l));
+//   --bgcolor-navbar-hs: 0,1%;
+//   --bgcolor-navbar-l: 16%;
+//   --bgcolor-search-top-dropdown: var(--themecolor);
+//   --bgcolor-search-top-dropdown-hs: var(--themecolor-hs);
+//   --bgcolor-search-top-dropdown-l: var(--themecolor-l);
+//   --border-image-navbar: linear-gradient(to right, var(--themecolor) 0%, #{hsl.darken(var(--themecolor), 20%)} 100%);
+
+//   // Logo colors
+//   --bgcolor-logo: #{hsl.darken(var(--themecolor), 10%)};
+//   --fillcolor-logo-mark: #{lighten(desaturate(#b9b177, 50%), 50%)}; // Icon colors
+//   --color-editor-icons: var(--color-global);
+
+//   // Sidebar
+//   --bgcolor-sidebar: var(--themecolor);
+//   --bgcolor-sidebar-hs: var(--themecolor-hs);
+//   --bgcolor-sidebar-l: var(--themecolor-l);
+
+//   // Sidebar contents
+//   --color-sidebar-context: hsl(var(--color-sidebar-context-hs),var(--color-sidebar-context-l));
+//   --color-sidebar-context-hs: 44,93%;
+//   --color-sidebar-context-l: 32%;
+//   --bgcolor-sidebar-context: #{hsl.lighten(var(--themecolor), 38%)};
+//   --bgcolor-sidebar-context-hs: var(--themecolor-hs);
+//   --bgcolor-sidebar-context-l: calc(var(--themecolor-l) + 38%);
+
+//   // Sidebar list group
+//   --bgcolor-sidebar-list-group: rgba(#f7f5f1, 0.5);
+
+//   // Sidebar resize button
+//   --color-resize-button: white;
+//   --bgcolor-resize-button: var(--themecolor);
+//   --bgcolor-resize-button-hs: var(--themecolor-hs);
+//   --bgcolor-resize-button-l: var(--themecolor-l);
+//   --color-resize-button-hover: var(--color-reversal);
+//   --bgcolor-resize-button-hover: #{hsl.lighten(var(--bgcolor-resize-button), 5%)};
+
+//   // Border colors
+//   --border-color-theme: #{bs.$gray-300}; // former: `$navbar-border: $gray-300;`
+//   --bordercolor-inline-code: #ccc8c8; // optional
+
+//   // Dropdown colors
+//   --bgcolor-dropdown-link-active: #{var.$growi-blue};
+
+//   // admin theme box
+//   --color-theme-color-box: #{hsl.lighten(var(--primary), 20%)};
+
+//   // alert
+//   --color-alert: var(--color-reversal);
+
+//   // Subnavigation
+//   --bgcolor-subnav: hsl(var(--bgcolor-subnav-hs),var(--bgcolor-subnav-l));
+//   --bgcolor-subnav-hs: var(--bgcolor-global-hs);
+//   --bgcolor-subnav-l: calc(var(--bgcolor-global-l) - 3%);
+
+//   &, body {
+//     background-image: url('../images/wood/wood.jpg');
+//     background-attachment: fixed;
+//     background-position: 50%;
+//     background-size: cover;
+//   }
+
+//   /*
+//    * Modal
+//    */
+//   .modal-dialog .modal-header.bg-primary {
+//     background-image: url('../images/wood/wood-navbar.jpg');
+//   }
+
+//   // Sidebar
+//   .grw-sidebar {
+//     div[data-testid='GlobalNavigation'] {
+//       background-image: url('../images/wood/wood-navbar.jpg');
+
+//       button,
+//       .btn {
+//         border: none;
+//       }
+//     }
+//     div[data-testid='ContextualNavigation'] {
+//       > div {
+//         background-color: rgba(white, 0.6);
+//         background-image: url('../images/wood/wood-navbar.jpg');
+//         background-blend-mode: lighten;
+//       }
+//     }
+//   }
+
+//   // login and register
+//   .nologin {
+//     background: unset !important;
+
+//     .page-wrapper{
+//       .nologin-header,
+//       .nologin-dialog {
+//         background-color: rgba(black, 0.1);
+//         a.link-switch {
+//           color: rgba(black, 0.5);
+//         }
+//       }
+
+//       .grw-external-auth-form {
+//         border-color: #aaa;
+//       }
+//     }
+//   }
+
+//   // Button
+//   .btn-group.grw-page-editor-mode-manager {
+//     .btn.btn-outline-primary {
+//       @include page-editor-mode-manager.btn-page-editor-mode-manager(#{hsl.darken(var(--primary), 30%)}, #{hsl.lighten(var(--primary), 15%)}, #{hsl.lighten(var(--primary), 25%)});
+//     }
+//   }
+// }

+ 3 - 4
packages/preset-themes/vite.themes.config.ts

@@ -18,14 +18,13 @@ export default defineConfig(({ mode }) => {
           '/src/styles/future.scss',
           // '/src/styles/halloween.scss',
           // '/src/styles/hufflepuff.scss',
-          // '/src/styles/island.scss',
           '/src/styles/kibela.scss',
+          '/src/styles/island.scss',
           '/src/styles/jade-green.scss',
-          // '/src/styles/kibela.scss',
           '/src/styles/mono-blue.scss',
-          // '/src/styles/nature.scss',
+          '/src/styles/nature.scss',
           '/src/styles/spring.scss',
-          // '/src/styles/wood.scss',
+          '/src/styles/wood.scss',
         ],
         output: {
           assetFileNames: isProd

Некоторые файлы не были показаны из-за большого количества измененных файлов