diff --git a/AGENTS.md b/AGENTS.md
index 5522d6c..79ecacc 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -48,5 +48,7 @@
## セキュリティ & 設定の注意点
- 環境ファイル: `.env.local`(開発用)、`.env.production`(ビルド用)。秘密情報はコミット禁止。
+- `pnpm build` を行う場合、直前に `.output` ディレクトリを削除すること。もし他のプロセスが掴んでロックされている場合は `taskkill /IM node.exe /F` 後に `Remove-Item -Force .output` で削除する。
+- `pnpm build` もしくは `pnpm preview` を実行しCLIがタイムアウトした場合は処理を中断して状況を報告すること。
- `runtimeConfig` のキー(例: `API_BASE_URL`, `GOOGLE_CLIENT_ID`)は正しく設定すること。
- ローカル HTTPS は `public/` 内の証明書を使用。警告が出たら信頼設定を行うこと。
diff --git a/assets/styles/primeicons.css b/assets/styles/primeicons.css
new file mode 100644
index 0000000..768afda
--- /dev/null
+++ b/assets/styles/primeicons.css
@@ -0,0 +1,1329 @@
+@font-face {
+ font-family: 'primeicons';
+ font-display: block;
+ src: url("/fonts/primeicons.woff2") format("woff2");
+ font-weight: normal;
+ font-style: normal;
+}
+
+.pi {
+ font-family: 'primeicons';
+ speak: none;
+ font-style: normal;
+ font-weight: normal;
+ font-variant: normal;
+ text-transform: none;
+ line-height: 1;
+ display: inline-block;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+.pi:before {
+ --webkit-backface-visibility:hidden;
+ backface-visibility: hidden;
+}
+
+.pi-fw {
+ width: 1.28571429em;
+ text-align: center;
+}
+
+.pi-spin {
+ -webkit-animation: fa-spin 2s infinite linear;
+ animation: fa-spin 2s infinite linear;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .pi-spin {
+ -webkit-animation-delay: -1ms;
+ animation-delay: -1ms;
+ -webkit-animation-duration: 1ms;
+ animation-duration: 1ms;
+ -webkit-animation-iteration-count: 1;
+ animation-iteration-count: 1;
+ -webkit-transition-delay: 0s;
+ transition-delay: 0s;
+ -webkit-transition-duration: 0s;
+ transition-duration: 0s;
+ }
+}
+
+@-webkit-keyframes fa-spin {
+ 0% {
+ -webkit-transform: rotate(0deg);
+ transform: rotate(0deg);
+ }
+ 100% {
+ -webkit-transform: rotate(359deg);
+ transform: rotate(359deg);
+ }
+}
+
+@keyframes fa-spin {
+ 0% {
+ -webkit-transform: rotate(0deg);
+ transform: rotate(0deg);
+ }
+ 100% {
+ -webkit-transform: rotate(359deg);
+ transform: rotate(359deg);
+ }
+}
+
+.pi-folder-plus:before {
+ content: "\ea05";
+}
+
+.pi-receipt:before {
+ content: "\ea06";
+}
+
+.pi-asterisk:before {
+ content: "\ea07";
+}
+
+.pi-face-smile:before {
+ content: "\ea08";
+}
+
+.pi-pinterest:before {
+ content: "\ea09";
+}
+
+.pi-expand:before {
+ content: "\ea0a";
+}
+
+.pi-pen-to-square:before {
+ content: "\ea0b";
+}
+
+.pi-wave-pulse:before {
+ content: "\ea0c";
+}
+
+.pi-turkish-lira:before {
+ content: "\ea0d";
+}
+
+.pi-spinner-dotted:before {
+ content: "\ea0e";
+}
+
+.pi-crown:before {
+ content: "\ea0f";
+}
+
+.pi-pause-circle:before {
+ content: "\ea10";
+}
+
+.pi-warehouse:before {
+ content: "\ea11";
+}
+
+.pi-objects-column:before {
+ content: "\ea12";
+}
+
+.pi-clipboard:before {
+ content: "\ea13";
+}
+
+.pi-play-circle:before {
+ content: "\ea14";
+}
+
+.pi-venus:before {
+ content: "\ea15";
+}
+
+.pi-cart-minus:before {
+ content: "\ea16";
+}
+
+.pi-file-plus:before {
+ content: "\ea17";
+}
+
+.pi-microchip:before {
+ content: "\ea18";
+}
+
+.pi-twitch:before {
+ content: "\ea19";
+}
+
+.pi-building-columns:before {
+ content: "\ea1a";
+}
+
+.pi-file-check:before {
+ content: "\ea1b";
+}
+
+.pi-microchip-ai:before {
+ content: "\ea1c";
+}
+
+.pi-trophy:before {
+ content: "\ea1d";
+}
+
+.pi-barcode:before {
+ content: "\ea1e";
+}
+
+.pi-file-arrow-up:before {
+ content: "\ea1f";
+}
+
+.pi-mars:before {
+ content: "\ea20";
+}
+
+.pi-tiktok:before {
+ content: "\ea21";
+}
+
+.pi-arrow-up-right-and-arrow-down-left-from-center:before {
+ content: "\ea22";
+}
+
+.pi-ethereum:before {
+ content: "\ea23";
+}
+
+.pi-list-check:before {
+ content: "\ea24";
+}
+
+.pi-thumbtack:before {
+ content: "\ea25";
+}
+
+.pi-arrow-down-left-and-arrow-up-right-to-center:before {
+ content: "\ea26";
+}
+
+.pi-equals:before {
+ content: "\ea27";
+}
+
+.pi-lightbulb:before {
+ content: "\ea28";
+}
+
+.pi-star-half:before {
+ content: "\ea29";
+}
+
+.pi-address-book:before {
+ content: "\ea2a";
+}
+
+.pi-chart-scatter:before {
+ content: "\ea2b";
+}
+
+.pi-indian-rupee:before {
+ content: "\ea2c";
+}
+
+.pi-star-half-fill:before {
+ content: "\ea2d";
+}
+
+.pi-cart-arrow-down:before {
+ content: "\ea2e";
+}
+
+.pi-calendar-clock:before {
+ content: "\ea2f";
+}
+
+.pi-sort-up-fill:before {
+ content: "\ea30";
+}
+
+.pi-sparkles:before {
+ content: "\ea31";
+}
+
+.pi-bullseye:before {
+ content: "\ea32";
+}
+
+.pi-sort-down-fill:before {
+ content: "\ea33";
+}
+
+.pi-graduation-cap:before {
+ content: "\ea34";
+}
+
+.pi-hammer:before {
+ content: "\ea35";
+}
+
+.pi-bell-slash:before {
+ content: "\ea36";
+}
+
+.pi-gauge:before {
+ content: "\ea37";
+}
+
+.pi-shop:before {
+ content: "\ea38";
+}
+
+.pi-headphones:before {
+ content: "\ea39";
+}
+
+.pi-eraser:before {
+ content: "\ea04";
+}
+
+.pi-stopwatch:before {
+ content: "\ea01";
+}
+
+.pi-verified:before {
+ content: "\ea02";
+}
+
+.pi-delete-left:before {
+ content: "\ea03";
+}
+
+.pi-hourglass:before {
+ content: "\e9fe";
+}
+
+.pi-truck:before {
+ content: "\ea00";
+}
+
+.pi-wrench:before {
+ content: "\e9ff";
+}
+
+.pi-microphone:before {
+ content: "\e9fa";
+}
+
+.pi-megaphone:before {
+ content: "\e9fb";
+}
+
+.pi-arrow-right-arrow-left:before {
+ content: "\e9fc";
+}
+
+.pi-bitcoin:before {
+ content: "\e9fd";
+}
+
+.pi-file-edit:before {
+ content: "\e9f6";
+}
+
+.pi-language:before {
+ content: "\e9f7";
+}
+
+.pi-file-export:before {
+ content: "\e9f8";
+}
+
+.pi-file-import:before {
+ content: "\e9f9";
+}
+
+.pi-file-word:before {
+ content: "\e9f1";
+}
+
+.pi-gift:before {
+ content: "\e9f2";
+}
+
+.pi-cart-plus:before {
+ content: "\e9f3";
+}
+
+.pi-thumbs-down-fill:before {
+ content: "\e9f4";
+}
+
+.pi-thumbs-up-fill:before {
+ content: "\e9f5";
+}
+
+.pi-arrows-alt:before {
+ content: "\e9f0";
+}
+
+.pi-calculator:before {
+ content: "\e9ef";
+}
+
+.pi-sort-alt-slash:before {
+ content: "\e9ee";
+}
+
+.pi-arrows-h:before {
+ content: "\e9ec";
+}
+
+.pi-arrows-v:before {
+ content: "\e9ed";
+}
+
+.pi-pound:before {
+ content: "\e9eb";
+}
+
+.pi-prime:before {
+ content: "\e9ea";
+}
+
+.pi-chart-pie:before {
+ content: "\e9e9";
+}
+
+.pi-reddit:before {
+ content: "\e9e8";
+}
+
+.pi-code:before {
+ content: "\e9e7";
+}
+
+.pi-sync:before {
+ content: "\e9e6";
+}
+
+.pi-shopping-bag:before {
+ content: "\e9e5";
+}
+
+.pi-server:before {
+ content: "\e9e4";
+}
+
+.pi-database:before {
+ content: "\e9e3";
+}
+
+.pi-hashtag:before {
+ content: "\e9e2";
+}
+
+.pi-bookmark-fill:before {
+ content: "\e9df";
+}
+
+.pi-filter-fill:before {
+ content: "\e9e0";
+}
+
+.pi-heart-fill:before {
+ content: "\e9e1";
+}
+
+.pi-flag-fill:before {
+ content: "\e9de";
+}
+
+.pi-circle:before {
+ content: "\e9dc";
+}
+
+.pi-circle-fill:before {
+ content: "\e9dd";
+}
+
+.pi-bolt:before {
+ content: "\e9db";
+}
+
+.pi-history:before {
+ content: "\e9da";
+}
+
+.pi-box:before {
+ content: "\e9d9";
+}
+
+.pi-at:before {
+ content: "\e9d8";
+}
+
+.pi-arrow-up-right:before {
+ content: "\e9d4";
+}
+
+.pi-arrow-up-left:before {
+ content: "\e9d5";
+}
+
+.pi-arrow-down-left:before {
+ content: "\e9d6";
+}
+
+.pi-arrow-down-right:before {
+ content: "\e9d7";
+}
+
+.pi-telegram:before {
+ content: "\e9d3";
+}
+
+.pi-stop-circle:before {
+ content: "\e9d2";
+}
+
+.pi-stop:before {
+ content: "\e9d1";
+}
+
+.pi-whatsapp:before {
+ content: "\e9d0";
+}
+
+.pi-building:before {
+ content: "\e9cf";
+}
+
+.pi-qrcode:before {
+ content: "\e9ce";
+}
+
+.pi-car:before {
+ content: "\e9cd";
+}
+
+.pi-instagram:before {
+ content: "\e9cc";
+}
+
+.pi-linkedin:before {
+ content: "\e9cb";
+}
+
+.pi-send:before {
+ content: "\e9ca";
+}
+
+.pi-slack:before {
+ content: "\e9c9";
+}
+
+.pi-sun:before {
+ content: "\e9c8";
+}
+
+.pi-moon:before {
+ content: "\e9c7";
+}
+
+.pi-vimeo:before {
+ content: "\e9c6";
+}
+
+.pi-youtube:before {
+ content: "\e9c5";
+}
+
+.pi-flag:before {
+ content: "\e9c4";
+}
+
+.pi-wallet:before {
+ content: "\e9c3";
+}
+
+.pi-map:before {
+ content: "\e9c2";
+}
+
+.pi-link:before {
+ content: "\e9c1";
+}
+
+.pi-credit-card:before {
+ content: "\e9bf";
+}
+
+.pi-discord:before {
+ content: "\e9c0";
+}
+
+.pi-percentage:before {
+ content: "\e9be";
+}
+
+.pi-euro:before {
+ content: "\e9bd";
+}
+
+.pi-book:before {
+ content: "\e9ba";
+}
+
+.pi-shield:before {
+ content: "\e9b9";
+}
+
+.pi-paypal:before {
+ content: "\e9bb";
+}
+
+.pi-amazon:before {
+ content: "\e9bc";
+}
+
+.pi-phone:before {
+ content: "\e9b8";
+}
+
+.pi-filter-slash:before {
+ content: "\e9b7";
+}
+
+.pi-facebook:before {
+ content: "\e9b4";
+}
+
+.pi-github:before {
+ content: "\e9b5";
+}
+
+.pi-twitter:before {
+ content: "\e9b6";
+}
+
+.pi-step-backward-alt:before {
+ content: "\e9ac";
+}
+
+.pi-step-forward-alt:before {
+ content: "\e9ad";
+}
+
+.pi-forward:before {
+ content: "\e9ae";
+}
+
+.pi-backward:before {
+ content: "\e9af";
+}
+
+.pi-fast-backward:before {
+ content: "\e9b0";
+}
+
+.pi-fast-forward:before {
+ content: "\e9b1";
+}
+
+.pi-pause:before {
+ content: "\e9b2";
+}
+
+.pi-play:before {
+ content: "\e9b3";
+}
+
+.pi-compass:before {
+ content: "\e9ab";
+}
+
+.pi-id-card:before {
+ content: "\e9aa";
+}
+
+.pi-ticket:before {
+ content: "\e9a9";
+}
+
+.pi-file-o:before {
+ content: "\e9a8";
+}
+
+.pi-reply:before {
+ content: "\e9a7";
+}
+
+.pi-directions-alt:before {
+ content: "\e9a5";
+}
+
+.pi-directions:before {
+ content: "\e9a6";
+}
+
+.pi-thumbs-up:before {
+ content: "\e9a3";
+}
+
+.pi-thumbs-down:before {
+ content: "\e9a4";
+}
+
+.pi-sort-numeric-down-alt:before {
+ content: "\e996";
+}
+
+.pi-sort-numeric-up-alt:before {
+ content: "\e997";
+}
+
+.pi-sort-alpha-down-alt:before {
+ content: "\e998";
+}
+
+.pi-sort-alpha-up-alt:before {
+ content: "\e999";
+}
+
+.pi-sort-numeric-down:before {
+ content: "\e99a";
+}
+
+.pi-sort-numeric-up:before {
+ content: "\e99b";
+}
+
+.pi-sort-alpha-down:before {
+ content: "\e99c";
+}
+
+.pi-sort-alpha-up:before {
+ content: "\e99d";
+}
+
+.pi-sort-alt:before {
+ content: "\e99e";
+}
+
+.pi-sort-amount-up:before {
+ content: "\e99f";
+}
+
+.pi-sort-amount-down:before {
+ content: "\e9a0";
+}
+
+.pi-sort-amount-down-alt:before {
+ content: "\e9a1";
+}
+
+.pi-sort-amount-up-alt:before {
+ content: "\e9a2";
+}
+
+.pi-palette:before {
+ content: "\e995";
+}
+
+.pi-undo:before {
+ content: "\e994";
+}
+
+.pi-desktop:before {
+ content: "\e993";
+}
+
+.pi-sliders-v:before {
+ content: "\e991";
+}
+
+.pi-sliders-h:before {
+ content: "\e992";
+}
+
+.pi-search-plus:before {
+ content: "\e98f";
+}
+
+.pi-search-minus:before {
+ content: "\e990";
+}
+
+.pi-file-excel:before {
+ content: "\e98e";
+}
+
+.pi-file-pdf:before {
+ content: "\e98d";
+}
+
+.pi-check-square:before {
+ content: "\e98c";
+}
+
+.pi-chart-line:before {
+ content: "\e98b";
+}
+
+.pi-user-edit:before {
+ content: "\e98a";
+}
+
+.pi-exclamation-circle:before {
+ content: "\e989";
+}
+
+.pi-android:before {
+ content: "\e985";
+}
+
+.pi-google:before {
+ content: "\e986";
+}
+
+.pi-apple:before {
+ content: "\e987";
+}
+
+.pi-microsoft:before {
+ content: "\e988";
+}
+
+.pi-heart:before {
+ content: "\e984";
+}
+
+.pi-mobile:before {
+ content: "\e982";
+}
+
+.pi-tablet:before {
+ content: "\e983";
+}
+
+.pi-key:before {
+ content: "\e981";
+}
+
+.pi-shopping-cart:before {
+ content: "\e980";
+}
+
+.pi-comments:before {
+ content: "\e97e";
+}
+
+.pi-comment:before {
+ content: "\e97f";
+}
+
+.pi-briefcase:before {
+ content: "\e97d";
+}
+
+.pi-bell:before {
+ content: "\e97c";
+}
+
+.pi-paperclip:before {
+ content: "\e97b";
+}
+
+.pi-share-alt:before {
+ content: "\e97a";
+}
+
+.pi-envelope:before {
+ content: "\e979";
+}
+
+.pi-volume-down:before {
+ content: "\e976";
+}
+
+.pi-volume-up:before {
+ content: "\e977";
+}
+
+.pi-volume-off:before {
+ content: "\e978";
+}
+
+.pi-eject:before {
+ content: "\e975";
+}
+
+.pi-money-bill:before {
+ content: "\e974";
+}
+
+.pi-images:before {
+ content: "\e973";
+}
+
+.pi-image:before {
+ content: "\e972";
+}
+
+.pi-sign-in:before {
+ content: "\e970";
+}
+
+.pi-sign-out:before {
+ content: "\e971";
+}
+
+.pi-wifi:before {
+ content: "\e96f";
+}
+
+.pi-sitemap:before {
+ content: "\e96e";
+}
+
+.pi-chart-bar:before {
+ content: "\e96d";
+}
+
+.pi-camera:before {
+ content: "\e96c";
+}
+
+.pi-dollar:before {
+ content: "\e96b";
+}
+
+.pi-lock-open:before {
+ content: "\e96a";
+}
+
+.pi-table:before {
+ content: "\e969";
+}
+
+.pi-map-marker:before {
+ content: "\e968";
+}
+
+.pi-list:before {
+ content: "\e967";
+}
+
+.pi-eye-slash:before {
+ content: "\e965";
+}
+
+.pi-eye:before {
+ content: "\e966";
+}
+
+.pi-folder-open:before {
+ content: "\e964";
+}
+
+.pi-folder:before {
+ content: "\e963";
+}
+
+.pi-video:before {
+ content: "\e962";
+}
+
+.pi-inbox:before {
+ content: "\e961";
+}
+
+.pi-lock:before {
+ content: "\e95f";
+}
+
+.pi-unlock:before {
+ content: "\e960";
+}
+
+.pi-tags:before {
+ content: "\e95d";
+}
+
+.pi-tag:before {
+ content: "\e95e";
+}
+
+.pi-power-off:before {
+ content: "\e95c";
+}
+
+.pi-save:before {
+ content: "\e95b";
+}
+
+.pi-question-circle:before {
+ content: "\e959";
+}
+
+.pi-question:before {
+ content: "\e95a";
+}
+
+.pi-copy:before {
+ content: "\e957";
+}
+
+.pi-file:before {
+ content: "\e958";
+}
+
+.pi-clone:before {
+ content: "\e955";
+}
+
+.pi-calendar-times:before {
+ content: "\e952";
+}
+
+.pi-calendar-minus:before {
+ content: "\e953";
+}
+
+.pi-calendar-plus:before {
+ content: "\e954";
+}
+
+.pi-ellipsis-v:before {
+ content: "\e950";
+}
+
+.pi-ellipsis-h:before {
+ content: "\e951";
+}
+
+.pi-bookmark:before {
+ content: "\e94e";
+}
+
+.pi-globe:before {
+ content: "\e94f";
+}
+
+.pi-replay:before {
+ content: "\e94d";
+}
+
+.pi-filter:before {
+ content: "\e94c";
+}
+
+.pi-print:before {
+ content: "\e94b";
+}
+
+.pi-align-right:before {
+ content: "\e946";
+}
+
+.pi-align-left:before {
+ content: "\e947";
+}
+
+.pi-align-center:before {
+ content: "\e948";
+}
+
+.pi-align-justify:before {
+ content: "\e949";
+}
+
+.pi-cog:before {
+ content: "\e94a";
+}
+
+.pi-cloud-download:before {
+ content: "\e943";
+}
+
+.pi-cloud-upload:before {
+ content: "\e944";
+}
+
+.pi-cloud:before {
+ content: "\e945";
+}
+
+.pi-pencil:before {
+ content: "\e942";
+}
+
+.pi-users:before {
+ content: "\e941";
+}
+
+.pi-clock:before {
+ content: "\e940";
+}
+
+.pi-user-minus:before {
+ content: "\e93e";
+}
+
+.pi-user-plus:before {
+ content: "\e93f";
+}
+
+.pi-trash:before {
+ content: "\e93d";
+}
+
+.pi-external-link:before {
+ content: "\e93c";
+}
+
+.pi-window-maximize:before {
+ content: "\e93b";
+}
+
+.pi-window-minimize:before {
+ content: "\e93a";
+}
+
+.pi-refresh:before {
+ content: "\e938";
+}
+
+.pi-user:before {
+ content: "\e939";
+}
+
+.pi-exclamation-triangle:before {
+ content: "\e922";
+}
+
+.pi-calendar:before {
+ content: "\e927";
+}
+
+.pi-chevron-circle-left:before {
+ content: "\e928";
+}
+
+.pi-chevron-circle-down:before {
+ content: "\e929";
+}
+
+.pi-chevron-circle-right:before {
+ content: "\e92a";
+}
+
+.pi-chevron-circle-up:before {
+ content: "\e92b";
+}
+
+.pi-angle-double-down:before {
+ content: "\e92c";
+}
+
+.pi-angle-double-left:before {
+ content: "\e92d";
+}
+
+.pi-angle-double-right:before {
+ content: "\e92e";
+}
+
+.pi-angle-double-up:before {
+ content: "\e92f";
+}
+
+.pi-angle-down:before {
+ content: "\e930";
+}
+
+.pi-angle-left:before {
+ content: "\e931";
+}
+
+.pi-angle-right:before {
+ content: "\e932";
+}
+
+.pi-angle-up:before {
+ content: "\e933";
+}
+
+.pi-upload:before {
+ content: "\e934";
+}
+
+.pi-download:before {
+ content: "\e956";
+}
+
+.pi-ban:before {
+ content: "\e935";
+}
+
+.pi-star-fill:before {
+ content: "\e936";
+}
+
+.pi-star:before {
+ content: "\e937";
+}
+
+.pi-chevron-left:before {
+ content: "\e900";
+}
+
+.pi-chevron-right:before {
+ content: "\e901";
+}
+
+.pi-chevron-down:before {
+ content: "\e902";
+}
+
+.pi-chevron-up:before {
+ content: "\e903";
+}
+
+.pi-caret-left:before {
+ content: "\e904";
+}
+
+.pi-caret-right:before {
+ content: "\e905";
+}
+
+.pi-caret-down:before {
+ content: "\e906";
+}
+
+.pi-caret-up:before {
+ content: "\e907";
+}
+
+.pi-search:before {
+ content: "\e908";
+}
+
+.pi-check:before {
+ content: "\e909";
+}
+
+.pi-check-circle:before {
+ content: "\e90a";
+}
+
+.pi-times:before {
+ content: "\e90b";
+}
+
+.pi-times-circle:before {
+ content: "\e90c";
+}
+
+.pi-plus:before {
+ content: "\e90d";
+}
+
+.pi-plus-circle:before {
+ content: "\e90e";
+}
+
+.pi-minus:before {
+ content: "\e90f";
+}
+
+.pi-minus-circle:before {
+ content: "\e910";
+}
+
+.pi-circle-on:before {
+ content: "\e911";
+}
+
+.pi-circle-off:before {
+ content: "\e912";
+}
+
+.pi-sort-down:before {
+ content: "\e913";
+}
+
+.pi-sort-up:before {
+ content: "\e914";
+}
+
+.pi-sort:before {
+ content: "\e915";
+}
+
+.pi-step-backward:before {
+ content: "\e916";
+}
+
+.pi-step-forward:before {
+ content: "\e917";
+}
+
+.pi-th-large:before {
+ content: "\e918";
+}
+
+.pi-arrow-down:before {
+ content: "\e919";
+}
+
+.pi-arrow-left:before {
+ content: "\e91a";
+}
+
+.pi-arrow-right:before {
+ content: "\e91b";
+}
+
+.pi-arrow-up:before {
+ content: "\e91c";
+}
+
+.pi-bars:before {
+ content: "\e91d";
+}
+
+.pi-arrow-circle-down:before {
+ content: "\e91e";
+}
+
+.pi-arrow-circle-left:before {
+ content: "\e91f";
+}
+
+.pi-arrow-circle-right:before {
+ content: "\e920";
+}
+
+.pi-arrow-circle-up:before {
+ content: "\e921";
+}
+
+.pi-info:before {
+ content: "\e923";
+}
+
+.pi-info-circle:before {
+ content: "\e924";
+}
+
+.pi-home:before {
+ content: "\e925";
+}
+
+.pi-spinner:before {
+ content: "\e926";
+}
+
diff --git a/components/common/Chart.vue b/components/common/Chart.vue
index fd0c301..ef65aa0 100644
--- a/components/common/Chart.vue
+++ b/components/common/Chart.vue
@@ -1,111 +1,162 @@
-
-
-
-
-
-
+watch(
+ () => chartRef.value?.chart,
+ (chart) => {
+ if (chart) {
+ emit("chartReady", chart)
+ }
+ },
+)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/nuxt.config.ts b/nuxt.config.ts
index 661bbe7..807077f 100644
--- a/nuxt.config.ts
+++ b/nuxt.config.ts
@@ -1,4 +1,4 @@
-// https://nuxt.com/docs/api/configuration/nuxt-config
+// https://nuxt.com/docs/api/configuration/nuxt-config
import fs from "node:fs"
import path from "node:path"
import { PrimeVueResolver } from "@primevue/auto-import-resolver"
@@ -20,11 +20,72 @@ const httpsOptions = {
),
}
+function sanitizeChunkName(name: string): string {
+ return name
+ .replace(/[^a-z0-9]+/gi, "-")
+ .replace(/^-+|-+$/g, "")
+ .toLowerCase()
+}
+
+function resolvePrimevueChunk(id: string): string | null {
+ const formsMatch = id.match(/[/]@primevue[/]forms[/](?:dist[/]esm[/])?(.+)/)
+ if (formsMatch?.[1]) {
+ const segments = formsMatch[1].split(/[/]/).filter(Boolean)
+ if (segments.length > 0) {
+ const formsKey =
+ segments[0] === "components" && segments[1]
+ ? `component-${sanitizeChunkName(segments[1])}`
+ : segments[0]
+ return `primevue-forms-${sanitizeChunkName(formsKey)}`
+ }
+ return "primevue-forms"
+ }
+
+ const themeMatch = id.match(
+ /[/]@primeuix[/]themes[/](?:dist[/]esm[/])?(.+)/,
+ )
+ if (themeMatch?.[1]) {
+ const segments = themeMatch[1].split(/[/]/).filter(Boolean)
+ return `primevue-theme-${sanitizeChunkName(segments[0] ?? "core")}`
+ }
+
+ const coreMatch = id.match(/[/]@primevue[/]core[/](?:dist[/]esm[/])?(.+)/)
+ if (coreMatch?.[1]) {
+ const segments = coreMatch[1].split(/[/]/).filter(Boolean)
+ if (segments.length > 0) {
+ const [first, second] = segments
+ if (first === "components" && second) {
+ return `primevue-core-${sanitizeChunkName(second)}`
+ }
+ return `primevue-core-${sanitizeChunkName(first)}`
+ }
+ return "primevue-core"
+ }
+
+ const componentMatch = id.match(/[/]primevue[/](?:dist[/]esm[/])?(.+)/)
+ if (componentMatch?.[1]) {
+ const segments = componentMatch[1].split(/[/]/).filter(Boolean)
+ if (segments.length > 0) {
+ const [first, second] = segments
+ if (first === "components" && second) {
+ return `primevue-${sanitizeChunkName(second)}`
+ }
+ return `primevue-${sanitizeChunkName(first)}`
+ }
+ return "primevue-core"
+ }
+
+ if (/[\\/](?:primevue|@primevue|@primeuix)[\\/]/.test(id)) {
+ return "primevue-core"
+ }
+
+ return null
+}
export default defineNuxtConfig({
compatibilityDate: "2025-05-15",
nitro: {
preset: "cloudflare_module", // SSR用 .output/server/wrangler.json が生成される 'cloudflare' はPages用
- minify: false,
+ minify: true,
cloudflare: {
deployConfig: true,
nodeCompat: true,
@@ -43,7 +104,7 @@ export default defineNuxtConfig({
},
devtools: { enabled: false },
typescript: {
- typeCheck: true,
+ typeCheck: process.env.NUXT_TYPESCRIPT_CHECK !== "false",
tsConfig: {
include: ["types/**/*.d.ts"],
},
@@ -92,12 +153,32 @@ export default defineNuxtConfig({
},
},
css: [
- "primeicons/primeicons.css",
+ "@/assets/styles/primeicons.css",
"@loadingio/loading.css/loading.css",
"@/assets/styles/tailwind_v4.scss",
"@/assets/styles/index.scss",
],
modules: ["@pinia/nuxt", "@primevue/nuxt-module", "@nuxtjs/i18n"],
+ hooks: {
+ "components:extend"(components) {
+ for (let i = components.length - 1; i >= 0; i--) {
+ if (
+ components[i].filePath?.includes(
+ "vite/modulepreload-polyfill.js",
+ )
+ ) {
+ components.splice(i, 1)
+ }
+ }
+ },
+ "imports:extend"(imports) {
+ for (let i = imports.length - 1; i >= 0; i--) {
+ if (imports[i].from === "vite/modulepreload-polyfill.js") {
+ imports.splice(i, 1)
+ }
+ }
+ },
+ },
devServer: {
host: "pull.log",
port: 4649,
@@ -109,6 +190,46 @@ export default defineNuxtConfig({
vite: {
build: {
sourcemap: process.env.NUXT_SOURCEMAP === "true",
+ rollupOptions: {
+ output: {
+ manualChunks(id) {
+ if (!id.includes("node_modules")) {
+ return undefined
+ }
+
+ if (
+ id.includes("chart.js") ||
+ id.includes("chartjs-plugin") ||
+ id.includes("vue-chartjs")
+ ) {
+ return "chart"
+ }
+
+ if (/(?:^|[\\/])primeicons[\\/]/.test(id)) {
+ return "primeicons"
+ }
+
+ const primevueChunk = resolvePrimevueChunk(id)
+ if (primevueChunk) {
+ return primevueChunk
+ }
+
+ if (id.includes("luxon")) {
+ return "luxon"
+ }
+
+ if (
+ id.includes("vue-i18n") ||
+ id.includes("@intlify")
+ ) {
+ return "i18n"
+ }
+
+ return undefined
+ },
+ },
+ },
+ chunkSizeWarningLimit: 1200,
},
plugins: [
Components({
@@ -136,6 +257,17 @@ export default defineNuxtConfig({
primevue: {
usePrimeVue: true,
autoImport: true,
+ components: {
+ exclude: [
+ "Dialog",
+ "Drawer",
+ "DataTable",
+ "Column",
+ "ColumnGroup",
+ "FileUpload",
+ "Editor",
+ ],
+ },
options: {
theme: {
preset: PullLogPreset,
diff --git a/package.json b/package.json
index 5730d27..ff390ec 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "pulllog",
- "version": "1.2.0",
+ "version": "1.2.1",
"description": "Manage Your Gacha History and Analyze Statistics.",
"author": "MAGIC METHODS",
"license": "UNLICENSED",
@@ -9,7 +9,7 @@
"type": "module",
"scripts": {
"clean": "rm -rf .nuxt node_modules/.vite",
- "build": "NUXT_SOURCEMAP=false NUXT_TYPESCRIPT_CHECK=false nuxt build --dotenv .env.production",
+ "build": "cross-env NUXT_SOURCEMAP=false NUXT_TYPESCRIPT_CHECK=false nuxt build --dotenv .env.production",
"dev": "nuxt dev --dotenv .env.local",
"generate": "nuxt generate --dotenv .env.production",
"preview": "nuxt preview --dotenv .env.production",
@@ -60,6 +60,7 @@
"@types/luxon": "^3.6.2",
"@types/sortablejs": "^1.15.8",
"autoprefixer": "^10.4.21",
+ "cross-env": "^7.0.3",
"husky": "^9.1.7",
"lint-staged": "^16.1.6",
"postcss": "^8.5.6",
diff --git a/plugins/primevue-lazy.client.ts b/plugins/primevue-lazy.client.ts
new file mode 100644
index 0000000..4ecf114
--- /dev/null
+++ b/plugins/primevue-lazy.client.ts
@@ -0,0 +1,23 @@
+import { defineAsyncComponent } from "vue"
+import { defineNuxtPlugin } from "#app"
+
+const components = {
+ Dialog: () => import("primevue/dialog"),
+ Drawer: () => import("primevue/drawer"),
+ DataTable: () => import("primevue/datatable"),
+ Column: () => import("primevue/column"),
+ ColumnGroup: () => import("primevue/columngroup"),
+ FileUpload: () => import("primevue/fileupload"),
+} as const
+
+export default defineNuxtPlugin((nuxtApp) => {
+ for (const [name, loader] of Object.entries(components)) {
+ nuxtApp.vueApp.component(
+ name,
+ defineAsyncComponent({
+ loader: async () => (await loader()).default,
+ suspensible: false,
+ }),
+ )
+ }
+})
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 5fa21e0..4e6d754 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -105,6 +105,9 @@ importers:
autoprefixer:
specifier: ^10.4.21
version: 10.4.21(postcss@8.5.6)
+ cross-env:
+ specifier: ^7.0.3
+ version: 7.0.3
husky:
specifier: ^9.1.7
version: 9.1.7
@@ -2356,6 +2359,11 @@ packages:
resolution: {integrity: sha512-p9nwwR4qyT5W996vBZhdvBCnMhicY5ytZkR4D1Xj0wuTDEiMnjwR57Q3RXYY/s0EpX6Ay3vgIcfaR+ewGHsi+g==}
engines: {node: '>=18.0'}
+ cross-env@7.0.3:
+ resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==}
+ engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'}
+ hasBin: true
+
cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
@@ -7601,6 +7609,10 @@ snapshots:
croner@9.1.0: {}
+ cross-env@7.0.3:
+ dependencies:
+ cross-spawn: 7.0.6
+
cross-spawn@7.0.6:
dependencies:
path-key: 3.1.1
diff --git a/public/fonts/primeicons.woff2 b/public/fonts/primeicons.woff2
new file mode 100644
index 0000000..26fb219
Binary files /dev/null and b/public/fonts/primeicons.woff2 differ