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

Merge pull request #10838 from growilabs/support/migrate-to-turbopack

support: Migrate to Turbopack
Yuki Takei 1 месяц назад
Родитель
Сommit
34969a8f57
100 измененных файлов с 762 добавлено и 602 удалено
  1. 1 3
      .kiro/steering/tech.md
  2. 4 2
      apps/app/.claude/skills/app-commands/SKILL.md
  3. 82 35
      apps/app/.claude/skills/build-optimization/SKILL.md
  4. 116 0
      apps/app/.claude/skills/vendor-styles-components/SKILL.md
  5. 2 0
      apps/app/.env.development
  6. 3 0
      apps/app/.gitignore
  7. 2 8
      apps/app/config/next-i18next.config.js
  8. 0 6
      apps/app/next-env.d.ts
  9. 35 61
      apps/app/next.config.ts
  10. 6 6
      apps/app/package.json
  11. 2 1
      apps/app/src/client/components/Admin/AuditLog/DateRangePicker.tsx
  12. 6 0
      apps/app/src/client/components/Admin/AuditLog/DateRangePicker.vendor-styles.ts
  13. 1 1
      apps/app/src/client/components/Admin/Customize/ThemeColorBox.module.scss
  14. 3 5
      apps/app/src/client/components/Admin/UserGroup/UserGroupTable.module.scss
  15. 9 9
      apps/app/src/client/components/Admin/UserManagement.module.scss
  16. 3 3
      apps/app/src/client/components/Admin/Users/ExternalAccountTable.module.scss
  17. 1 1
      apps/app/src/client/components/Admin/Users/UserMenu.module.scss
  18. 4 4
      apps/app/src/client/components/AuthorInfo/AuthorInfo.module.scss
  19. 3 3
      apps/app/src/client/components/Bookmarks/BookmarkFolderMenu.module.scss
  20. 20 20
      apps/app/src/client/components/Bookmarks/BookmarkFolderTree.module.scss
  21. 6 6
      apps/app/src/client/components/Common/CopyDropdown/CopyDropdown.module.scss
  22. 2 2
      apps/app/src/client/components/Common/DrawerToggler/DrawerToggler.module.scss
  23. 3 2
      apps/app/src/client/components/Common/ImageCropModal.tsx
  24. 6 0
      apps/app/src/client/components/Common/ImageCropModal.vendor-styles.ts
  25. 2 2
      apps/app/src/client/components/CompleteUserRegistrationForm.module.scss
  26. 4 4
      apps/app/src/client/components/CustomNavigation/CustomNav.module.scss
  27. 7 7
      apps/app/src/client/components/DescendantsPageListModal/DescendantsPageListModal.module.scss
  28. 6 0
      apps/app/src/client/components/GrowiEditor.vendor-styles.ts
  29. 2 2
      apps/app/src/client/components/InAppNotification/ModelNotification/ModelNotification.module.scss
  30. 2 2
      apps/app/src/client/components/InstallerForm.module.scss
  31. 15 15
      apps/app/src/client/components/LoginForm/LoginForm.module.scss
  32. 12 12
      apps/app/src/client/components/Me/AccessTokenScopeList.module.scss
  33. 4 4
      apps/app/src/client/components/Navbar/GrowiNavbarBottom.module.scss
  34. 7 7
      apps/app/src/client/components/Navbar/PageEditorModeManager.module.scss
  35. 7 7
      apps/app/src/client/components/PageAccessoriesModal/PageAccessoriesModal.module.scss
  36. 3 3
      apps/app/src/client/components/PageAttachment/DeleteAttachmentModal.module.scss
  37. 9 9
      apps/app/src/client/components/PageComment.module.scss
  38. 17 17
      apps/app/src/client/components/PageComment/Comment.module.scss
  39. 13 13
      apps/app/src/client/components/PageComment/CommentEditor.module.scss
  40. 4 4
      apps/app/src/client/components/PageComment/CommentEditor.tsx
  41. 3 3
      apps/app/src/client/components/PageComment/DeleteCommentModal/DeleteCommentModal.module.scss
  42. 1 1
      apps/app/src/client/components/PageComment/ReplyComments.module.scss
  43. 6 6
      apps/app/src/client/components/PageComment/SwitchingButtonGroup.module.scss
  44. 6 6
      apps/app/src/client/components/PageControls/BookmarkButtons.module.scss
  45. 6 6
      apps/app/src/client/components/PageControls/LikeButtons.module.scss
  46. 4 4
      apps/app/src/client/components/PageControls/PageControls.module.scss
  47. 1 1
      apps/app/src/client/components/PageControls/SearchButton.module.scss
  48. 5 5
      apps/app/src/client/components/PageControls/SeenUserInfo.module.scss
  49. 2 2
      apps/app/src/client/components/PageControls/SubscribeButton.module.scss
  50. 1 1
      apps/app/src/client/components/PageControls/user-list-popover.module.scss
  51. 3 3
      apps/app/src/client/components/PageCreateModal.module.scss
  52. 2 2
      apps/app/src/client/components/PageEditor/ConflictDiffModal/ConflictDiffModal.module.scss
  53. 1 1
      apps/app/src/client/components/PageEditor/EditorNavbar/EditingUserList.module.scss
  54. 1 1
      apps/app/src/client/components/PageEditor/EditorNavbar/EditorNavbar.module.scss
  55. 7 7
      apps/app/src/client/components/PageEditor/EditorNavbarBottom/EditorNavbarBottom.module.scss
  56. 15 15
      apps/app/src/client/components/PageEditor/GridEditModal.module.scss
  57. 10 10
      apps/app/src/client/components/PageEditor/HandsontableModal/HandsontableModal.module.scss
  58. 2 1
      apps/app/src/client/components/PageEditor/HandsontableModal/HandsontableModal.tsx
  59. 6 0
      apps/app/src/client/components/PageEditor/HandsontableModal/HandsontableModal.vendor-styles.ts
  60. 1 1
      apps/app/src/client/components/PageEditor/PageEditor.tsx
  61. 5 5
      apps/app/src/client/components/PageEditor/Preview.module.scss
  62. 1 1
      apps/app/src/client/components/PageHeader/PageHeader.module.scss
  63. 5 5
      apps/app/src/client/components/PageHeader/PagePathHeader.module.scss
  64. 4 4
      apps/app/src/client/components/PageHeader/PageTitleHeader.module.scss
  65. 2 2
      apps/app/src/client/components/PageHistory/PageRevisionTable.module.scss
  66. 4 4
      apps/app/src/client/components/PageHistory/Revision.module.scss
  67. 4 4
      apps/app/src/client/components/PageHistory/RevisionDiff.module.scss
  68. 1 1
      apps/app/src/client/components/PageHistory/RevisionDiff.tsx
  69. 6 0
      apps/app/src/client/components/PageHistory/RevisionDiff.vendor-styles.ts
  70. 3 3
      apps/app/src/client/components/PagePathNavSticky/PagePathNavSticky.module.scss
  71. 5 5
      apps/app/src/client/components/PagePresentationModal/PagePresentationModal.module.scss
  72. 6 6
      apps/app/src/client/components/PageSelectModal/TreeItemForModal.module.scss
  73. 5 5
      apps/app/src/client/components/PageSideContents/PageAccessoriesControl.module.scss
  74. 6 6
      apps/app/src/client/components/PageStatusAlert.module.scss
  75. 6 6
      apps/app/src/client/components/PageTags/TagEditModal/TagsInput.module.scss
  76. 6 6
      apps/app/src/client/components/PageTags/TagLabels.module.scss
  77. 3 3
      apps/app/src/client/components/PageTimeline.module.scss
  78. 1 1
      apps/app/src/client/components/Presentation/Presentation.tsx
  79. 6 0
      apps/app/src/client/components/Presentation/Presentation.vendor-styles.ts
  80. 0 2
      apps/app/src/client/components/Presentation/Slides.tsx
  81. 4 4
      apps/app/src/client/components/ReactMarkdownComponents/DrawioViewerWithEditButton.module.scss
  82. 2 2
      apps/app/src/client/components/ReactMarkdownComponents/DrawioViewerWithEditButton.tsx
  83. 6 0
      apps/app/src/client/components/ReactMarkdownComponents/DrawioViewerWithEditButton.vendor-styles.ts
  84. 9 9
      apps/app/src/client/components/ReactMarkdownComponents/Header.module.scss
  85. 2 2
      apps/app/src/client/components/ReactMarkdownComponents/RichAttachment.module.scss
  86. 4 4
      apps/app/src/client/components/ReactMarkdownComponents/TableWithEditButton.module.scss
  87. 10 10
      apps/app/src/client/components/RevisionComparer/RevisionComparer.module.scss
  88. 8 8
      apps/app/src/client/components/SearchTypeahead.module.scss
  89. 3 3
      apps/app/src/client/components/ShortcutsModal/ShortcutsModal.module.scss
  90. 8 8
      apps/app/src/client/components/Sidebar/AppTitle/AppTitle.module.scss
  91. 2 2
      apps/app/src/client/components/Sidebar/Custom/CustomSidebarSubstance.module.scss
  92. 10 10
      apps/app/src/client/components/Sidebar/PageCreateButton/CreateButton.module.scss
  93. 11 11
      apps/app/src/client/components/Sidebar/PageCreateButton/DropendToggle.module.scss
  94. 11 11
      apps/app/src/client/components/Sidebar/PageTreeItem/PageTreeItem.module.scss
  95. 13 13
      apps/app/src/client/components/Sidebar/RecentChanges/RecentChangesSubstance.module.scss
  96. 7 7
      apps/app/src/client/components/Sidebar/ResizableArea/ResizableArea.module.scss
  97. 51 60
      apps/app/src/client/components/Sidebar/Sidebar.module.scss
  98. 0 2
      apps/app/src/client/components/Sidebar/Sidebar.tsx
  99. 3 3
      apps/app/src/client/components/Sidebar/SidebarContents.module.scss
  100. 2 2
      apps/app/src/client/components/Sidebar/SidebarHead/SidebarHead.module.scss

+ 1 - 3
.kiro/steering/tech.md

@@ -6,9 +6,7 @@ See: `.claude/skills/tech-stack/SKILL.md` (auto-loaded by Claude Code)
 
 ### Bundler Strategy (Project-Wide Decision)
 
-GROWI uses **Webpack** (not Turbopack) across all Next.js applications. Turbopack is the default in Next.js 16, but GROWI opts out via `--webpack` flag due to custom webpack configuration that Turbopack does not support.
-
-Turbopack migration is deferred as a separate initiative. See `apps/app/.claude/skills/build-optimization/SKILL.md` for details and blockers.
+GROWI uses **Turbopack** (Next.js 16 default) for development. Webpack fallback is available via `USE_WEBPACK=1` environment variable for debugging. Production builds still use `next build --webpack`. All custom webpack loaders/plugins have been migrated to Turbopack equivalents (`turbopack.rules`, `turbopack.resolveAlias`). See `apps/app/.claude/skills/build-optimization/SKILL.md` for details.
 
 ### Import Optimization Principles
 

+ 4 - 2
apps/app/.claude/skills/app-commands/SKILL.md

@@ -101,10 +101,12 @@ Generated specs output to `tmp/openapi-spec-apiv3.json`.
 
 ```bash
 # Development mode
-pnpm run dev:pre:styles
+pnpm run dev:pre:styles-commons
+pnpm run dev:pre:styles-components
 
 # Production mode
-pnpm run pre:styles
+pnpm run pre:styles-commons
+pnpm run pre:styles-commons-components
 ```
 
 Pre-builds SCSS styles into CSS bundles using Vite.

+ 82 - 35
apps/app/.claude/skills/build-optimization/SKILL.md

@@ -1,58 +1,110 @@
 ---
 name: build-optimization
-description: GROWI apps/app webpack configuration, module optimization, and build measurement tooling. Auto-invoked when working in apps/app.
-user-invocable: false
+description: GROWI apps/app Turbopack configuration, module optimization, and build measurement tooling. Auto-invoked when working in apps/app.
+user-invokable: false
 ---
 
 # Build Optimization (apps/app)
 
-## Next.js Version & Bundler Strategy
+## Next.js Version & Bundler
 
-- **Next.js 16** (`^16.0.0`) with **Webpack** bundler (not Turbopack)
-- Turbopack is the default in v16, but GROWI opts out via `--webpack` flag due to custom webpack configuration
-- Build: `next build --webpack`; Dev: Express server calls `next({ dev })` which uses webpack when `webpack()` config exists
+- **Next.js 16** (`^16.0.0`) with **Turbopack** bundler (default)
+- Build: `next build`; Dev: Express server calls `next({ dev })` which uses Turbopack by default
 - React stays at `^18.2.0` — Pages Router has full React 18 support in v16
+- Webpack has been fully removed (no `webpack()` hook, no `--webpack` flag)
 
-## Custom Webpack Configuration
+## Turbopack Configuration
 
-| Component | File | Purpose |
-|-----------|------|---------|
-| **superjson-ssr-loader** | `src/utils/superjson-ssr-loader.js` | Auto-wraps `getServerSideProps` with SuperJSON serialization |
-| **null-loader rules** (7) | `next.config.ts` | Exclude server-only packages from client bundle |
-| **I18NextHMRPlugin** | `next.config.ts` | i18n hot module replacement in dev mode |
-| **ChunkModuleStatsPlugin** | `src/utils/next.config.utils.js` | Dev-time module count analysis (initial/async-only/total) |
-| **source-map-loader** | `next.config.ts` | Source map extraction in dev builds |
+### Custom Loader Rules (`turbopack.rules`)
 
-### null-loader Rules
+| Rule | Pattern | Condition | Purpose |
+|------|---------|-----------|---------|
+| superjson-ssr-loader | `*.page.ts`, `*.page.tsx` | `{ not: 'browser' }` (server-only) | Auto-wraps `getServerSideProps` with SuperJSON serialization |
 
-7 packages excluded from client bundle: `dtrace-provider`, `mongoose`, `mathjax-full`, `i18next-fs-backend`, `bunyan`, `bunyan-format`, `core-js`
+- Loaders are registered in `next.config.ts` under `turbopack.rules`
+- `condition: { not: 'browser' }` restricts the loader to server-side compilation only
+- `as: '*.ts'` / `as: '*.tsx'` tells Turbopack to continue processing the transformed output as TypeScript
 
