qrac/musubii

v8 開発メモ

Closed this issue · 9 comments

qrac commented

コンセプト

  • 依存させない:コピペで部分利用可・書き捨て可・使い捨て可・プロジェクト側で追えないCSSを減らす
  • 複雑にしない:内部構造の簡略化・最小限の開発環境
  • フルスクラッチ

案件のコンポーネントをChakraMUIみたいな感じで行きたいけど、納品物がSaaS用のテンプレだったりWordPressだったりするし、仮にJamstackなReact案件だとしてもアップデート追う(追ってもらう)のが非現実的なので結局は依存性のない自作に落ち着く。とはいえボタンやフォームのCSSを1から書くのはしんどい。いい感じのものを書いておいてimportではなく簡単にコピペできるようにしておきたい。見た目的にはBootstrapより今風のTailwind UIライクにして、むしろTailwind CSSと併用しても大丈夫にする。

大きな変更点

  • IE非対応
    • 業界全体で対応終了の流れ / 仕事でもほぼ対応していない
    • CSS Variablesの出力分岐をなくせる
    • gapを適応>マイナスマージンを使ったレイアウトをなくせる
    • セレクタに:is()と:where()を使える
  • モディファイアの再考
    • 使用頻度が低く案件で書けば済むものは削除する
    • パターンを増やしだすとキリがないので案件側に任せるかtailwindに任せるか考慮する
  • Sass削除
    • コンポーネント単位の開発で使っていない / 仕事でもまったく使っていない
    • ソースコードが分かりづらくエディタで追いにくい
    • v7はnode-sass準拠なのでSassを継続させるにはDart Sass準拠で作り直す必要がある
  • カラーパレット削除
    • 主要な色以外使っていないため
  • カラーパレットはTailwind Colorから作成
    • importはしないが参考用に何パターンかCSSファイルは作成しておく
    • 昔のBootstrapやMaterial Designは日本のWebデザイン向けには鮮やかすぎたため、v7以前はMUSUBiiのベースとなるMOFTONEなどを自作していた。 Tailwindカラーパレットは昨今の流行りにも合うしそのまま使える色相だと感じる。
    • 色名をTailwindと共用して思い出しやすくする
    • 全色CSS Variablesを未圧縮で読み込んだ場合の容量は8〜9KB
    • Tailwind Colorパレット作成(メモ用)
    • Chakra Colorパレット作成(メモ用)
    • レガシーカラーパレット作成(メモ用)
  • Utilitiy classのxxl~xxsは値が不明確なので1remや16pxなど固定値の名前にする
  • デモはWeb Componentsでカプセル化した方が作りやすいかも
  • デモのコピペ時にCSS変数をコンパイルできると急いでる時に助かる
  • デモソースはリポジトリから切り離す
    • 依存アップデートbotの通知が多すぎ
  • CSSテストにHTML React Alpine.js Viteを採用
  • CSS Variables無し版も作成(minify利用・コピペ用)PostCSS CSS Variables
  • Purgecss系のドキュメントを追加する
  • 高位コンポーネントは別のライブラリとして作成

