## 仕様
><img src="../../image/2025-09-14-01-34-44.png" width="300">
- article.tsとdata.tsがある
- data.tsはスニペット記事の元データの配列となっている
- article.tsはprops経由で記事データを受け取り、コンポーネントを返す
- navigation, トグルボタン, articleのスタイルを用意する
- page.tsではgetStorageThemeのようなlocalStorageを扱う関数を用意する
- localStorageからdefault themeを取得し、もしなければlight-themeと設定するようにしたい
- useStateでthemeを取得し、初期化する値はgetStorageThemeから取得したthemeにする
- トグルボタンを用意して、現在のthemeに応じてライト、ダークを切り替える
- useEffectでclassNameがthemeに応じて切り替わり、localStorageの値も切り替わるようにする
- 実装のイメージとしては、添付した画像、app/lesson3のdata.tsとpage.module.cssがサンプルとなる。
- cssについては現在のnextjs構成では適さない要素の指定もあるので、適宜修正すること。
## 実装
> <video controls src="https://i.gyazo.com/25e74dbc2ae752103e341fbb73c767cc.mp4" width="300" title="ダーク・ライドモード"></video>
[github](https://github.com/kmishima16/react-project-school/blob/main/app/lesson3/page.tsx)
TODO
- テーマの管理と切り替え機能
- ローカルストレージへのテーマの保存と読み込み
- UIコンポーネントの表示
### テーマ管理
今回は、page.tsの親要素の`style`属性に、テーマに関する`CSSProterties`の変数を代入する形で実装を行った。
CSS Modulesで定義した`page.module.css`の各クラスも[[CSS変数]]を参照するようにしている。
bgColor,primaryColor,secondaryColor,borderColor,shadowColorなどのCSS変数を用いるようにすることで、
useStateによる[[CSSProterties]]の切り替えに対応してスタイルが切り替わるようになる。
#### CSS変数を切り替える方法
ReactでstyleプロパティにReact.CSSPropertiesを指定し、その中にCSS変数を含めることで、その要素とその子孫要素におけるCSS変数の値が確定(上書き)されます。
- [インラインスタイルでCSSを渡すときの方はCSSProterties](https://qiita.com/penpeenpen/items/a587d2a0c76a1cb97fec)
より大規模なアプリケーションであれば、Hooksを使ってCSS変数を切り替えるような仕組みにすることで、状態管理をhooksやcontextとして切り出しすることができる。
- [hooksでCSS変数を切り替える](https://zenn.dev/stin/articles/how-to-change-theme-with-css-modues)
### ローカルストレージのテーマ保存
ローカルストレージはグローバル変数`localStorage: Storage`として利用可能のようだった。
useStateの変数theme変更の副作用として、[[localStorage]]内にテーマを保存しておき、セッションが途切れても保存テーマを読み込めるようにしている。
localStorageからの値取得はコンポーネントの外で関数として定義しておく。
→ ただし、こうするとレンダリングが終わってからuseEffectが走るので、一瞬ライトテーマになった後ダークテーマに切り替わってしまう・・・
```ts
function getStorageTheme(): Theme {
if (typeof window !== "undefined") {
const savedTheme: Theme = localStorage.getItem("theme") as Theme;
return savedTheme || "light-theme";
}
return "light-theme";
}
```
#### `if (typeof window !== "undefined")`について
ブラウザ環境(window オブジェクトが存在するか)をチェックしてからアクセスしているため、
サーバーサイドでのレンダリング時(Next.jsなどのフレームワークを使用している場合)にエラーが発生するのを防いでいる。
SSRの場合、`window`プロパティが存在しない場合もあるので、エラーハンドリングは行っておく必要がある。
- [localStorageの利用をカスタムフック化させる事例](https://qiita.com/YuukiYoshida/items/7c1cecfa80aba9f7edc9)
lealogさんも同じような調査をしていた
- [NextJSでサーバ・クライアントで一度だけやりたい処理](https://leaysgur.github.io/posts/2020/09/09/132905/)
### CSSのマークアップ
- containerはflex, flex-direction:columnにする
- navigationはflex, justify-content: space-between, align-items: centerにする
- titleはposition: relativeにしている
- トグルボタンのコンテナ
- display: flex, align-items:centerにしている
- Icon: display: inline-block
- themeToggle: 楕円形のボタン position: relative, outline: none, overflow:hidden
- スライダーが絶対位置なので、この楕円を基準点にしている
- outline:none にすることで、フォーカス時の枠線を非表示にしている
- overflow: hiddenで、ボックスから要素がはみ出たものは隠すようにしている
- toggleSlider: 小さい円のボタン position: absoluteで絶対位置の操作にして、topとleftで初期位置を決める
- あとはdark, lightでスタイルを切り替える
- darkになったときは、`translateX(24px)`で、右に移動させる
- main: flex:1 にしているのはなぜ?
- article
- gridレイアウト:`grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));`
- コンテナの幅に応じて、最低300pxの幅を持つ列をできるだけ多く配置してください。もしスペースが余ったら、すべての列が均等にそのスペースを分け合ってコンテナ幅いっぱいに広がるようにしてください。
- article: flex, flex-direction: column
- タイトル
- メタデータ: flex, space-between, align-items:center, flex-wrap: wrap
- Date
- ReadForLength
- 余白のつけかたとか、フレキシブルボックスの使い分けとか、かなり参考になるなぁ🤔
#### mainで`flex:1`にしている理由は?
このコードにおける `.main` の `flex: 1;` は、親要素である `.container` の中で、利用可能な残りのスペースをすべて占めるように伸長させるためのものです。
`.container` は `display: flex;` と `flex-direction: column;` を持っているため、子要素は縦に並びます。
`.main` に `flex: 1;` を指定することで、他の兄弟要素(この場合は `.navigation`)が必要なスペースを取った後、
残った垂直方向のスペースをすべて `.main` が埋めるようになります。
これにより、コンテンツの量に関わらず、メインコンテンツエリアが常にページの残りの部分を埋めるため、レイアウトの整合性を保つことができます。
## 余白の付け方のパターン
#CSSテク
### 中央揃えのテクニック (margin: 0 auto;)
コンテナ要素を親要素の中央に配置する定番の方法です。
```css
.container {
box-sizing: border-box;
margin: 0 auto;
padding: 20px;
```
### 要素間の垂直方向の距離 (margin-bottom)
特定の要素の下に余白を作り、次の要素との間にスペースを設けます。
```css
.navigation {
/* ... */
margin-bottom: 2rem;
/* ... */
}
.articleTitle {
/* ... */
margin-bottom: 1rem;
/* ... */
}
```
### コンテンツの周りに余白を作る
コンテナやカード、ボタンなどの内側に余白を持たせ、コンテンツが詰まって見えないようにします。
```css
.container {
/* ... */
padding: 20px;
/* ... */
}
.article {
padding: 2rem;
/* ... */
}
```
### パターン1: ヘッダー要素の横並びと両端揃え
justify-content: space-between で子要素を両端に配置し、align-items: center で垂直方向の中央に揃えています。Webサイトのヘッダーでよく使われるレイアウトです。
```css
.navigation {
display: flex;
justify-content: space-between;
align-items: center;
/* ... */
}
```
### パターン2: ページ全体の基本構造(縦積み)
flex-direction: column を使うと、子要素を縦方向に並べることができます。ページ全体のレイアウトや、カード内のコンテンツを上から下に並べる際に便利です。
```css
.container {
/* ... */
display: flex;
flex-direction: column;
/* ... */
}
.article {
/* ... */
display: flex;
flex-direction: column;
}
```
### パターン3: 特定の要素を一番下に配置するテクニック
これは少し応用的な使い方です。Flexコンテナの中で、特定の子要素に margin-top: auto を設定すると、その要素はコンテナの一番下まで押し下げられます。 このファイルでは、記事カード (.article) の高さが異なっても、記事の抜粋 (.articleSnippet) が常に下部に表示されるようにするために使われています。
```css
.article {
padding: 2rem;
height: 100%;
display: flex;
flex-direction: column;
}
.articleSnippet {
/* ... */
flex: 1;
margin-top: auto;
}
```