-**Important**: Any changes to these loaders/plugins must be verified against the module count baseline.
+### Resolve Aliases (`turbopack.resolveAlias`)
+
+7 server-only packages + `fs` are aliased to `./src/lib/empty-module.ts` in browser context:
+
+| Package | Reason |
+|---------|--------|
+| `fs` | Node.js built-in, not available in browser |
+| `dtrace-provider` | Native module, server-only |
+| `mongoose` | MongoDB driver, server-only |
+| `i18next-fs-backend` | File-system i18n loader, server-only |
+| `bunyan` | Server-side logger |
+| `bunyan-format` | Server-side logger formatter |
+| `core-js` | Server-side polyfills |
+
+- Uses conditional `{ browser: './src/lib/empty-module.ts' }` syntax so server-side resolution is unaffected
+- `resolveAlias` requires **relative paths** (e.g., `./src/lib/empty-module.ts`), not absolute paths — absolute paths cause "server relative imports are not implemented yet" errors
+- If a new server-only package leaks into the client bundle, add it to `resolveAlias` with the same pattern
 
 ## SuperJSON Serialization Architecture
 
-The `next-superjson` SWC plugin was replaced by a custom webpack loader:
+The `next-superjson` SWC plugin was replaced by a custom loader:
 
-- **Build time**: `superjson-ssr-loader.js` auto-wraps `getServerSideProps` in `.page.{ts,tsx}` files with `withSuperJSONProps()`
+- **Build time**: `superjson-ssr-loader.ts` auto-wraps `getServerSideProps` in `.page.{ts,tsx}` files with `withSuperJSONProps()` via Turbopack `rules`
 - **Runtime (server)**: `withSuperJSONProps()` in `src/pages/utils/superjson-ssr.ts` serializes props via superjson
 - **Runtime (client)**: `_app.page.tsx` calls `deserializeSuperJSONProps()` for centralized deserialization
 - **No per-page changes needed** — new pages automatically get superjson serialization
 - Custom serializers registered in `_app.page.tsx` (ObjectId, PageRevisionWithMeta)
 
+## CSS Modules Turbopack Compatibility
+
+### `:global` Syntax
+
+Turbopack only supports the **function form** `:global(...)`. The block form `:global { ... }` is NOT supported:
+
+```scss
+// WRONG — Turbopack rejects this
+.parent :global {
+  .child { color: red; }
+}
+
+// CORRECT — function form
+.parent {
+  :global(.child) { color: red; }
+}
+```
+
+Nested blocks must also use function form:
+
+```scss
+// WRONG
+.parent :global {
+  .child {
+    .grandchild { }
+  }
+}
+
+// CORRECT
+.parent {
+  :global(.child) {
+    :global(.grandchild) { }
+  }
+}
+```
+
+### Other Turbopack CSS Restrictions
+
+- **Standalone `:local` / `&:local`**: Not supported. Inside `:global(...)`, properties are locally scoped by default — remove `&:local` wrappers
+- **`@extend` with `:global()`**: `@extend .class` fails when target is wrapped in `:global(.class)` — Sass doesn't match them as the same selector. Use shared selector groups (comma-separated selectors) instead
+- **IE CSS hacks**: `*zoom:1`, `*display:inline`, `filter:alpha()` cannot be parsed by Turbopack's CSS parser (lightningcss). Avoid CSS files containing these hacks
+
+### Vendor CSS Imports
+
+Global CSS cannot be imported from files other than `_app.page.tsx` under Turbopack Pages Router. See the `vendor-styles-components` skill for the precompilation system that handles per-component vendor CSS.
+
 ## Module Optimization Configuration
 
 - `bundlePagesRouterDependencies: true` — bundles server-side dependencies for Pages Router
 - `serverExternalPackages: ['handsontable']` — packages excluded from server-side bundling
 - `optimizePackageImports` — 11 `@growi/*` packages configured (expansion to third-party packages was tested and reverted — it increased dev module count)
 
-## Module Count Measurement
-
-KPI: `[ChunkModuleStats] initial: N, async-only: N, total: N`
-
-- `initial` = modules in eager (initial) chunks — the primary reduction target
-- Measured via `bin/measure-chunk-stats.sh` (cleans `.next`, starts `next dev`, triggers compilation)
-- Any changes to webpack config or import patterns should be verified against the `initial` count
-
 ## Effective Module Reduction Techniques
 
 Techniques that have proven effective for reducing module count, ordered by typical impact:
@@ -63,7 +115,7 @@ Techniques that have proven effective for reducing module count, ordered by typi
 | `next/dynamic({ ssr: false })` | Client-only heavy components (e.g., Mermaid diagrams, interactive editors) |
 | Subpath imports | Packages with large barrel exports (e.g., `date-fns/format` instead of `date-fns`) |
 | Deep ESM imports | Packages that re-export multiple engines via barrel (e.g., `react-syntax-highlighter/dist/esm/prism-async-light`) |
-| null-loader | Server-only packages leaking into client bundle via transitive imports |
+| resolveAlias | Server-only packages leaking into client bundle via transitive imports |
 | Lightweight replacements | Replace large libraries used for a single feature (e.g., `tinykeys` instead of `react-hotkeys`, regex instead of `validator`) |
 
 ### Techniques That Did NOT Work
@@ -71,11 +123,6 @@ Techniques that have proven effective for reducing module count, ordered by typi
 - **Expanding `optimizePackageImports` to third-party packages** — In dev mode, this resolves individual sub-module files instead of barrel, resulting in MORE module entries. Reverted.
 - **Refactoring internal barrel exports** — Internal barrels (`states/`, `features/`) are small and well-scoped; refactoring had no measurable impact.
 
-## Turbopack Migration Path (Future)
-
-Turbopack adoption is deferred. Key blockers:
+## i18n HMR
 
-- `webpack()` config not supported — null-loader rules need `turbopack.resolveAlias` migration
-- Custom loaders (superjson-ssr-loader) need Turbopack rules testing
-- I18NextHMRPlugin has no Turbopack equivalent
-- Use `--webpack` flag in both dev and build until migration is complete
+`I18NextHMRPlugin` was removed during the Turbopack migration. Translation file changes require a manual browser refresh. The performance gain from Turbopack (faster Fast Refresh overall) outweighs the loss of i18n-specific HMR. Monitor if `i18next-hmr` adds Turbopack support in the future.

+ 116 - 0
apps/app/.claude/skills/vendor-styles-components/SKILL.md

@@ -0,0 +1,116 @@
+---
+name: vendor-styles-components
+description: Vendor CSS precompilation system for Turbopack compatibility. How to add third-party CSS to components without violating Pages Router global CSS restriction. Auto-invoked when working in apps/app.
+---
+
+# Vendor CSS Precompilation (apps/app)
+
+## Problem
+
+Turbopack (Pages Router) strictly enforces: **global CSS can only be imported from `_app.page.tsx`**. Components cannot `import 'package/style.css'` directly — Turbopack rejects these at compile time.
+
+Centralizing all vendor CSS in `_app` would degrade FCP for pages that don't need those styles.
+
+## Solution: Two-Track Vendor CSS System
+
+### Commons Track (globally shared CSS)
+
+- **File**: `src/styles/vendor.scss`
+- **For**: CSS needed on most pages (e.g., `simplebar-react`)
+- **Mechanism**: Compiled via `vite.vendor-styles-commons.ts` into `src/styles/prebuilt/`
+- **Imported from**: `_app.page.tsx`
+
+### Components Track (component-specific CSS)
+
+- **For**: CSS needed only by specific components
+- **Mechanism**: Vite precompiles `*.vendor-styles.ts` entry points into `*.vendor-styles.prebuilt.js` using `?inline` CSS import suffix
+- **Output**: Pure JS modules (no CSS imports) — Turbopack sees them as regular JS
+
+## How It Works
+
+1. **Entry point** (`ComponentName.vendor-styles.ts`): imports CSS via Vite `?inline` suffix, which inlines CSS as a string
+2. **Runtime injection**: the entry point creates a `<style>` tag and appends CSS to `document.head`
+3. **Vite prebuild** (`pre:styles-components` Turborepo task): compiles entry points into `*.vendor-styles.prebuilt.js`
+4. **Component import**: imports the `.prebuilt.js` file instead of raw CSS
+
+### Entry Point Template
+
+```typescript
+// @ts-nocheck -- Processed by Vite only; ?inline is a Vite-specific import suffix
+import css from 'some-package/dist/style.css?inline';
+
+const s = document.createElement('style');
+s.textContent = css;
+document.head.appendChild(s);
+```
+
+For multiple CSS sources in one component:
+
+```typescript
+// @ts-nocheck
+import css1 from 'package-a/style.css?inline';
+import css2 from 'package-b/style.css?inline';
+
+const s = document.createElement('style');
+s.textContent = css1 + css2;
+document.head.appendChild(s);
+```
+
+## Current Entry Points
+
+| Entry Point | CSS Sources | Consuming Components |
+|---|---|---|
+| `Renderer.vendor-styles.ts` | `@growi/remark-lsx`, `@growi/remark-attachment-refs`, `katex` | renderer.tsx |
+| `GrowiEditor.vendor-styles.ts` | `@growi/editor` | PageEditor, CommentEditor |
+| `HandsontableModal.vendor-styles.ts` | `handsontable` (non-full variant) | HandsontableModal |
+| `DateRangePicker.vendor-styles.ts` | `react-datepicker` | DateRangePicker |
+| `RevisionDiff.vendor-styles.ts` | `diff2html` | RevisionDiff |
+| `DrawioViewerWithEditButton.vendor-styles.ts` | `@growi/remark-drawio` | DrawioViewerWithEditButton |
+| `ImageCropModal.vendor-styles.ts` | `react-image-crop` | ImageCropModal |
+| `Presentation.vendor-styles.ts` | `@growi/presentation` | Presentation, Slides |
+
+## Adding New Vendor CSS
+
+1. Create `{ComponentName}.vendor-styles.ts` next to the consuming component:
+   ```typescript
+   // @ts-nocheck
+   import css from 'new-package/dist/style.css?inline';
+   const s = document.createElement('style');
+   s.textContent = css;
+   document.head.appendChild(s);
+   ```
+2. In the component, replace `import 'new-package/dist/style.css'` with:
+   ```typescript
+   import './ComponentName.vendor-styles.prebuilt';
+   ```
+3. Run `pnpm run pre:styles-components` (or let Turborepo handle it during `dev`/`build`)
+4. The `.prebuilt.js` file is git-ignored and auto-generated
+
+**Decision guide**: If the CSS is needed on nearly every page, add it to the commons track (`vendor.scss`) instead.
+
+## Font/Asset Handling
+
+When vendor CSS references external assets (e.g., KaTeX `@font-face` with `url(fonts/KaTeX_*.woff2)`):
+
+- Vite emits asset files to `src/assets/` during build
+- The `moveAssetsToPublic` plugin (in `vite.vendor-styles-components.ts`) relocates them to `public/static/fonts/`
+- URL references in prebuilt JS are rewritten from `/assets/` to `/static/fonts/`
+- Fonts are served by the existing `express.static(crowi.publicDir)` middleware
+- Both `public/static/fonts/` and `src/**/*.vendor-styles.prebuilt.js` are git-ignored
+
+## Build Pipeline Integration
+
+```
+turbo.json tasks:
+  pre:styles-components  →  build (dependency)
+  dev:pre:styles-components  →  dev (dependency)
+
+Inputs:  vite.vendor-styles-components.ts, src/**/*.vendor-styles.ts, package.json
+Outputs: src/**/*.vendor-styles.prebuilt.js, public/static/fonts/**
+```
+
+## Important Caveats
+
+- **SSR**: CSS is injected via `<style>` tags at runtime — not available during SSR. Most consuming components use `next/dynamic({ ssr: false })`, so FOUC is not a practical concern
+- **`@ts-nocheck`**: Required because `?inline` is a Vite-specific import suffix not understood by TypeScript
+- **handsontable**: Must use `handsontable/dist/handsontable.css` (non-full, non-minified). The "full" variant (`handsontable.full.min.css`) contains IE CSS hacks (`*zoom:1`, `filter:alpha()`) that Turbopack's CSS parser (lightningcss) cannot parse. The "full" variant also includes Pikaday which is unused.

+ 2 - 0
apps/app/.env.development

@@ -4,6 +4,8 @@
 ##
 MIGRATIONS_DIR=src/migrations/
 
+NEXT_TELEMETRY_DISABLED=1
+
 APP_SITE_URL=http://localhost:3000
 FILE_UPLOAD=mongodb
 # MONGO_GRIDFS_TOTAL_LIMIT=10485760

+ 3 - 0
apps/app/.gitignore

@@ -1,6 +1,7 @@
 # next.js
 /.next/
 /out/
+next-env.d.ts
 
 # test
 .reg
@@ -9,10 +10,12 @@
 /build/
 /dist/
 /transpiled/
+/public/static/fonts
 /public/static/js
 /public/static/styles
 /public/uploads
 /src/styles/prebuilt
+/src/**/*.vendor-styles.prebuilt.js
 /tmp/
 
 # cache

+ 2 - 8
apps/app/config/next-i18next.config.js

@@ -1,5 +1,4 @@
 const isDev = process.env.NODE_ENV === 'development';
-
 // biome-ignore lint/style/useNodejsImportProtocol: ignore
 const path = require('path');
 
@@ -8,8 +7,6 @@ const { isServer } = require('@growi/core/dist/utils');
 
 const { defaultLang } = require('./i18next.config');
 