細かい変更点

  • 全体
    • 近年のデザインテイストに合わせて彩度とコントラストをUP
    • primaryカラーをcyanからskyに変更
    • focus効果をブラウザデフォルトにする
    • hover効果の適応範囲をメディアクエリで絞り込む
    • transition効果の適応範囲をメディアクエリで絞り込むは削除
    • paddingの方向にinlineとblockを使用※案件側でオーバーライドしづらそうなので中止
    • xxl等の命名は2xl等に変更(Tailwindより)
    • ブラウザによってはパフォーマンスが落ちる text-rendering: optimizeLegibility; を削除
    • ブラウザによっては文字が見づらくなる -webkit-font-smoothing: antialiased; を削除
  • Button
    • 和欧混植フォントを指定(混ぜて書くことは稀なためどちらにも最適化しておく)
    • plainの命名をsolidに変更(FontAwesomeやChakraなどよく見かける)
    • mildsubtleボタンを追加(Chakraより・背景はsolidよりも弱くテキストがアクセントカラー)
    • outlineはhoverの色変化を小さく
    • meltの命名は思い出しづらいためghostに戻す
    • ghostのhover背景色はそれぞれの色にする
    • ボタンの高さをremで指定
    • hover colorをfilterで代用
    • disabled効果をopacity透過で統一
    • aタグのdisabledはaria-disabled="true"推奨に変更(is-disabledも使える)
    • infoを使う場面が明確でないので代わりにsecondaryを用意
    • warningを使う場面はほとんどないので削除
    • textボタンを追加
    • sizeを固有Utilityとして設定
    • sizeのxxsはモバイルで押しづらいので削除
    • sizeのxxlは品のないデザインになりがちなので削除
    • strongを削除(アンチエイリアスを削除することで文字の視認性が上がったため)
    • is-width-*を追加
    • is-squareとis-circleを削りis-aspect-squareを追加(Tailwindのclassに寄せた)可変でアスペクト比維持
    • is-roundはis-rounded-fullに変更(Tailwindのclassに寄せた)案件でのborder-radiusパターン追加を想定した命名
    • is-rounded-noneを追加
    • angleにpaddingを付与(widthFull時には反映させない)
    • angleのupとdownを1つのclass指定で設定できるよう変更
    • is-floatingはis-shadowに変更しis-shadow-coloredで色変更を分離 floatingは削除
  • Badge
    • 和欧混植フォントを指定
    • plainの命名をsolidに変更
    • subtleボタンを追加
    • ボタンの高さをremで指定
    • デフォルトのサイズを0.75remに変更
    • sizeを固有Utilityとして設定
    • size変更の値が小さく余白に端数がでがちなのでpaddingは固定値を使用
    • strongを削除
    • is-squareとis-circleを削りis-aspect-squareを追加
    • is-roundはis-rounded-fullに変更
    • アイコンのみの使用を想定しない

開発ディレクトリ

qrac commented

buttons-wip-21122101

ボタンはinfoの使い所が不明確なのでsecondaryに変更し、同じく使い所のなかったwarningを削除。全体的に彩度とコントラストを調整。

qrac commented

Demo with react

試しにブラウザ用Reactで作ったデモ。

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Button</title>
    <link rel="stylesheet" href="../../css/musubii.css" />
    <link rel="stylesheet" href="../demo.css" />
    <script
      crossorigin
      src="https://unpkg.com/react@17/umd/react.development.js"
    ></script>
    <script
      crossorigin
      src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"
    ></script>
    <script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
    <script
      data-plugins="transform-es2015-modules-umd"
      type="text/babel"
      src="../demo.js"
    ></script>
  </head>
  <body>
    <div id="app"></div>
    <script type="text/babel" data-plugins="transform-es2015-modules-umd">
      import { DemoRadios } from "../demo.js"

      const App = () => {
        const variants = ["plain", "outline", "ghost"]
        const [variant, setVariant] = React.useState("plain")
        return (
          <div className="demo-contents">
            <div className="demo-content">
              <DemoRadios
                items={variants}
                name="variant"
                checked={variant}
                action={(value) => setVariant(value)}
              />
            </div>
            <div className="demo-content">
              <Buttons variant={variant} />
            </div>
          </div>
        )
      }

      const Buttons = ({ variant }) => {
        return (
          <div className="demo-buttons">
            <Button variant={variant} text="戻る" />
            <Button variant={variant} text="決定" color="primary" />
            <Button variant={variant} text="変更" color="secondary" />
            <Button variant={variant} text="登録" color="success" />
            <Button variant={variant} text="削除" color="danger" />
          </div>
        )
      }

      const Button = ({ variant, text, color }) => {
        const classNames = [
          "button",
          variant ? `is-${variant}` : "is-plain",
          color && `is-${color}`,
        ].join(" ")
        return (
          <button type="button" className={classNames}>
            {text}
          </button>
        )
      }

      const app = document.querySelector("#app")
      const element = <App />
      ReactDOM.render(element, app)
    </script>
  </body>