-const HMRPlugin = isDev ? require('i18next-hmr/plugin').HMRPlugin : undefined;
-
 /** @type {import('next-i18next').UserConfig} */
 module.exports = {
   ...require('./i18next.config').initOptions,
@@ -24,11 +21,8 @@ module.exports = {
 
   use: isDev
     ? isServer()
-      ? [new HMRPlugin({ webpack: { server: true } })]
-      : [
-          require('i18next-chained-backend').default,
-          new HMRPlugin({ webpack: { client: true } }),
-        ]
+      ? []
+      : [require('i18next-chained-backend').default]
     : [],
   backend: {
     backends: isServer()

+ 0 - 6
apps/app/next-env.d.ts

@@ -1,6 +0,0 @@
-/// <reference types="next" />
-/// <reference types="next/image-types/global" />
-import "./.next/dev/types/routes.d.ts";
-
-// NOTE: This file should not be edited
-// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.

+ 35 - 61
apps/app/next.config.ts

@@ -14,12 +14,9 @@ import path from 'node:path';
 import bundleAnalyzer from '@next/bundle-analyzer';
 
 import nextI18nConfig from './config/next-i18next.config';
-import {
-  createChunkModuleStatsPlugin,
-  listPrefixedPackages,
-} from './src/utils/next.config.utils';
+import { listPrefixedPackages } from './src/utils/next.config.utils';
 
-const { i18n, localePath } = nextI18nConfig;
+const { i18n } = nextI18nConfig;
 
 const getTranspilePackages = (): string[] => {
   const packages = [
@@ -117,62 +114,39 @@ export default (phase: string): NextConfig => {
       optimizePackageImports,
     },
 
-    webpack(config, options) {
-      // Auto-wrap getServerSideProps with superjson serialization (replaces next-superjson SWC plugin)
-      if (options.isServer) {
-        config.module!.rules!.push({
-          test: /\.page\.(tsx|ts)$/,
-          use: [path.resolve(__dirname, 'src/utils/superjson-ssr-loader.ts')],
-        });
-      }
-
-      if (!options.isServer) {
-        // Avoid "Module not found: Can't resolve 'fs'"
-        // See: https://stackoverflow.com/a/68511591
-        config.resolve!.fallback = { ...config.resolve!.fallback, fs: false };
-
-        // exclude packages from the output bundles
-        config.module!.rules!.push(
-          ...[
-            /dtrace-provider/,
-            /mongoose/,
-            /mathjax-full/, // required from marp
-            /i18next-fs-backend/, // server-only filesystem translation backend (leaks via next-i18next)
-            /\/bunyan\//, // server-only logging (client uses browser-bunyan via universal-bunyan)
-            /bunyan-format/, // server-only log formatter (client uses @browser-bunyan/console-formatted-stream)
-            /[\\/]core-js[\\/]/, // polyfills baked into next-i18next/react-stickynode dist; all APIs natively supported by target browsers (Chrome 64+, Safari 12+)
-          ].map((packageRegExp) => {
-            return {
-              test: packageRegExp,
-              use: 'null-loader',
-            };
-          }),
-        );
-      }
-
-      // extract sourcemap
-      if (options.dev) {
-        config.module!.rules!.push({
-          test: /.(c|m)?js$/,
-          exclude: [/node_modules/, path.resolve(__dirname)],
-          enforce: 'pre',
-          use: ['source-map-loader'],
-        });
-      }
-
-      // setup i18next-hmr
-      if (!options.isServer && options.dev) {
-        const { I18NextHMRPlugin } = require('i18next-hmr/webpack');
-        config.plugins!.push(new I18NextHMRPlugin({ localesDir: localePath }));
-      }
-
-      // Log eager vs lazy module counts for dev compilation analysis
-      if (!options.isServer && options.dev) {
-        // biome-ignore lint/suspicious/noExplicitAny: webpack plugin type compatibility
-        config.plugins!.push(createChunkModuleStatsPlugin() as any);
-      }
-
-      return config;
+    turbopack: {
+      rules: {
+        // Server-only: auto-wrap getServerSideProps with SuperJSON serialization
+        '*.page.ts': [
+          {
+            condition: { not: 'browser' },
+            loaders: [
+              path.resolve(__dirname, 'src/utils/superjson-ssr-loader.ts'),
+            ],
+            as: '*.ts',
+          },
+        ],
+        '*.page.tsx': [
+          {
+            condition: { not: 'browser' },
+            loaders: [
+              path.resolve(__dirname, 'src/utils/superjson-ssr-loader.ts'),
+            ],
+            as: '*.tsx',
+          },
+        ],
+      },
+      resolveAlias: {
+        // Exclude fs from client bundle
+        fs: { browser: './src/lib/empty-module.ts' },
+        // Exclude server-only packages from client bundle
+        'dtrace-provider': { browser: './src/lib/empty-module.ts' },
+        mongoose: { browser: './src/lib/empty-module.ts' },
+        'i18next-fs-backend': { browser: './src/lib/empty-module.ts' },
+        bunyan: { browser: './src/lib/empty-module.ts' },
+        'bunyan-format': { browser: './src/lib/empty-module.ts' },
+        'core-js': { browser: './src/lib/empty-module.ts' },
+      },
     },
   };
 

+ 6 - 6
apps/app/package.json

@@ -7,18 +7,20 @@
     "//// for production": "",
     "build": "run-p build:*",
     "start": "next start",
-    "build:client": "next build --webpack",
+    "build:client": "next build",
     "build:server": "cross-env NODE_ENV=production tspc -p tsconfig.build.server.json",
     "postbuild:server": "shx echo \"Listing files under transpiled\" && shx ls transpiled && shx rm -rf dist && shx mv transpiled/src dist && shx rm -rf transpiled",
     "clean": "shx rm -rf dist transpiled .next",
     "server": "cross-env NODE_ENV=production node -r dotenv-flow/config dist/server/app.js",
     "server:ci": "pnpm run server --ci",
     "preserver": "cross-env NODE_ENV=production pnpm run migrate",
-    "pre:styles": "vite build -c vite.styles-prebuilt.config.ts",
+    "pre:styles-commons": "vite build -c vite.vendor-styles-commons.ts",
+    "pre:styles-components": "vite build --config vite.vendor-styles-components.ts",
     "migrate": "node -r dotenv-flow/config node_modules/migrate-mongo/bin/migrate-mongo up -f config/migrate-mongo-config.js",
     "//// for development": "",
     "dev": "cross-env NODE_ENV=development nodemon --exec pnpm run ts-node --inspect src/server/app.ts",
-    "dev:pre:styles": "pnpm run pre:styles --mode dev",
+    "dev:pre:styles-commons": "pnpm run pre:styles-commons --mode dev",
+    "dev:pre:styles-components": "pnpm run pre:styles-components",
     "dev:migrate-mongo": "cross-env NODE_ENV=development pnpm run ts-node node_modules/migrate-mongo/bin/migrate-mongo",
     "dev:migrate": "pnpm run dev:migrate:status > tmp/cache/migration-status.out && pnpm run dev:migrate:up",
     "dev:migrate:status": "pnpm run dev:migrate-mongo status -f config/migrate-mongo-config.js",
@@ -32,6 +34,7 @@
     "lint:openapi:apiv3": "node node_modules/swagger2openapi/oas-validate tmp/openapi-spec-apiv3.json",
     "lint:openapi:apiv1": "node node_modules/swagger2openapi/oas-validate tmp/openapi-spec-apiv1.json",
     "lint": "run-p lint:**",
+    "prelint:typecheck": "next typegen",
     "prelint:openapi:apiv3": "pnpm run openapi:generate-spec:apiv3",
     "prelint:openapi:apiv1": "pnpm run openapi:generate-spec:apiv1",
     "test": "vitest run",
@@ -308,7 +311,6 @@
     "handsontable": "=6.2.2",
     "happy-dom": "^15.7.4",
     "i18next-chained-backend": "^4.6.2",
-    "i18next-hmr": "^3.1.3",
     "i18next-http-backend": "^2.6.2",
     "i18next-localstorage-backend": "^4.2.0",
     "jotai-devtools": "^0.11.0",
@@ -319,7 +321,6 @@
     "mongodb-connection-string-url": "^7.0.0",
     "mongodb-memory-server-core": "^9.1.1",
     "morgan": "^1.10.0",
-    "null-loader": "^4.0.1",
     "openapi-typescript": "^7.8.0",
     "pretty-bytes": "^6.1.1",
     "react-copy-to-clipboard": "^5.0.1",
@@ -334,7 +335,6 @@
     "sass": "^1.53.0",
     "simplebar-react": "^2.3.6",
     "socket.io-client": "^4.7.5",
-    "source-map-loader": "^4.0.1",
     "supertest": "^7.1.4",
     "swagger2openapi": "^7.0.8",
     "tinykeys": "^3.0.0",

+ 2 - 1
apps/app/src/client/components/Admin/AuditLog/DateRangePicker.tsx

@@ -3,7 +3,8 @@ import { forwardRef, useCallback } from 'react';
 import { addDays } from 'date-fns/addDays';
 import { format } from 'date-fns/format';
 import DatePicker from 'react-datepicker';
-import 'react-datepicker/dist/react-datepicker.css';
+
+import './DateRangePicker.vendor-styles.prebuilt';
 
 type CustomInputProps = {
   value?: string;

+ 6 - 0
apps/app/src/client/components/Admin/AuditLog/DateRangePicker.vendor-styles.ts

@@ -0,0 +1,6 @@
+// @ts-nocheck -- Processed by Vite only; ?inline is a Vite-specific import suffix
+import css from 'react-datepicker/dist/react-datepicker.css?inline';
+
+const s = document.createElement('style');
+s.textContent = css;
+document.head.appendChild(s);

+ 1 - 1
apps/app/src/client/components/Admin/Customize/ThemeColorBox.module.scss

@@ -1,6 +1,6 @@
 @use '@growi/core-styles/scss/bootstrap/init' as bs;
 
 // layout
-.theme-option-container :global {
+.theme-option-container {
   min-width: 100px;
 }

+ 3 - 5
apps/app/src/client/components/Admin/UserGroup/UserGroupTable.module.scss

@@ -4,15 +4,13 @@
 
 // switch visibility of the edit icon
 .user-group-edit-link {
-  :global {
-    .grw-edit-icon {
+    :global(.grw-edit-icon) {
       visibility: hidden;
     }
-  }
 
-  &:global {
+  & {
     &:hover {
-      .grw-edit-icon {
+      :global(.grw-edit-icon) {
         visibility: visible;
       }
     }

+ 9 - 9
apps/app/src/client/components/Admin/UserManagement.module.scss

@@ -1,18 +1,18 @@
 @use '@growi/core-styles/scss/bootstrap/init' as bs;
 
 // styles for admin user search
-.search-typeahead :global {
+.search-typeahead {
   position: relative;
   width: 100%;
 
   // corner radius
   border-top-right-radius: bs.$border-radius;
   border-bottom-right-radius: bs.$border-radius;
-  .rbt-input-main,
-  input {
+  :global(.rbt-input-main),
+  :global(input) {
     padding-right: 36px;
   }
-  .search-clear {
+  :global(.search-clear) {
     position: absolute;
     top: 50%;
     right: 6px;
@@ -25,22 +25,22 @@
     transform: translateY(-50%);
   }
 
-  .rbt-menu {
+  :global(.rbt-menu) {
     max-height: none !important;
     margin-top: 3px;
 
-    li a span {
-      .page-path {
+    :global(li a span) {
+      :global(.page-path) {
         display: inline;
         padding: 0 4px;
         color: inherit;
       }
 
-      .page-list-meta {
+      :global(.page-list-meta) {
         font-size: 0.9em;
         color: bs.$gray-400;
 
-        > span {
+        > :global(span) {
           margin-right: 0.3rem;
         }
       }

+ 3 - 3
apps/app/src/client/components/Admin/Users/ExternalAccountTable.module.scss

@@ -1,8 +1,8 @@
-.ea-table :global {
-  thead th {
+.ea-table {
+  :global(thead th) {
     vertical-align: top;
   }
-  td {
+  :global(td) {
     vertical-align: middle;
   }
 }

+ 1 - 1
apps/app/src/client/components/Admin/Users/UserMenu.module.scss

@@ -1,4 +1,4 @@
-.grw-usermenu-notification-icon :global {
+.grw-usermenu-notification-icon {
   position: absolute;
   top: -6px;
   left: 3px;

+ 4 - 4
apps/app/src/client/components/AuthorInfo/AuthorInfo.module.scss

@@ -3,19 +3,19 @@
 $author-font-size: 12px;
 $date-font-size: 12px;
 
-.grw-author-info :global {
+.grw-author-info {
   font-size: $author-font-size;
 
-  .text-date {
+  :global(.text-date) {
     font-size: $date-font-size;
   }
 
-  .user-picture {
+  :global(.user-picture) {
     width: 22px;
     height: 22px;
     border: 1px solid bs.$gray-300;
 
-    &.user-picture-xs {
+    &:global(.user-picture-xs) {
       width: 14px;
       height: 14px;
     }

+ 3 - 3
apps/app/src/client/components/Bookmarks/BookmarkFolderMenu.module.scss

@@ -1,10 +1,10 @@
-.grw-bookmark-folder-menu  :global {
+.grw-bookmark-folder-menu {
   max-width: 65%;
 
-  .grw-bookmark-folder-menu-item-folder-first {
+  :global(.grw-bookmark-folder-menu-item-folder-first) {
     padding-left: 40px;
   }
-  .grw-bookmark-folder-menu-item-folder-second {
+  :global(.grw-bookmark-folder-menu-item-folder-second) {
     padding-left: 60px;
   }
 }

+ 20 - 20
apps/app/src/client/components/Bookmarks/BookmarkFolderTree.module.scss

@@ -1,54 +1,54 @@
 $grw-foldertree-item-padding-left: 15px;
 $grw-bookmark-item-padding-left: 35px;
 
-.grw-folder-tree-container :global {
-  .grw-foldertree-item-container, .grw-drop-item-area {
-    & .grw-accept-drop-item {
+.grw-folder-tree-container {
+  :global(.grw-foldertree-item-container), :global(.grw-drop-item-area) {
+    & :global(.grw-accept-drop-item) {
       border-style: dashed !important;
       border-width: 0.15rem !important;
     }
   }
 
-  .grw-drop-item-area {
+  :global(.grw-drop-item-area) {
     padding: 1rem;
-    & .grw-accept-drop-item {
+    & :global(.grw-accept-drop-item) {
       padding: 0.7rem;
     }
   }
-  .grw-drag-drop-container > .grw-drop-item-area {
+  :global(.grw-drag-drop-container > .grw-drop-item-area) {
     margin: 1rem;
     border-style: dashed !important;
     border-width: 0.15rem !important;
   }
 }
 
-.grw-foldertree :global {
+.grw-foldertree {
 
-  .list-group-item {
-    .grw-visible-on-hover {
+  :global(.list-group-item) {
+    :global(.grw-visible-on-hover) {
       display: none;
     }
 
     &:hover {
-      .grw-visible-on-hover {
+      :global(.grw-visible-on-hover) {
         display: block;
       }
-      .page-list-meta {
+      :global(.page-list-meta) {
         display: none;
       }
     }
 
-    .grw-foldertree-triangle-btn {
+    :global(.grw-foldertree-triangle-btn) {
       border: 0;
       transition: all 0.2s ease-out;
       transform: rotate(0deg);
 
-      &.grw-foldertree-open {
+      &:global(.grw-foldertree-open) {
         transform: rotate(90deg);
       }
     }
 
-    .grw-foldertree-title-anchor {
+    :global(.grw-foldertree-title-anchor) {
       width: 100%;
       overflow: hidden;
       font-size: 14px;
@@ -56,27 +56,27 @@ $grw-bookmark-item-padding-left: 35px;
     }
   }
 
-  .grw-foldertree-item-container {
-    .grw-triangle-container {
+  :global(.grw-foldertree-item-container) {
+    :global(.grw-triangle-container) {
       height:30px;
     }
 
-    .grw-bookmark-item-list{
+    :global(.grw-bookmark-item-list){
       min-width: 30px;
       height: 50px;
 
-      .user-picture {
+      :global(.user-picture) {
         width: 16px;
         height: 16px;
         vertical-align: text-bottom;
 
-        &.user-picture-md {
+        &:global(.user-picture-md) {
           width: 20px;
           height: 20px;
         }
       }
 
-      .grw-foldertree-control{
+      :global(.grw-foldertree-control){
         margin-left: auto;
       }
     }

+ 6 - 6
apps/app/src/client/components/Common/CopyDropdown/CopyDropdown.module.scss

@@ -1,7 +1,7 @@
 @use '@growi/core-styles/scss/bootstrap/init' as bs;
 @use '@growi/ui/scss/atoms/btn-muted';
 
-.copy-clipboard-dropdown-menu :global {
+.copy-clipboard-dropdown-menu {
     min-width: 310px;
     max-width: 375px;
 
@@ -9,25 +9,25 @@
       max-width: 600px;
     }
 
-    .dropdown-header {
+    :global(.dropdown-header) {
       margin-bottom: 0.5em;
       font-size: 1.1em;
     }
 
     // unset active styles
-    .dropdown-item:active {
+    :global(.dropdown-item:active) {
       color: unset;
       background-color: unset;
     }
 
-    .card {
+    :global(.card) {
       font-size: 0.7em;
       word-break: break-all;
     }
   }
 
-.grw-copy-dropdown :global {
-  .btn.btn-copy {
+.grw-copy-dropdown {
+  :global(.btn.btn-copy) {
     @include btn-muted.colorize(bs.$gray-500);
   }
 }

+ 2 - 2
apps/app/src/client/components/Common/DrawerToggler/DrawerToggler.module.scss

@@ -2,8 +2,8 @@
 @use 'styles/variables' as var;
 
 
-.grw-drawer-toggler :global {
-  .btn {
+.grw-drawer-toggler {
+  :global(.btn) {
     --bs-btn-color: rgba(var(--bs-tertiary-color-rgb), 0.5);
     --bs-btn-bg: transparent;
     --bs-btn-hover-color: rgba(var(--bs-tertiary-color-rgb), 0.7);

+ 3 - 2
apps/app/src/client/components/Common/ImageCropModal.tsx

@@ -1,5 +1,5 @@
 import type { FC } from 'react';
-import React, { useCallback, useEffect, useState } from 'react';
+import { useCallback, useEffect, useState } from 'react';
 import canvasToBlob from 'async-canvas-to-blob';
 import { useTranslation } from 'react-i18next';
 import ReactCrop from 'react-image-crop';
@@ -7,7 +7,8 @@ import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
 
 import { toastError } from '~/client/util/toastr';
 import loggerFactory from '~/utils/logger';
-import 'react-image-crop/dist/ReactCrop.css';
+
+import './ImageCropModal.vendor-styles.prebuilt';
 
 const logger = loggerFactory('growi:ImageCropModal');
 

+ 6 - 0
apps/app/src/client/components/Common/ImageCropModal.vendor-styles.ts

@@ -0,0 +1,6 @@
+// @ts-nocheck -- Processed by Vite only; ?inline is a Vite-specific import suffix
+import css from 'react-image-crop/dist/ReactCrop.css?inline';
+
+const s = document.createElement('style');
+s.textContent = css;
+document.head.appendChild(s);

+ 2 - 2
apps/app/src/client/components/CompleteUserRegistrationForm.module.scss

@@ -1,8 +1,8 @@
 @use 'styles/atoms/placeholders/buttons';
 
 :root {
-  .complete-user-registration-form :global {
-    .btn-register {
+  .complete-user-registration-form {
+    :global(.btn-register) {
       @extend %btn-nologin;
       @extend %btn-register;
     }

+ 4 - 4
apps/app/src/client/components/CustomNavigation/CustomNav.module.scss

@@ -1,15 +1,15 @@
 @use '@growi/core-styles/scss/bootstrap/init' as bs;
 
-.grw-custom-nav-tab :global {
-  .nav-title {
+.grw-custom-nav-tab {
+  :global(.nav-title) {
     flex-wrap: nowrap;
   }
 
-  .nav-link {
+  :global(.nav-link) {
     padding: 1rem 1.5rem;
   }
 
-  .grw-nav-slide-hr {
+  :global(.grw-nav-slide-hr) {
     border-top: 0;
     border-bottom: 3px solid;
     transition: 0.3s ease-in-out;

+ 7 - 7
apps/app/src/client/components/DescendantsPageListModal/DescendantsPageListModal.module.scss

@@ -1,22 +1,22 @@
-.grw-descendants-page-list-modal :global {
-  .modal-header {
-    button.btn-close {
+.grw-descendants-page-list-modal {
+  :global(.modal-header) {
+    :global(button.btn-close) {
       margin: auto 0 auto auto;
     }
   }
 
-  .modal-body {
+  :global(.modal-body) {
     padding: 25px 30px;
   }
 
-  .grw-tab-content-style-md-down {
+  :global(.grw-tab-content-style-md-down) {
     padding-top: 25px;
   }
 
-  .grw-modal-body-style {
+  :global(.grw-modal-body-style) {
     max-height: calc(100vh - 100px);
   }
-  ul.pagination {
+  :global(ul.pagination) {
     margin-bottom: 0;
   }
 }

+ 6 - 0
apps/app/src/client/components/GrowiEditor.vendor-styles.ts

@@ -0,0 +1,6 @@
+// @ts-nocheck -- Processed by Vite only; ?inline is a Vite-specific import suffix
+import css from '@growi/editor/dist/style.css?inline';
+
+const s = document.createElement('style');
+s.textContent = css;
+document.head.appendChild(s);

+ 2 - 2
apps/app/src/client/components/InAppNotification/ModelNotification/ModelNotification.module.scss

@@ -1,5 +1,5 @@
-.modal-notification :global {
-  .page-title {
+.modal-notification {
+  :global(.page-title) {
     font-size: 14px;
   }
 }

+ 2 - 2
apps/app/src/client/components/InstallerForm.module.scss

@@ -1,8 +1,8 @@
 @use 'styles/atoms/placeholders/buttons';
 
 :root {
-  .installer-form :global {
-    .btn-register {
+  .installer-form {
+    :global(.btn-register) {
       @extend %btn-nologin;
       @extend %btn-register;
     }

+ 15 - 15
apps/app/src/client/components/LoginForm/LoginForm.module.scss

@@ -1,7 +1,7 @@
 @use '@growi/core-styles/scss/bootstrap/init' as bs;
 @use 'styles/atoms/placeholders/buttons';
 
-.login-form :global {
+.login-form {
   //
   // deactivated in order to fix https://redmine.weseek.co.jp/issues/143531 -- 2024.04.02 Yuki Takei
   //
@@ -12,17 +12,17 @@
   //   height: 0% !important;
   // }
 
-  .collapse-external-auth {
+  :global(.collapse-external-auth) {
     overflow: hidden;
   }
 
-  .link-growi-org {
+  :global(.link-growi-org) {
     position: absolute;
     bottom: 9px;
     z-index: 3;
   }
 
-  .text-line {
+  :global(.text-line) {
     &::before,
     &::after {
       flex-grow: 1;
@@ -34,11 +34,11 @@
   }
 
 
-  .ldap-space {
+  :global(.ldap-space) {
     padding-right: 76px;
   }
 
-  .input-ldap {
+  :global(.input-ldap) {
     position: absolute;
     top: 4px;
     right: 5px;
@@ -48,47 +48,47 @@
 
 // Button colors
 :root {
-  .login-form :global {
+  .login-form {
 
-    .btn {
+    :global(.btn) {
       @extend %btn-nologin;
     }
 
-    .btn-register {
+    :global(.btn-register) {
       @extend %btn-register;
     }
 
-    .btn-login {
+    :global(.btn-login) {
       --bs-btn-bg: #{rgba(#204986, 0.6)};
       --bs-btn-hover-bg: #{rgba(#204986, 0.8)};
       --bs-btn-active-bg: #{rgba(#204986, 0.8)};
     }
 
-    .btn-function {
+    :global(.btn-function) {
       --bs-btn-bg: #{rgba(bs.$gray-800, 0.8)};
       --bs-btn-hover-bg: #{rgba(bs.$gray-800, 0.5)};
       --bs-btn-active-bg: #{rgba(bs.$gray-800, 0.5)};
     }
 
-    .btn-auth-google {
+    :global(.btn-auth-google) {
       --bs-btn-bg: #{rgba(#4285F4, 0.4)};
       --bs-btn-hover-bg: #{rgba(#4285F4, 0.8)};
       --bs-btn-active-bg: #{rgba(#4285F4, 0.8)};
     }
 
-    .btn-auth-github {
+    :global(.btn-auth-github) {
       --bs-btn-bg: #{rgba(#403D3E, 0.4)};
       --bs-btn-hover-bg: #{rgba(#403D3E, 0.7)};
       --bs-btn-active-bg: #{rgba(#403D3E, 0.7)};
     }
 
-    .btn-auth-oidc {
+    :global(.btn-auth-oidc) {
       --bs-btn-bg: #{rgba(#835B1A, 0.4)};
       --bs-btn-hover-bg: #{rgba(#835B1A, 0.8)};
       --bs-btn-active-bg: #{rgba(#835B1A, 0.8)};
     }
 
-    .btn-auth-saml {
+    :global(.btn-auth-saml) {
       --bs-btn-bg: #{rgba(#138957, 0.4)};
       --bs-btn-hover-bg: #{rgba(#138957, 0.7)};
       --bs-btn-active-bg: #{rgba(#138957, 0.7)};

+ 12 - 12
apps/app/src/client/components/Me/AccessTokenScopeList.module.scss

@@ -1,35 +1,35 @@
 $baseMargin: 20px;
 
-.access-token-scope-list :global {
-  .indentation {
-    &.indentation-level-1 {
+.access-token-scope-list {
+  :global(.indentation) {
+    &:global(.indentation-level-1) {
       margin-left: $baseMargin;
     }
-    &.indentation-level-2 {
+    &:global(.indentation-level-2) {
       margin-left: $baseMargin * 2;
     }
-    &.indentation-level-3 {
+    &:global(.indentation-level-3) {
       margin-left: $baseMargin * 3;
     }
-    &.indentation-level-4 {
+    &:global(.indentation-level-4) {
       margin-left: $baseMargin * 4;
     }
-    &.indentation-level-5 {
+    &:global(.indentation-level-5) {
       margin-left: $baseMargin * 5;
     }
-    &.indentation-level-6 {
+    &:global(.indentation-level-6) {
       margin-left: $baseMargin * 6;
     }
-    &.indentation-level-7 {
+    &:global(.indentation-level-7) {
       margin-left: $baseMargin * 7;
     }
-    &.indentation-level-8 {
+    &:global(.indentation-level-8) {
       margin-left: $baseMargin * 8;
     }
-    &.indentation-level-9 {
+    &:global(.indentation-level-9) {
       margin-left: $baseMargin * 9;
     }
-    &.indentation-level-10 {
+    &:global(.indentation-level-10) {
       margin-left: $baseMargin * 10;
     }
   }

+ 4 - 4
apps/app/src/client/components/Navbar/GrowiNavbarBottom.module.scss

@@ -1,13 +1,13 @@
 @use 'styles/variables' as var;
 @use 'styles/mixins';
 
-.grw-navbar-bottom :global {
+.grw-navbar-bottom {
   // apply transition
   transition-property: bottom;
 
   @include mixins.apply-navigation-transition();
 
-  .navbar {
+  :global(.navbar) {
     height: var.$grw-navbar-bottom-height;
   }
 }
@@ -17,8 +17,8 @@
 }
 
 // centering icons
-.grw-navbar-bottom :global {
-  .nav-link {
+.grw-navbar-bottom {
+  :global(.nav-link) {
     display: flex;
     align-items: center;
   }

+ 7 - 7
apps/app/src/client/components/Navbar/PageEditorModeManager.module.scss

@@ -2,8 +2,8 @@
 @use '@growi/core-styles/scss/bootstrap/init' as bs;
 @use 'styles/mixins';
 
-.grw-page-editor-mode-manager :global {
-  .btn {
+.grw-page-editor-mode-manager {
+  :global(.btn) {
     --bs-btn-font-size: 13px;
     --bs-btn-border-width: 2px;
 
@@ -17,7 +17,7 @@
   }
 }
 
-.grw-page-editor-mode-manager-skeleton :global {
+.grw-page-editor-mode-manager-skeleton {
   width: 90px;
   height: 38px;
 
@@ -29,8 +29,8 @@
 
 // == Colors
 @include bs.color-mode(light) {
-  .grw-page-editor-mode-manager :global {
-    .btn {
+  .grw-page-editor-mode-manager {
+    :global(.btn) {
       $color: var(--grw-page-editor-mode-manager-btn-color, var(--grw-primary-700));
       $bg: var(--grw-page-editor-mode-manager-btn-bg, var(--grw-primary-100));
       $bg-rgb: var(--grw-page-editor-mode-manager-btn-bg-rgb, var(--grw-primary-100-rgb));
@@ -50,8 +50,8 @@
 }
 
 @include bs.color-mode(dark) {
-  .grw-page-editor-mode-manager :global {
-    .btn {
+  .grw-page-editor-mode-manager {
+    :global(.btn) {
       $color: var(--grw-page-editor-mode-manager-btn-color, var(--grw-primary-300));
       $bg: var(--grw-page-editor-mode-manager-btn-bg, var(--grw-primary-800));
       $bg-rgb: var(--grw-page-editor-mode-manager-btn-bg-rgb, var(--grw-primary-800-rgb));

+ 7 - 7
apps/app/src/client/components/PageAccessoriesModal/PageAccessoriesModal.module.scss

@@ -1,22 +1,22 @@
-.grw-page-accessories-modal :global {
-  .modal-header {
-    button.btn-close {
+.grw-page-accessories-modal {
+  :global(.modal-header) {
+    :global(button.btn-close) {
       margin: auto 0 auto auto;
     }
   }
 
-  .modal-body {
+  :global(.modal-body) {
     padding: 25px 30px;
   }
 
-  .grw-tab-content-style-md-down {
+  :global(.grw-tab-content-style-md-down) {
     padding-top: 25px;
   }
 
-  .grw-modal-body-style {
+  :global(.grw-modal-body-style) {
     max-height: calc(100vh - 100px);
   }
-  ul.pagination {
+  :global(ul.pagination) {
     margin-bottom: 0;
   }
 }

+ 3 - 3
apps/app/src/client/components/PageAttachment/DeleteAttachmentModal.module.scss

@@ -1,8 +1,8 @@
-.attachment-delete-modal :global {
-  .attachment-delete-image {
+.attachment-delete-modal {
+  :global(.attachment-delete-image) {
     text-align: center;
 
-    img {
+    :global(img) {
       max-width: 100%;
     }
   }

+ 9 - 9
apps/app/src/client/components/PageComment.module.scss

@@ -1,19 +1,19 @@
 @use '@growi/core-styles/scss/bootstrap/init' as bs;
 
-.page-comment-styles :global {
-  .page-comments {
-    h4 {
+.page-comment-styles {
+  :global(.page-comments) {
+    :global(h4) {
       margin-bottom: 1em;
     }
   }
 
   // reply button
-  .btn-comment-reply {
+  :global(.btn-comment-reply) {
     backdrop-filter: blur(10px);
   }
 
   // TODO: Refacotr Soft-coding
-  .page-comment-button-skeleton {
+  :global(.page-comment-button-skeleton) {
     width: 70.0167px;
     height: 26.3833px;
     margin-top: 0.5em;
@@ -23,8 +23,8 @@
 
 // // Light mode color
 @include bs.color-mode(light) {
-  .page-comment-styles :global {
-    .btn-comment-reply {
+  .page-comment-styles {
+    :global(.btn-comment-reply) {
       --bs-btn-color: #{( bs.$gray-600 )};
       --bs-btn-hover-color:  #{( bs.$gray-700 )};
       --bs-btn-bg: #{rgba( bs.$gray-200, 0.3 )};
@@ -37,8 +37,8 @@
 
 // dark mode color
 @include bs.color-mode(dark) {
-  .page-comment-styles :global {
-    .btn-comment-reply {
+  .page-comment-styles {
+    :global(.btn-comment-reply) {
       --bs-btn-color: #{( bs.$gray-500 )};
       --bs-btn-hover-color:  #{( bs.$gray-400 )};
       --bs-btn-bg: #{rgba( bs.$gray-800, 0.3 )};

+ 17 - 17
apps/app/src/client/components/PageComment/Comment.module.scss

@@ -2,31 +2,31 @@
 @use 'styles/variables' as var;
 @use './comment-inheritance';
 
-.comment-styles :global {
+.comment-styles {
 
-  .page-comment {
+  :global(.page-comment) {
     position: relative;
     pointer-events: none;
     scroll-margin-top: var.$grw-scroll-margin-top-in-view;
 
     // background
-    .bg-comment {
+    :global(.bg-comment) {
       @extend %bg-comment;
     }
 
     // user icon
-    .user-picture {
+    :global(.user-picture) {
       @extend %user-picture;
     }
 
     // comment section
-    .page-comment-main {
+    :global(.page-comment-main) {
       @extend %comment-section;
 
       pointer-events: auto;
 
       // delete button
-      .page-comment-control {
+      :global(.page-comment-control) {
         position: absolute;
         top: 0;
         right: 0;
@@ -39,9 +39,9 @@
     }
 
     // comment body
-    .page-comment-body {
+    :global(.page-comment-body) {
       word-wrap: break-word;
-      .wiki p {
+      :global(.wiki p) {
         margin: 8px 0;
       }
     }
@@ -51,7 +51,7 @@
     // }
 
     // newer comments
-    &.page-comment-newer {
+    &:global(.page-comment-newer) {
       opacity: 0.7;
 
       &:hover {
@@ -59,15 +59,15 @@
       }
     }
 
-    .page-comment-revision {
-      .material-symbols-outlined {
+    :global(.page-comment-revision) {
+      :global(.material-symbols-outlined) {
         font-size: 16px;
         vertical-align: middle;
       }
     }
 
 
-    .page-comment-meta {
+    :global(.page-comment-meta) {
       display: flex;
       justify-content: flex-end;
       font-size: 0.9em;
@@ -77,7 +77,7 @@
   }
 
   // TODO: Refacotr Soft-coding
-  .page-comment-comment-body-skeleton {
+  :global(.page-comment-comment-body-skeleton) {
     position: relative;
     height: 66px;
     padding: 1em;
@@ -91,8 +91,8 @@
 
 // // Light mode color
 @include bs.color-mode(light) {
-  .comment-styles :global {
-    .page-comment-revision {
+  .comment-styles {
+    :global(.page-comment-revision) {
       color: bs.$gray-500;
     }
   }
@@ -100,8 +100,8 @@
 
 // // Dark mode color
 @include bs.color-mode(dark) {
-  .comment-styles :global {
-    .page-comment-revision {
+  .comment-styles {
+    :global(.page-comment-revision) {
       color: bs.$gray-600;
     }
   }

+ 13 - 13
apps/app/src/client/components/PageComment/CommentEditor.module.scss

@@ -3,17 +3,17 @@
 @use '../PageEditor/page-editor-inheritance';
 
 // display cheatsheet for comment form only
-.comment-editor-styles :global {
-  .comment-form {
+.comment-editor-styles {
+  :global(.comment-form) {
     position: relative;
 
     // background
-    .bg-comment {
+    :global(.bg-comment) {
       @extend %bg-comment
     }
 
     // user icon
-    .user-picture {
+    :global(.user-picture) {
       @extend %user-picture;
     }
 
@@ -22,36 +22,36 @@
 
 
 // adjust height
-.comment-editor-styles :global {
+.comment-editor-styles {
   // Set `display: flex` instead of `display: block` to make it work with `flex: 1` of the children
   // This helps users focus on the editor by clicking on the broader area
-  .tab-pane.active {
+  :global(.tab-pane.active) {
     display: flex;
   }
 
-  .cm-editor {
+  :global(.cm-editor) {
     min-height: comment-inheritance.$codemirror-default-height !important;
     max-height: #{2 * comment-inheritance.$codemirror-default-height};
   }
-  .cm-gutters {
+  :global(.cm-gutters) {
     min-height: comment-inheritance.$codemirror-default-height !important;
   }
-  .comment-preview-container {
+  :global(.comment-preview-container) {
     min-height: page-editor-inheritance.$navbar-editor-height + comment-inheritance.$codemirror-default-height;
     padding-top: 0.5em;
   }
 }
 
 // border-radius
-.comment-editor-styles :global {
-  .cm-editor, .cm-scroller {
+.comment-editor-styles {
+  :global(.cm-editor), :global(.cm-scroller) {
     border-radius: var(--bs-border-radius);
   }
 }
 
 // remove outline
-.comment-editor-styles :global {
-  .cm-editor {
+.comment-editor-styles {
+  :global(.cm-editor) {
     outline: none;
   }
 }

+ 4 - 4
apps/app/src/client/components/PageComment/CommentEditor.tsx

@@ -1,5 +1,5 @@
 import type { JSX, ReactNode } from 'react';
-import React, {
+import {
   useCallback,
   useEffect,
   useLayoutEffect,
@@ -35,11 +35,11 @@ import { NotAvailableIfReadOnlyUserNotAllowedToComment } from '../NotAvailableFo
 import { CommentPreview } from './CommentPreview';
 import { SwitchingButtonGroup } from './SwitchingButtonGroup';
 
-import '@growi/editor/dist/style.css';
-
 import styles from './CommentEditor.module.scss';
 
-const logger = loggerFactory('growi:components:CommentEditor');
+import '../GrowiEditor.vendor-styles.prebuilt';
+
+const _logger = loggerFactory('growi:components:CommentEditor');
 
 const SlackNotification = dynamic(
   () => import('../SlackNotification').then((mod) => mod.SlackNotification),

+ 3 - 3
apps/app/src/client/components/PageComment/DeleteCommentModal/DeleteCommentModal.module.scss

@@ -1,7 +1,7 @@
 // modal
-.page-comment-delete-modal :global {
-  .modal-content .modal-body {
-    .comment-body {
+.page-comment-delete-modal {
+  :global(.modal-content .modal-body) {
+    :global(.comment-body) {
       max-height: 13em;
 
       // scrollable

+ 1 - 1
apps/app/src/client/components/PageComment/ReplyComments.module.scss

@@ -1,5 +1,5 @@
 
 // remove margin after hidden replies
-.page-comments-hidden-replies + .page-comment-reply :global {
+.page-comments-hidden-replies + .page-comment-reply {
   margin-top: 0;
 }

+ 6 - 6
apps/app/src/client/components/PageComment/SwitchingButtonGroup.module.scss

@@ -1,8 +1,8 @@
 @use '@growi/core-styles/scss/bootstrap/init' as bs;
 
 
-.btn-group-switching :global {
-  .btn {
+.btn-group-switching {
+  :global(.btn) {
     --bs-btn-border-width: 2px;
 
     width: 60px;
@@ -18,8 +18,8 @@
 
 // == Colors
 @include bs.color-mode(light) {
-  .btn-group-switching :global {
-    .btn {
+  .btn-group-switching {
+    :global(.btn) {
       $bg: var(--bs-gray-500);
 
       --bs-btn-border-color: #{$bg};
@@ -33,8 +33,8 @@
 }
 
 @include bs.color-mode(dark) {
-  .btn-group-switching :global {
-    .btn {
+  .btn-group-switching {
+    :global(.btn) {
       $bg: var(--bs-gray-800);
 
       --bs-btn-border-color: #{$bg};

+ 6 - 6
apps/app/src/client/components/PageControls/BookmarkButtons.module.scss

@@ -2,14 +2,14 @@
 @use '@growi/ui/scss/atoms/btn-muted';
 @use './button-styles';
 
-.btn-group-bookmark :global {
-  .btn-bookmark {
+.btn-group-bookmark {
+  :global(.btn-bookmark) {
     @extend %btn-basis;
   }
-  .dropdown .btn-bookmark {
+  :global(.dropdown .btn-bookmark) {
     padding-right: 1px;
   }
-  .total-counts {
+  :global(.total-counts) {
     @extend %btn-total-counts-basis;
 
     padding-left: 5px;
@@ -17,9 +17,9 @@
 }
 
 // == Colors
-.btn-group-bookmark :global {
+.btn-group-bookmark {
   /* stylelint-disable-next-line no-descending-specificity */
-  .btn-bookmark {
+  :global(.btn-bookmark) {
     @include btn-muted.colorize(bs.$orange);
   }
 }

+ 6 - 6
apps/app/src/client/components/PageControls/LikeButtons.module.scss

@@ -2,14 +2,14 @@
 @use '@growi/ui/scss/atoms/btn-muted';
 @use './button-styles';
 
-.btn-group-like :global {
-  .btn-like {
+.btn-group-like {
+  :global(.btn-like) {
     @extend %btn-basis;
   }
-  .btn-like#like-button {
+  :global(.btn-like#like-button) {
     padding-right: 3px;
   }
-  .total-counts {
+  :global(.total-counts) {
     @extend %btn-total-counts-basis;
 
     padding-left: 5px;
@@ -17,8 +17,8 @@
 }
 
 // == Colors
-.btn-group-like :global {
-  .btn-like {
+.btn-group-like {
+  :global(.btn-like) {
     @include btn-muted.colorize(bs.$red);
   }
 }

+ 4 - 4
apps/app/src/client/components/PageControls/PageControls.module.scss

@@ -3,16 +3,16 @@
 @use './button-styles';
 
 // PageItemControl styles
-.grw-page-controls :global {
-  .btn-page-item-control {
+.grw-page-controls {
+  :global(.btn-page-item-control) {
     @extend %btn-basis;
   }
 }
 
 // == Colors
 // PageItemControl colors
-.grw-page-controls :global {
-  .btn-page-item-control {
+.grw-page-controls {
+  :global(.btn-page-item-control) {
     @include btn-muted.colorize(bs.$gray-500);
   }
 }

+ 1 - 1
apps/app/src/client/components/PageControls/SearchButton.module.scss

@@ -2,7 +2,7 @@
 @use '@growi/ui/scss/atoms/btn-muted';
 @use './button-styles';
 
-.btn-search :global {
+.btn-search {
   @extend %btn-basis;
 }
 

+ 5 - 5
apps/app/src/client/components/PageControls/SeenUserInfo.module.scss

@@ -2,11 +2,11 @@
 @use '@growi/ui/scss/atoms/btn-muted';
 @use './button-styles';
 
-.grw-seen-user-info :global {
-  .btn-seen-user {
+.grw-seen-user-info {
+  :global(.btn-seen-user) {
     @extend %btn-basis;
   }
-  .total-counts {
+  :global(.total-counts) {
     @extend %text-total-counts-basis;
   }
 }
@@ -14,10 +14,10 @@
 
 // == Colors
 
-.grw-seen-user-info :global {
+.grw-seen-user-info {
   $color: #549c79;
 
-  .btn-seen-user {
+  :global(.btn-seen-user) {
     @include btn-muted.colorize($color);
   }
 }

+ 2 - 2
apps/app/src/client/components/PageControls/SubscribeButton.module.scss

@@ -2,10 +2,10 @@
 @use '@growi/ui/scss/atoms/btn-muted';
 @use './button-styles';
 
-.btn-subscribe :global {
+.btn-subscribe {
   @extend %btn-basis;
 
-  .total-counts {
+  :global(.total-counts) {
     @extend %btn-total-counts-basis;
   }
 }

+ 1 - 1
apps/app/src/client/components/PageControls/user-list-popover.module.scss

@@ -1,5 +1,5 @@
 @use '@growi/ui/scss/molecules/user-list-popover';
 
-.user-list-popover :global {
+.user-list-popover {
   @extend %user-list-popover
 }

+ 3 - 3
apps/app/src/client/components/PageCreateModal.module.scss

@@ -1,8 +1,8 @@
-.grw-create-page :global {
-  .page-today-input1 {
+.grw-create-page {
+  :global(.page-today-input1) {
     width: 60px;
   }
-  .grw-btn-create-page {
+  :global(.grw-btn-create-page) {
     min-width: 90px;
   }
 }

+ 2 - 2
apps/app/src/client/components/PageEditor/ConflictDiffModal/ConflictDiffModal.module.scss

@@ -1,6 +1,6 @@
 // TODO: https://redmine.weseek.co.jp/issues/142208
-.conflict-diff-modal :global {
-  .cm-editor {
+.conflict-diff-modal {
+  :global(.cm-editor) {
     height: 400px !important;
   }
 }

+ 1 - 1
apps/app/src/client/components/PageEditor/EditorNavbar/EditingUserList.module.scss

@@ -1,5 +1,5 @@
 @use '@growi/ui/scss/molecules/user-list-popover';
 
-.user-list-popover :global {
+.user-list-popover {
   @extend %user-list-popover;
 }

+ 1 - 1
apps/app/src/client/components/PageEditor/EditorNavbar/EditorNavbar.module.scss

@@ -1,6 +1,6 @@
 @use '@growi/core-styles/scss/bootstrap/init' as bs;
 
-.editor-navbar :global {
+.editor-navbar {
   min-height: 72px;
 
   @include bs.media-breakpoint-down(sm) {

+ 7 - 7
apps/app/src/client/components/PageEditor/EditorNavbarBottom/EditorNavbarBottom.module.scss

@@ -3,25 +3,25 @@
 @use 'styles/mixins';
 
 @include mixins.at-editing() {
-  .grw-editor-navbar-bottom :global {
-    .grw-grant-selector {
+  .grw-editor-navbar-bottom {
+    :global(.grw-grant-selector) {
       max-width: 250px;
-      .material-symbols-outlined  {
+      :global(.material-symbols-outlined)  {
         padding-bottom: 2px;
         font-size: 19px;
       }
     }
-    .btn-submit {
+    :global(.btn-submit) {
       width: 100px;
     }
 
-    .btn-expand {
+    :global(.btn-expand) {
       // rotate icon
-      i {
+      :global(i) {
         display: inline-block;
         transition: transform 200ms;
       }
-      &.expand i {
+      &:global(.expand i) {
         transform: rotate(-180deg);
       }
     }

+ 15 - 15
apps/app/src/client/components/PageEditor/GridEditModal.module.scss

@@ -1,40 +1,40 @@
 @use '@growi/core-styles/scss/bootstrap/init' as bs;
 
-.grw-grid-edit-modal :global {
-  .desktop-preview,
-  .tablet-preview,
-  .mobile-preview {
-    .row {
+.grw-grid-edit-modal {
+  :global(.desktop-preview),
+  :global(.tablet-preview),
+  :global(.mobile-preview) {
+    :global(.row) {
       height: 140px;
       margin: 0;
     }
   }
-  .desktop-preview {
-    .row {
-      div {
+  :global(.desktop-preview) {
+    :global(.row) {
+      :global(div) {
         padding: 0;
       }
     }
   }
 
-  .tablet-preview {
-    .row {
-      div {
+  :global(.tablet-preview) {
+    :global(.row) {
+      :global(div) {
         padding: 0;
       }
     }
   }
 
-  .mobile-preview {
+  :global(.mobile-preview) {
     width: 75%;
-    .row {
-      div {
+    :global(.row) {
+      :global(div) {
         padding: 0;
       }
     }
   }
 
-  .grid-division-menu {
+  :global(.grid-division-menu) {
     width: 60vw;
 
     @include bs.media-breakpoint-down(lg) {

+ 10 - 10
apps/app/src/client/components/PageEditor/HandsontableModal/HandsontableModal.module.scss

@@ -1,23 +1,23 @@
-.grw-handsontable :global {
-  .handsontable {
+.grw-handsontable {
+  :global(.handsontable) {
     position: relative;
 
-    .handsontableInput {
+    :global(.handsontableInput) {
       max-width: 290px !important;
     }
 
-    td {
+    :global(td) {
       word-break: break-all;
     }
 
-    th {
+    :global(th) {
       text-align: inherit;
     }
   }
 
   // expand .hot-table-container (with flexbox)
-  .grw-modal-expanded {
-    .hot-table-container {
+  :global(.grw-modal-expanded) {
+    :global(.hot-table-container) {
       flex: 1;
     }
   }
@@ -27,15 +27,15 @@
   // see https://github.com/handsontable/handsontable/issues/2937#issuecomment-287390111
   // This issue fixing from Handsontable v 7.0.0
   // see: https://github.com/handsontable/handsontable/issues/2937#issuecomment-480824024
-  .modal.in .modal-dialog.handsontable-modal {
+  :global(.modal.in .modal-dialog.handsontable-modal) {
     transform: none;
 
-    .data-import-button {
+    :global(.data-import-button) {
       position: relative;
       padding-right: 35px;
       padding-left: 10px;
 
-      i::before {
+      :global(i::before) {
         position: absolute;
         top: 6px;
         right: 8px;

+ 2 - 1
apps/app/src/client/components/PageEditor/HandsontableModal/HandsontableModal.tsx

@@ -35,7 +35,8 @@ import ExpandOrContractButton from '../../ExpandOrContractButton';
 import { MarkdownTableDataImportForm } from '../MarkdownTableDataImportForm';
 
 import styles from './HandsontableModal.module.scss';
-import 'handsontable/dist/handsontable.full.min.css';
+
+import './HandsontableModal.vendor-styles.prebuilt';
 
 const DEFAULT_HOT_HEIGHT = 300;
 const MARKDOWNTABLE_TO_HANDSONTABLE_ALIGNMENT_SYMBOL_MAPPING = {

+ 6 - 0
apps/app/src/client/components/PageEditor/HandsontableModal/HandsontableModal.vendor-styles.ts

@@ -0,0 +1,6 @@
+// @ts-nocheck -- Processed by Vite only; ?inline is a Vite-specific import suffix
+import css from 'handsontable/dist/handsontable.css?inline';
+
+const s = document.createElement('style');
+s.textContent = css;
+document.head.appendChild(s);

+ 1 - 1
apps/app/src/client/components/PageEditor/PageEditor.tsx

@@ -74,7 +74,7 @@ import { EditorNavbarBottom } from './EditorNavbarBottom';
 import Preview from './Preview';
 import { useScrollSync } from './ScrollSyncHelper';
 
-import '@growi/editor/dist/style.css';
+import '../GrowiEditor.vendor-styles.prebuilt';
 
 const logger = loggerFactory('growi:PageEditor');
 

+ 5 - 5
apps/app/src/client/components/PageEditor/Preview.module.scss

@@ -1,7 +1,7 @@
 @use 'styles/mixins';
 
-.page-editor-preview-body :global {
-  .wiki {
+.page-editor-preview-body {
+  :global(.wiki) {
     max-width: 980px;
     padding: 0 15px;
     margin: 0 auto;
@@ -10,9 +10,9 @@
 
 // modify width for fluid layout
 .page-editor-preview-body {
-  &:global {
-    &.fluid-layout {
-      .wiki {
+  & {
+    &:global(.fluid-layout) {
+      :global(.wiki) {
         @include mixins.fluid-layout();
       }
     }

+ 1 - 1
apps/app/src/client/components/PageHeader/PageHeader.module.scss

@@ -1,3 +1,3 @@
 /* stylelint-disable-next-line block-no-empty */
-.page-header :global {
+.page-header {
 }

+ 5 - 5
apps/app/src/client/components/PageHeader/PagePathHeader.module.scss

@@ -1,5 +1,5 @@
-.page-path-header :global {
-  input {
+.page-path-header {
+  :global(input) {
     min-width: 20px;
     min-height: unset;
     padding-top: 2px;
@@ -7,10 +7,10 @@
     line-height: 1.2em;
   }
 
-  .page-path-header-buttons {
+  :global(.page-path-header-buttons) {
     height: 0;
 
-    .btn {
+    :global(.btn) {
       width: 24px;
       height: 24px;
       transform: translateY(12px);
@@ -18,7 +18,7 @@
   }
 
   // Make Truncated elements horizontally scrollable and hide the scroll bar
-  .page-path-header-input {
+  :global(.page-path-header-input) {
     scrollbar-width: none;
   }
 }

+ 4 - 4
apps/app/src/client/components/PageHeader/PageTitleHeader.module.scss

@@ -1,5 +1,5 @@
-.page-title-header :global {
-  input {
+.page-title-header {
+  :global(input) {
     min-width: 20px;
     min-height: unset;
     padding: 0 0.5rem;
@@ -8,7 +8,7 @@
   }
 
   // Make Truncated elements horizontally scrollable and hide the scroll bar
-  .page-title-header-input {
+  :global(.page-title-header-input) {
     scrollbar-width: none;
   }
 }
@@ -16,7 +16,7 @@
 .page-title-header-border-color {
   --bs-border-color: transparent;
 
-  &:global {
+  & {
     &:hover {
       --bs-border-color: var(--bs-primary-border-subtle);
     }

+ 2 - 2
apps/app/src/client/components/PageHistory/PageRevisionTable.module.scss

@@ -1,5 +1,5 @@
-.revision-history-table :global {
-  tbody {
+.revision-history-table {
+  :global(tbody) {
     max-height: 250px;
   }
 }

+ 4 - 4
apps/app/src/client/components/PageHistory/Revision.module.scss

@@ -1,12 +1,12 @@
-.revision-history-main :global {
-  img.user-picture-lg {
+.revision-history-main {
+  :global(img.user-picture-lg) {
     width: 32px;
     height: 32px;
   }
 }
 
-.revision-history-main-nodiff :global {
-  .picture-container {
+.revision-history-main-nodiff {
+  :global(.picture-container) {
     min-width: 32px;
     text-align: center; // centering .picture
   }

+ 4 - 4
apps/app/src/client/components/PageHistory/RevisionDiff.module.scss

@@ -1,14 +1,14 @@
-.revision-diff-container :global {
-  .link-created-at {
+.revision-diff-container {
+  :global(.link-created-at) {
     text-decoration-line: underline;
   }
 
-  .revision-history-diff {
+  :global(.revision-history-diff) {
     table-layout: fixed;
 
     // revision-history
     // to stay d2h-code-side-line-number in the revision history diff area
-    .d2h-wrapper {
+    :global(.d2h-wrapper) {
       position: relative;
     }
   }

+ 1 - 1
apps/app/src/client/components/PageHistory/RevisionDiff.tsx

@@ -18,7 +18,7 @@ import { useSWRxGrowiThemeSetting } from '../../../stores/admin/customize';
 
 import styles from './RevisionDiff.module.scss';
 
-import 'diff2html/bundles/css/diff2html.min.css';
+import './RevisionDiff.vendor-styles.prebuilt';
 
 const moduleClass = styles['revision-diff-container'];
 

+ 6 - 0
apps/app/src/client/components/PageHistory/RevisionDiff.vendor-styles.ts

@@ -0,0 +1,6 @@
+// @ts-nocheck -- Processed by Vite only; ?inline is a Vite-specific import suffix
+import css from 'diff2html/bundles/css/diff2html.min.css?inline';
+
+const s = document.createElement('style');
+s.textContent = css;
+document.head.appendChild(s);

+ 3 - 3
apps/app/src/client/components/PagePathNavSticky/PagePathNavSticky.module.scss

@@ -1,6 +1,6 @@
-.grw-page-path-nav-sticky :global {
-  .sticky-inner-wrapper.active {
-    h1 {
+.grw-page-path-nav-sticky {
+  :global(.sticky-inner-wrapper.active) {
+    :global(h1) {
       font-size: 1.75rem !important;
     }
   }

+ 5 - 5
apps/app/src/client/components/PagePresentationModal/PagePresentationModal.module.scss

@@ -1,22 +1,22 @@
 @use 'styles/modal';
 
-.grw-presentation-modal :global {
+.grw-presentation-modal {
   /* stylelint-disable-next-line length-zero-no-unit */
-  @include modal.expand-modal-fullscreen(false, false, 0px);
+  @include modal.expand-modal-fullscreen(false, false, 0px, $css-modules: true);
 
-  .modal-content {
+  :global(.modal-content) {
     background-color: transparent;
     border-radius: 0;
   }
 
-  .grw-presentation-controls {
+  :global(.grw-presentation-controls) {
     position: absolute;
     top: 1rem;
     right: 1rem;
     z-index: 110; // over ".reveal .slides"
   }
 
-  .btn-close {
+  :global(.btn-close) {
     display: inline-block;
     width: 3rem;
     height: 3rem;

+ 6 - 6
apps/app/src/client/components/PageSelectModal/TreeItemForModal.module.scss

@@ -1,7 +1,7 @@
 @use '@growi/core-styles/scss/bootstrap/init' as bs;
 
-.tree-item-for-modal :global {
-  li {
+.tree-item-for-modal {
+  :global(li) {
     min-height: 36px;
   }
 }
@@ -9,16 +9,16 @@
 
 // == Colors
 @include bs.color-mode(light) {
-  .tree-item-for-modal :global {
-    .list-group-item-action {
+  .tree-item-for-modal {
+    :global(.list-group-item-action) {
       --bs-list-group-active-bg: var(--grw-primary-200);
     }
   }
 }
 
 @include bs.color-mode(dark) {
-  .tree-item-for-modal :global {
-    .list-group-item-action {
+  .tree-item-for-modal {
+    :global(.list-group-item-action) {
       --bs-list-group-active-bg: var(--grw-primary-700);
     }
   }

+ 5 - 5
apps/app/src/client/components/PageSideContents/PageAccessoriesControl.module.scss

@@ -1,11 +1,11 @@
 @use '@growi/core-styles/scss/bootstrap/init' as bs;
 
-.btn-page-accessories :global {
+.btn-page-accessories {
   display: flex;
   align-items: center;
   padding: 6px 8px;
 
-  .grw-labels {
+  :global(.grw-labels) {
     flex-grow: 1;
     align-items: center;
     justify-content: space-between;
@@ -13,13 +13,13 @@
 }
 
 @include bs.media-breakpoint-down(sm) {
-  .btn-page-accessories :global {
+  .btn-page-accessories {
     box-shadow: 0 3px 6px rgba(black, 0.15);
   }
 }
 
 // apply font-size
-.btn-page-accessories :global {
+.btn-page-accessories {
   --bs-btn-font-size: 14px;
 
   @include bs.media-breakpoint-down(lg) {
@@ -29,7 +29,7 @@
 
 // expand when larger than lg
 @include bs.media-breakpoint-up(lg) {
-  .btn-page-accessories :global {
+  .btn-page-accessories {
     flex-grow: 1;
   }
 }

+ 6 - 6
apps/app/src/client/components/PageStatusAlert.module.scss

@@ -1,7 +1,7 @@
 @use 'styles/variables' as var;
 @use '@growi/core-styles/scss/bootstrap/init' as bs;
 
-.grw-page-status-alert :global {
+.grw-page-status-alert {
   $margin-bottom: var.$grw-navbar-bottom-height + 10px;
 
   box-shadow: 0 2px 4px #0000004d;
@@ -10,10 +10,10 @@
   @include bs.media-breakpoint-down(sm) {
     margin: 0 10px $margin-bottom;
 
-    .grw-card-label-container {
+    :global(.grw-card-label-container) {
       text-align: center;
     }
-    .grw-card-btn-container {
+    :global(.grw-card-btn-container) {
       text-align: center;
     }
   }
@@ -22,14 +22,14 @@
     width: 700px;
     margin: 0 auto $margin-bottom;
 
-    .card-body {
+    :global(.card-body) {
       display: flex;
       align-items: center;
       justify-content: space-between;
     }
 
-    .grw-card-label-container,
-    .grw-card-btn-container {
+    :global(.grw-card-label-container),
+    :global(.grw-card-btn-container) {
       margin: 0;
     }
   }

+ 6 - 6
apps/app/src/client/components/PageTags/TagEditModal/TagsInput.module.scss

@@ -1,6 +1,6 @@
-.tags-input :global {
-  .rbt-token {
-    .rbt-token-label {
+.tags-input {
+  :global(.rbt-token) {
+    :global(.rbt-token-label) {
       // override to text-truncate
       overflow: hidden;
       font-size: 1rem; // adjust font-size
@@ -11,13 +11,13 @@
 }
 
 // == Colors
-.tags-input :global {
-  .rbt-token {
+.tags-input {
+  :global(.rbt-token) {
     // override to .badge color
     color: var(--bs-badge-color);
   }
 
-  .rbt-token-active {
+  :global(.rbt-token-active) {
     border-color: var(--grw-primary-400) !important;
   }
 }

+ 6 - 6
apps/app/src/client/components/PageTags/TagLabels.module.scss

@@ -2,14 +2,14 @@
 
 $grw-tag-label-font-size: 12px;
 
-.grw-tag-labels :global {
-  .grw-tag-label {
+.grw-tag-labels {
+  :global(.grw-tag-label) {
     font-size: $grw-tag-label-font-size;
     font-weight: normal;
     border-radius: bs.$border-radius;
   }
 
-  .grw-tag-simple-bar {
+  :global(.grw-tag-simple-bar) {
     width: 15.5rem;
     max-height: 5rem;
   }
@@ -17,8 +17,8 @@ $grw-tag-label-font-size: 12px;
 }
 
 // apply font-size
-.grw-tag-labels :global {
-  .btn-edit-tags {
+.grw-tag-labels {
+  :global(.btn-edit-tags) {
     --bs-btn-font-size: 14px;
 
     @include bs.media-breakpoint-down(lg) {
@@ -27,7 +27,7 @@ $grw-tag-label-font-size: 12px;
   }
 }
 
-.grw-tag-labels-skeleton :global {
+.grw-tag-labels-skeleton {
   width: 137px;
   height: calc(var(--bs-body-line-height) * 1em);
 

+ 3 - 3
apps/app/src/client/components/PageTimeline.module.scss

@@ -1,14 +1,14 @@
 @use '@growi/core-styles/scss/bootstrap/init' as bs;
 
-.card-timeline :global {
+.card-timeline {
   margin-bottom: 3rem;
   border: 1px solid bs.$gray-300;
 
-  .card-header {
+  :global(.card-header) {
     background-color: bs.$gray-300;
   }
 
-  .card-body {
+  :global(.card-body) {
     min-height: 15vh;
   }
 }

+ 1 - 1
apps/app/src/client/components/Presentation/Presentation.tsx

@@ -4,7 +4,7 @@ import {
   Presentation as PresentationSubstance,
 } from '@growi/presentation/dist/client';
 
-import '@growi/presentation/dist/style.css';
+import './Presentation.vendor-styles.prebuilt';
 
 export const Presentation = (props: PresentationProps): JSX.Element => {
   return <PresentationSubstance {...props} />;

+ 6 - 0
apps/app/src/client/components/Presentation/Presentation.vendor-styles.ts

@@ -0,0 +1,6 @@
+// @ts-nocheck -- Processed by Vite only; ?inline is a Vite-specific import suffix
+import css from '@growi/presentation/dist/style.css?inline';
+
+const s = document.createElement('style');
+s.textContent = css;
+document.head.appendChild(s);

+ 0 - 2
apps/app/src/client/components/Presentation/Slides.tsx

@@ -4,8 +4,6 @@ import {
   Slides as SlidesSubstance,
 } from '@growi/presentation/dist/client';
 
-import '@growi/presentation/dist/style.css';
-
 export const Slides = (props: SlidesProps): JSX.Element => {
   return <SlidesSubstance {...props} />;
 };

+ 4 - 4
apps/app/src/client/components/ReactMarkdownComponents/DrawioViewerWithEditButton.module.scss

@@ -1,9 +1,9 @@
 @use '@growi/core-styles/scss/bootstrap/init' as bs;
 
-.drawio-viewer-with-edit-button :global {
+.drawio-viewer-with-edit-button {
   position: relative;
 
-  .btn-edit-drawio {
+  :global(.btn-edit-drawio) {
     position: absolute;
     top: 11px;
     right: 10px;
@@ -16,8 +16,8 @@
   }
 }
 
-.drawio-viewer-with-edit-button:hover :global {
-  .btn-edit-drawio {
+.drawio-viewer-with-edit-button:hover {
+  :global(.btn-edit-drawio) {
     opacity: 1;
   }
 }

+ 2 - 2
apps/app/src/client/components/ReactMarkdownComponents/DrawioViewerWithEditButton.tsx

@@ -16,10 +16,10 @@ import {
 import { useShareLinkId } from '~/states/page/hooks';
 import { useIsRevisionOutdated } from '~/stores/page';
 
-import '@growi/remark-drawio/dist/style.css';
-
 import styles from './DrawioViewerWithEditButton.module.scss';
 
+import './DrawioViewerWithEditButton.vendor-styles.prebuilt';
+
 export const DrawioViewerWithEditButton = React.memo(
   (props: DrawioViewerProps): JSX.Element => {
     const { t } = useTranslation();

+ 6 - 0
apps/app/src/client/components/ReactMarkdownComponents/DrawioViewerWithEditButton.vendor-styles.ts

@@ -0,0 +1,6 @@
+// @ts-nocheck -- Processed by Vite only; ?inline is a Vite-specific import suffix
+import css from '@growi/remark-drawio/dist/style.css?inline';
+
+const s = document.createElement('style');
+s.textContent = css;
+document.head.appendChild(s);

+ 9 - 9
apps/app/src/client/components/ReactMarkdownComponents/Header.module.scss

@@ -1,33 +1,33 @@
-.revision-head :global {
-  a {
+.revision-head {
+  :global(a) {
     text-decoration: none;
   }
 
-  .revision-head-link {
+  :global(.revision-head-link) {
     left: -1em;
     width: 1em;
     user-select: none;
     opacity: 0;
   }
-  .revision-head-edit-button{
+  :global(.revision-head-edit-button){
     margin-left: 0.5em;
     font-size: 16px;
     user-select: none;
     opacity: 0;
-    .material-symbols-outlined{
+    :global(.material-symbols-outlined){
       vertical-align: middle;
     }
   }
 }
 
-.revision-head:hover :global {
-  .revision-head-link, .revision-head-edit-button {
+.revision-head:hover {
+  :global(.revision-head-link), :global(.revision-head-edit-button) {
     opacity: 0.5;
   }
-  .revision-head-link:hover {
+  :global(.revision-head-link:hover) {
     opacity: 1;
   }
-  .revision-head-edit-button:hover {
+  :global(.revision-head-edit-button:hover) {
     opacity: 1;
   }
 }

+ 2 - 2
apps/app/src/client/components/ReactMarkdownComponents/RichAttachment.module.scss

@@ -1,5 +1,5 @@
-.attachment :global {
-  .attachment-icon {
+.attachment {
+  :global(.attachment-icon) {
     flex-shrink: 0;
     width: 35px;
     height: 35px;

+ 4 - 4
apps/app/src/client/components/ReactMarkdownComponents/TableWithEditButton.module.scss

@@ -3,10 +3,10 @@
 /**
  * for table with handsontable modal button
  */
-.editable-with-handsontable :global {
+.editable-with-handsontable {
   position: relative;
 
-  .handsontable-modal-trigger {
+  :global(.handsontable-modal-trigger) {
     position: absolute;
     top: 8px;
     right: 10px;
@@ -24,8 +24,8 @@
   }
 }
 
-.editable-with-handsontable:hover :global {
-  .handsontable-modal-trigger {
+.editable-with-handsontable:hover {
+  :global(.handsontable-modal-trigger) {
     opacity: 1;
   }
 }

+ 10 - 10
apps/app/src/client/components/RevisionComparer/RevisionComparer.module.scss

@@ -1,40 +1,40 @@
 @use '@growi/core-styles/scss/bootstrap/init' as bs;
 @use '@growi/ui/scss/atoms/btn-muted';
 
-.revision-compare :global {
-  .revision-compare-container {
+.revision-compare {
+  :global(.revision-compare-container) {
     min-height: 100px;
 
-    &.nodiff {
+    &:global(.nodiff) {
       display: flex;
       align-items: center;
       justify-content: center;
     }
   }
-  .d2h-file-header {
+  :global(.d2h-file-header) {
     display: none;
   }
 
-  .grw-copy-dropdown {
-    .btn.btn-copy {
+  :global(.grw-copy-dropdown) {
+    :global(.btn.btn-copy) {
       @include btn-muted.colorize(bs.$gray-500);
     }
 
-    .dropdown-menu {
+    :global(.dropdown-menu) {
       min-width: 310px;
 
-      .dropdown-header {
+      :global(.dropdown-header) {
         margin-bottom: 0.5em;
         font-size: 1.1em;
       }
 
       // unset active styles
-      .dropdown-item:active {
+      :global(.dropdown-item:active) {
         color: unset;
         background-color: unset;
       }
 
-      .card {
+      :global(.card) {
         font-size: 0.7em;
         word-break: break-all;
       }

+ 8 - 8
apps/app/src/client/components/SearchTypeahead.module.scss

@@ -1,16 +1,16 @@
 @use '@growi/core-styles/scss/bootstrap/init' as bs;
 
-.search-typeahead :global {
+.search-typeahead {
   position: relative;
   width: 100%;
 
   // corner radius
   border-top-right-radius: bs.$border-radius;
   border-bottom-right-radius: bs.$border-radius;
-  .rbt-input-main {
+  :global(.rbt-input-main) {
     padding-right: 36px;
   }
-  .search-clear {
+  :global(.search-clear) {
     position: absolute;
     top: 4px;
     right: 4px;
@@ -21,22 +21,22 @@
     line-height: 0;
   }
 
-  .rbt-menu {
+  :global(.rbt-menu) {
     max-height: none !important;
     margin-top: 3px;
 
-    li a span {
-      .page-path {
+    :global(li a span) {
+      :global(.page-path) {
         display: inline;
         padding: 0 4px;
         color: inherit;
       }
 
-      .page-list-meta {
+      :global(.page-list-meta) {
         font-size: 0.9em;
         color: bs.$gray-400;
 
-        > span {
+        > :global(span) {
           margin-right: 0.3rem;
         }
       }

+ 3 - 3
apps/app/src/client/components/ShortcutsModal/ShortcutsModal.module.scss

@@ -1,9 +1,9 @@
 @use '@growi/core-styles/scss/bootstrap/init' as bs;
 @use '@growi/core-styles/scss/helpers/modifier-keys';
 
-.shortcuts-modal :global {
+.shortcuts-modal {
 
-  .key {
+  :global(.key) {
     /* Box Properties */
     padding: 0 4px;
 
@@ -15,7 +15,7 @@
 
   @include modifier-keys.modifier-key;
 
-  .grw-modal-body-style {
+  :global(.grw-modal-body-style) {
     max-height: calc(100vh - 200px);
   }
 }

+ 8 - 8
apps/app/src/client/components/Sidebar/AppTitle/AppTitle.module.scss

@@ -5,8 +5,8 @@
 @use 'styles/mixins';
 
 // GROWI Logo
-.grw-app-title :global {
-  .grw-logo {
+.grw-app-title {
+  :global(.grw-logo) {
     $width: var.$grw-sidebar-nav-width;
     $height: var.$grw-sidebar-nav-width; // declare $height with the same value as the sidebar nav width
     $logomark-width: 27.7px;
@@ -14,14 +14,14 @@
 
     width: $width;
 
-    svg {
+    :global(svg) {
       width: $width;
       height: $height;
       padding: (($height - $logomark-height) * 0.5) (($width - $logomark-width) * 0.5);
     }
   }
 
-  .confidential-tooltip {
+  :global(.confidential-tooltip) {
     max-width: 180px;
   }
 }
@@ -29,8 +29,8 @@
 // == GROWI Logo when Editor mode
 @include mixins.at-editing() {
   @include bs.media-breakpoint-up(xl) {
-    .grw-app-title :global {
-      .grw-logo {
+    .grw-app-title {
+      :global(.grw-logo) {
           opacity: 0.5;
           transition: opacity 0.8s ease;
 
@@ -130,8 +130,8 @@
 
 
 // == Colors
-.grw-app-title :global {
-  .grw-site-name {
+.grw-app-title {
+  :global(.grw-site-name) {
     --bs-link-color-rgb: var(--grw-app-title-color-rgb, var(--bs-tertiary-color-rgb));
     --bs-link-opacity: 0.5;
   }

+ 2 - 2
apps/app/src/client/components/Sidebar/Custom/CustomSidebarSubstance.module.scss

@@ -1,7 +1,7 @@
 @use 'styles/organisms/wiki-custom-sidebar.scss';
 
-.grw-custom-sidebar-content :global {
-  .wiki {
+.grw-custom-sidebar-content {
+  :global(.wiki) {
     @extend %grw-custom-sidebar-content;
   }
 }

+ 10 - 10
apps/app/src/client/components/Sidebar/PageCreateButton/CreateButton.module.scss

@@ -1,11 +1,11 @@
 @use 'styles/variables' as var;
 @use '../button-styles';
 
-.btn-create :global {
+.btn-create {
   @extend %btn-basis;
 
   // centering
-  .icon {
+  :global(.icon) {
     top: 50%;
     left: 50%;
     transform: translateX(-50%) translateY(-50%);
@@ -13,10 +13,10 @@
 }
 
 // pointer-events
-.btn-create :global {
+.btn-create {
   pointer-events: none;
 
-  svg .background {
+  :global(svg .background) {
     pointer-events: fill;
   }
 }
@@ -26,20 +26,20 @@
   background-color: transparent !important;
 }
 
-.btn-create :global {
-  svg {
+.btn-create {
+  :global(svg) {
     fill: var(--bs-btn-bg);
   }
 }
 
-.btn-create:hover :global {
-  svg {
+.btn-create:hover {
+  :global(svg) {
     fill: var(--bs-btn-hover-bg);
   }
 }
 
-.btn-create:active :global {
-  svg {
+.btn-create:active {
+  :global(svg) {
     fill: var(--bs-btn-active-bg);
   }
 }

+ 11 - 11
apps/app/src/client/components/Sidebar/PageCreateButton/DropendToggle.module.scss

@@ -1,13 +1,13 @@
 @use 'styles/variables' as var;
 @use '../button-styles';
 
-.btn-toggle :global {
+.btn-toggle {
   @extend %btn-basis;
 
   left: 12px;
   padding: 0;
 
-  .icon {
+  :global(.icon) {
     top: 50%;
     right: 0;
     font-size: 22px;
@@ -17,7 +17,7 @@
 
 // no caret
 .btn-toggle {
-  &:global {
+  & {
     // no caret
     &::after {
       display: none !important;
@@ -26,8 +26,8 @@
 }
 
 // hitarea
-.btn-toggle :global {
-  .hitarea {
+.btn-toggle {
+  :global(.hitarea) {
     inset: 0 -10px 0 0;
   }
 }
@@ -37,20 +37,20 @@
   background-color: transparent !important;
 }
 
-.btn-toggle :global {
-  svg {
+.btn-toggle {
+  :global(svg) {
     fill: var(--grw-primary-400);
   }
 }
 
-.btn-toggle:hover :global {
-  svg {
+.btn-toggle:hover {
+  :global(svg) {
     fill: var(--grw-primary-400);
   }
 }
 
-.btn-toggle:active :global {
-  svg {
+.btn-toggle:active {
+  :global(svg) {
     fill: var(--grw-primary-600);
   }
 }

+ 11 - 11
apps/app/src/client/components/Sidebar/PageTreeItem/PageTreeItem.module.scss

@@ -2,31 +2,31 @@
 
 
 // fix height
-.page-tree-item :global {
-  li {
+.page-tree-item {
+  :global(li) {
     min-height: 40px;
   }
 }
 
 // font size
-.page-tree-item :global {
+.page-tree-item {
     font-size: 14px;
 }
 
 // == Colors
 
 // drag over
-.page-tree-item :global {
-  .drag-over {
+.page-tree-item {
+  :global(.drag-over) {
     background-color: var(--bs-list-group-action-active-bg);
   }
 }
 
 @include bs.color-mode(light) {
   // button
-  .page-tree-item :global {
-    .list-group-item-action {
-      .btn-page-item-control {
+  .page-tree-item {
+    :global(.list-group-item-action) {
+      :global(.btn-page-item-control) {
         --bs-btn-bg: transparent;
         --bs-btn-hover-bg: var(--grw-primary-200);
         --bs-btn-active-bg: var(--grw-primary-300);
@@ -37,9 +37,9 @@
 
 @include bs.color-mode(dark) {
   // button
-  .page-tree-item :global {
-    .list-group-item-action {
-      .btn-page-item-control {
+  .page-tree-item {
+    :global(.list-group-item-action) {
+      :global(.btn-page-item-control) {
         --bs-btn-bg: transparent;
         --bs-btn-hover-bg: var(--grw-primary-600);
         --bs-btn-active-bg: var(--grw-primary-700);

+ 13 - 13
apps/app/src/client/components/Sidebar/RecentChanges/RecentChangesSubstance.module.scss

@@ -1,52 +1,52 @@
 @use 'styles/mixins' as *;
 
-.grw-recent-changes-resize-button :global {
+.grw-recent-changes-resize-button {
   line-height: normal;
   transform: translateY(-2px);
 }
 
-.list-group-item :global {
+.list-group-item {
   font-size: 12px;
 
-  h6 {
+  :global(h6) {
     font-size: 14px;
   }
 
-  .grw-recent-changes-skeleton-small {
+  :global(.grw-recent-changes-skeleton-small) {
     @include grw-skeleton-text($font-size: 14px, $line-height: 16px);
 
     max-width: 120px;
   }
 
-  .grw-recent-changes-skeleton-h5 {
+  :global(.grw-recent-changes-skeleton-h5) {
     @include grw-skeleton-h5;
 
     max-width: 120px;
   }
 
-  .grw-recent-changes-skeleton-date {
+  :global(.grw-recent-changes-skeleton-date) {
     @include grw-skeleton-text($font-size: 10px, $line-height: 12px);
 
     width: 80px;
   }
 
   // For truncate-text
-  .flex-grow-1 {
+  :global(.flex-grow-1) {
     min-width: 0;
   }
 
-  .truncate-text {
+  :global(.truncate-text) {
     max-width: fit-content;
   }
 }
 
-.grw-recent-changes-item-lower :global {
+.grw-recent-changes-item-lower {
   font-size: 12px;
 
-  .material-symbols-outlined {
+  :global(.material-symbols-outlined) {
     font-size: 14px;
   }
-  .grw-formatted-distance-date {
+  :global(.grw-formatted-distance-date) {
     font-size: 10px;
   }
 }
@@ -55,13 +55,13 @@
 .grw-former-link a {
   --bs-link-opacity: 0.5;
 
-  &:global {
+  & {
     &:hover {
       --bs-link-opacity: 1;
     }
   }
 }
 
-.grw-recent-changes-item-lower :global {
+.grw-recent-changes-item-lower {
   color: var(--bs-gray-500);
 }

+ 7 - 7
apps/app/src/client/components/Sidebar/ResizableArea/ResizableArea.module.scss

@@ -1,19 +1,19 @@
-.grw-resizable-area :global {
+.grw-resizable-area {
   will-change: width;
 }
 
-.grw-resizable-area:not(:global .dragging) {
+.grw-resizable-area:not(:global(.dragging)) {
   transition: width 100ms cubic-bezier(0.2, 0, 0, 1) 0s;
 }
 
 
-.grw-navigation-draggable :global {
+.grw-navigation-draggable {
   position: absolute;
   top: 0;
   bottom: 0;
   left: 100%;
 
-  .grw-navigation-draggable-hitarea {
+  :global(.grw-navigation-draggable-hitarea) {
     position: absolute;
     left: -4px;
     width: 24px;
@@ -24,7 +24,7 @@
     background: transparent;
     border: 0;
   }
-  .grw-navigation-draggable-line {
+  :global(.grw-navigation-draggable-line) {
     position: absolute;
     left: -1px;
     display: none;
@@ -34,8 +34,8 @@
   }
 }
 
-.grw-navigation-draggable:hover :global {
-  .grw-navigation-draggable-line {
+.grw-navigation-draggable:hover {
+  :global(.grw-navigation-draggable-line) {
     display: block;
   }
 }

+ 51 - 60
apps/app/src/client/components/Sidebar/Sidebar.module.scss

@@ -1,16 +1,17 @@
+// stylelint-disable no-descending-specificity -- false positives from :global() function form conversion; each .grw-sidebar block targets different mode classes
 @use '@growi/core-styles/scss/bootstrap/init' as bs;
 @use 'styles/variables' as var;
 @use 'styles/mixins';
 
-.grw-sidebar :global {
+.grw-sidebar {
   top: 0;
 }
 
 
 // TODO: commonize reload button style
-.grw-sidebar :global {
-  .grw-sidebar-content-header {
-    .grw-btn-reload {
+.grw-sidebar {
+  :global(.grw-sidebar-content-header) {
+    :global(.grw-btn-reload) {
       font-size: 16px;
     }
   }
@@ -18,35 +19,31 @@
 
 // Dock Mode
 .grw-sidebar {
-  &:global {
-    &.grw-sidebar-dock {
-      position: sticky;
-    }
+  &:global(.grw-sidebar-dock) {
+    position: sticky;
   }
 }
 
 // Collapsed Mode
 .grw-sidebar {
-  &:global {
-    &.grw-sidebar-collapsed {
-      position: sticky;
-
-      .sidebar-contents-container {
-        border-color: var(--bs-border-color);
-        border-style: solid;
-        border-width : 1px 1px 1px 0;
-      }
+  &:global(.grw-sidebar-collapsed) {
+    position: sticky;
+
+    :global(.sidebar-contents-container) {
+      border-color: var(--bs-border-color);
+      border-style: solid;
+      border-width : 1px 1px 1px 0;
+    }
 
-      // open
-      .sidebar-contents-container.open {
-        position: absolute;
-        left: var.$grw-sidebar-nav-width;
-        min-height: 50vh;
-        max-height: calc(100vh - var.$grw-sidebar-nav-width * 2);
-        border-radius: 0 4px 4px 0 ;
-        .simple-scrollbar {
-          max-height: inherit;
-        }
+    // open
+    :global(.sidebar-contents-container.open) {
+      position: absolute;
+      left: var.$grw-sidebar-nav-width;
+      min-height: 50vh;
+      max-height: calc(100vh - var.$grw-sidebar-nav-width * 2);
+      border-radius: 0 4px 4px 0 ;
+      :global(.simple-scrollbar) {
+        max-height: inherit;
       }
     }
   }
@@ -54,78 +51,72 @@
 
 // Drawer Mode
 .grw-sidebar {
-  &:global {
-    &.grw-sidebar-drawer {
-      position: fixed;
-      z-index: bs.$zindex-fixed + 2;
-      width: 348px;
+  &:global(.grw-sidebar-drawer) {
+    position: fixed;
+    z-index: bs.$zindex-fixed + 2;
+    width: 348px;
 
-      // apply transition
-      transition-property: transform;
+    // apply transition
+    transition-property: transform;
 
-      @include mixins.apply-navigation-transition();
+    @include mixins.apply-navigation-transition();
 
-      &:not(.open) {
-        transform: translateX(-100%);
-      }
-      &.open {
-        z-index: bs.$zindex-modal;
-        transform: translateX(0);
-      }
+    &:not(:global(.open)) {
+      transform: translateX(-100%);
+    }
+    &:global(.open) {
+      z-index: bs.$zindex-modal;
+      transform: translateX(0);
     }
   }
 }
 
 
-.grw-sidebar :global {
+.grw-sidebar {
 
   // overwrite simplebar-react css
-  .simplebar-scrollbar::before {
+  :global(.simplebar-scrollbar::before) {
     background-color:var(--bs-gray-500);
   }
 
 }
 
 @include bs.color-mode(light) {
-  .grw-sidebar :global {
+  .grw-sidebar {
     --bs-border-color: var(--grw-highlight-200);
 
-    .sidebar-contents-container {
+    :global(.sidebar-contents-container) {
       background-color: color-mix(in srgb, var(--grw-highlight-100), var(--bs-body-bg));
     }
   }
 
   // frosted glass effect in collapsed mode
   .grw-sidebar {
-    &:global {
-      &.grw-sidebar-collapsed {
-        .sidebar-contents-container {
-          background-color: rgba(var(--grw-highlight-100-rgb), .8);
-          backdrop-filter: blur(20px);
-        }
+    &:global(.grw-sidebar-collapsed) {
+      :global(.sidebar-contents-container) {
+        background-color: rgba(var(--grw-highlight-100-rgb), .8);
+        backdrop-filter: blur(20px);
       }
     }
   }
 }
 
 @include bs.color-mode(dark) {
-  .grw-sidebar :global {
+  .grw-sidebar {
     --bs-color: var(--bs-gray-400);
     --bs-border-color: var(--grw-highlight-800);
 
-    .sidebar-contents-container {
+    :global(.sidebar-contents-container) {
       background-color: color-mix(in srgb, var(--grw-highlight-800), var(--bs-body-bg));
     }
   }
 
   // frosted glass effect in collapsed mode
   .grw-sidebar {
-    &:global {
-      &.grw-sidebar-collapsed {
-        .sidebar-contents-container {
-          background-color: rgba(var(--grw-highlight-800-rgb), .8);
-          backdrop-filter: blur(20px);
-        }
+    &:global(.grw-sidebar-collapsed) {
+      :global(.sidebar-contents-container) {
+        background-color: rgba(var(--grw-highlight-800-rgb), .8);
+        backdrop-filter: blur(20px);
       }
     }
   }

+ 0 - 2
apps/app/src/client/components/Sidebar/Sidebar.tsx

@@ -40,8 +40,6 @@ import { ResizableAreaFallback } from './ResizableArea/ResizableAreaFallback';
 import { SidebarHead } from './SidebarHead';
 import { SidebarNav, type SidebarNavProps } from './SidebarNav';
 
-import 'simplebar-react/dist/simplebar.min.css';
-
 import styles from './Sidebar.module.scss';
 
 const SidebarContents = dynamic(

+ 3 - 3
apps/app/src/client/components/Sidebar/SidebarContents.module.scss

@@ -1,16 +1,16 @@
 @use '@growi/core-styles/scss/bootstrap/init' as bs;
 
-.grw-sidebar-contents :global {
+.grw-sidebar-contents {
   --bs-heading-color: var(--bs-tertiary-color);
   --bs-body-color: var(--bs-secondary-color);
   --bs-link-color-rgb: var(--bs-secondary-color-rgb);
   --bs-link-opacity: .75;
 
-  .list-group-item {
+  :global(.list-group-item) {
     --bs-list-group-bg: transparent;
   }
 
-  .wip-page-badge {
+  :global(.wip-page-badge) {
     --bs-badge-font-size: 0.5rem;
   }
 }

+ 2 - 2
apps/app/src/client/components/Sidebar/SidebarHead/SidebarHead.module.scss

@@ -3,7 +3,7 @@
 
 // == Colors
 @include bs.color-mode(light) {
-  .grw-sidebar-head :global {
+  .grw-sidebar-head {
     background-color: var(
       --grw-sidebar-head-bg,
       var(
@@ -15,7 +15,7 @@
 }
 
 @include bs.color-mode(dark) {
-  .grw-sidebar-head :global {
+  .grw-sidebar-head {
     background-color: var(
       --grw-sidebar-head-bg,
       var(

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