</html>
const DemoRadios = ({ items, name, checked, action }) => {
  return (
    <div className="demo-radios">
      {items.map((item, index) => (
        <label className="demo-radio" onClick={() => action(item)} key={index}>
          <input
            type="radio"
            name={name}
            onChange={() => action(item)}
            checked={checked === item}
          />
          <span>{item}</span>
        </label>
      ))}
    </div>
  )
}

export { DemoRadios }
qrac commented

Demo with Alpine.js

ReactDOMのレンダリングがライブリロードと相性良くなかったのでAlpine.jsに変更。

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Button</title>
    <link rel="stylesheet" href="../../css/musubii.css" />
    <link rel="stylesheet" href="../demo.css" />
    <script src="https://unpkg.com/alpinejs" defer></script>
  </head>
  <body>
    <div class="demo-contents" x-data="{ variant: 'plain' }">
      <div class="demo-content">
        <div class="demo-radios">
          <label class="demo-radio">
            <input
              type="radio"
              name="variant"
              x-model="variant"
              value="plain"
            />
            <span>plain</span>
          </label>
          <label class="demo-radio">
            <input
              type="radio"
              name="variant"
              x-model="variant"
              value="outline"
            />
            <span>outline</span>
          </label>
          <label class="demo-radio">
            <input
              type="radio"
              name="variant"
              x-model="variant"
              value="ghost"
            />
            <span>ghost</span>
          </label>
        </div>
      </div>
      <div class="demo-content">
        <div class="demo-buttons">
          <button class="button" :class="'is-'+variant" type="button">
            戻る
          </button>
          <button
            class="button is-primary"
            :class="'is-'+variant"
            type="button"
          >
            決定
          </button>
          <button
            class="button is-secondary"
            :class="'is-'+variant"
            type="button"
          >
            変更
          </button>
          <button
            class="button is-success"
            :class="'is-'+variant"
            type="button"
          >
            登録
          </button>
          <button class="button is-danger" :class="'is-'+variant" type="button">
            削除
          </button>
        </div>
      </div>
    </div>
  </body>
</html>
qrac commented

alpine.jsのx-bindが複数classで効かない(previewプラグインのバグかも)のと、ホットリロードでStateを保持しながらプレビューしたくなったのでVite+Reactでデモを作成する。

qrac commented

React Componentsの作業を楽にしたかったので、TypeScriptで書き直した。

qrac commented

Viteからのtsconfig.json

{
  "compilerOptions": {
    "target": "ESNext",
    "useDefineForClassFields": true,
    "lib": ["DOM", "DOM.Iterable", "ESNext"],
    "allowJs": false,
    "skipLibCheck": false,
    "esModuleInterop": false,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "module": "ESNext",
    "moduleResolution": "Node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx"
  },
  "include": ["./src", "./test"]
}
qrac commented

floating・shadowのパターンは複雑になりすぎるため、プロジェクト側でTailwindなどを使う方が良さそう。shadow colordはCSS Variable無しでは実装できないので、無しバージョンを作る今回のアップデートに向かないというのもある。

qrac commented

初期のメモ

コピペで部分利用できるようにする 内部構造の簡略化
image image

アプデのきっかけ

全部入りimportによる下地作りは、コンポーネント単位の開発で使いにくいかもと思った。

v7リリース後の1年(2021年)にMUSUBiiを参照したのは主にボタンとフォーム。案件でMUSUBiiのボタンangleスタイルやselectのスタイルを使おうとしたものの、そのためだけにimportで依存を増やしたくなかった。importして納品してもその後のアップデートは追えないだろうし、class名カスタマイズなどの機能を使った場合はドキュメントもあまり役に立たなくなる。スタイルだけコピペして依存なく使えると便利。

また、Sassを用いた構造が難解で改修しづらいため簡略化する。

qrac commented

このコンセプトの開発は別のOSSに引き継ぐためクローズします。