mcpField
D
dynamic-themes-with-codemirror

dynamic-themes-with-codemirror

github.com/rodydavisデザイン・UI6056 回インストール2 週間前に更新
リポジトリ

dynamic-themes-with-codemirror について

Dynamic Themes with CodeMirrorは、CodeMirrorを組み込み、Material Designのカラー ユーティリティでテーマを動的に変更するLitウェブコンポーネントのチュートリアルです。TypeScriptとViteを使う開発者を対象としています。

主な機能

  1. 01Litベースのコードエディターコンポーネント
  2. 02CodeMirrorとの連携
  3. 03基準色からのテーマ生成
  4. 04CSSカスタムプロパティによる色設定

活用シーン

  1. 01ウェブコンポーネントへのテーマ付きコード画面の追加
  2. 02選んだ基準色に応じたエディター配色の変更

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/rodydavis/skills --skill dynamic-themes-with-codemirror

インストール後、Claude Code で/dynamic-themes-with-codemirrorを入力して読み込まれたか確認します。

詳細

CodeMirror で動的テーマを作成する

この記事では、Lit のウェブコンポーネントをセットアップし、それを使って CodeMirror を使用するコードウィンドウを作成し、Material Design で動的テーマを適用する方法を解説します。

TLDR 最終的なソースはこちらのリンク、オンラインデモはこちらです。

前提条件

  • Vscode
  • Node >= 16
  • Typescript

はじめに

まず、ターミナルでプロジェクトの場所に移動し、次のコマンドを実行します。

npm init @vitejs/app --template lit-ts

次に、プロジェクト名 codemirror-dynamic-theme を入力します。続いて、vscode でプロジェクトを開き、依存関係をインストールします。

cd codemirror-dynamic-theme
npm i lit codemirror @material/material-color-utilities
npm i -D @types/node @types/codemirror
code .

vite.config.ts を次のように更新します。

import { defineConfig } from "vite";
import { resolve } from "path";

export default defineConfig({
  base: "/codemirror-dynamic-theme/",
  build: {
    rollupOptions: {
      input: {
        main: resolve(__dirname, "index.html"),
      },
    },
  },
});

テンプレート

index.html を開き、次のように更新します。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>CodeMirror Dynamic Theme</title>
    <script type="module" src="/src/code-window.ts"></script>
    <style>
      body {
        margin: 0;
        padding: 0;
      }
    </style>
  </head>
  <body>
    <code-window> </code-window>
  </body>
</html>

ウェブコンポーネント

コンポーネントを更新する前に、my-element.ts の名前を code-window.ts に変更する必要があります。

code-window.ts を開き、次のように更新します。

import { html, css, LitElement, unsafeCSS } from "lit";
import { customElement, property } from "lit/decorators.js";
import {
  applyTheme,
  argbFromHex,
  hexFromArgb,
  themeFromSourceColor,
} from "@material/material-color-utilities";
import CodeMirror from "codemirror";
import codemirrorStyles from "codemirror/lib/codemirror.css";

@customElement("code-window")
export class CodeWindow extends LitElement {
  static styles = css`
    ${unsafeCSS(codemirrorStyles)}

    main {
      width: 100vw;
      height: 100vh;
      background-color: var(--md-sys-color-background);
      color: var(--md-sys-color-on-background);
      --header-height: 48px;
      --input-size: 32px;
    }

    .toolbar {
      height: var(--header-height);
      background-color: var(--md-sys-color-primary-container);
      color: var(--md-sys-color-on-primary-container);
      display: flex;
      align-items: center;
    }

    .actions > * {
      margin-left: 4px;
      margin-right: 4px;
    }

    .toolbar .title {
      font-family: sans-serif;
      font-size: 18px;
      padding-left: 4px;
    }

    .toolbar .actions {
      display: flex;
      align-items: center;
    }

    .toolbar a {
      padding: 0;
      margin: 0;
      padding-left: 8px;
      padding-right: 8px;
      display: flex;
      align-items: center;
      cursor: pointer;
    }

    input[type="color"] {
      width: calc(var(--input-size) * 2);
      height: var(--input-size);
      outline: none;
      border: none;
      border-radius: 50%;
      background-color: var(--md-sys-color-primary-container);
    }
    input[type="color"]::-webkit-color-swatch-wrapper {
      padding: 0;
    }
    input[type="color"]::-webkit-color-swatch {
      border: none;
      border-radius: var(--input-size);
      border: var(--md-sys-color-outline) solid 1px;
    }

    button {
      border: none;
      border-radius: 4px;
      padding: 8px;
    }

    .tertiary {
      background-color: var(--md-sys-color-tertiary);
      color: var(--md-sys-color-on-tertiary);
    }

    .secondary {
      background-color: var(--md-sys-color-secondary);
      color: var(--md-sys-color-on-secondary);
    }

    .spacer {
      flex: 1;
    }

    .editor {
      height: calc(100% - var(--header-height));
      width: 100%;
    }
  `;

  @property() value = [
    `import {html, css, LitElement} from 'lit';`,
    `import {customElement, property} from 'lit/decorators.js';`,
    ``,
    `@customElement('simple-greeting')`,
    `export class SimpleGreeting extends LitElement {`,
    `  static styles = css\`p { color: blue }\`;`,
    ``,
    `  @property()`,
    `  name = 'Somebody';`,
    ``,
    `  render() {`,
    `    return html\`<p>Hello, \${this.name}!</p>\`;`,
    `  }`,
    `}`,
  ].join("\n");

  @property() color = "#6750A4";
  @property({ type: Boolean }) dark = window.matchMedia(
    "(prefers-color-scheme: dark)"
  ).matches;

  render() {
    return html`<main>
      <header class="toolbar">
        <div class="title">${document.title}</div>
        <div class="spacer"></div>
        <div class="actions">
          <button class="secondary" @click=${this.toggleDark.bind(this)}>
            ${this.dark ? "Light" : "Dark"}
          </button>
          <button class="tertiary" @click=${this.randomColor.bind(this)}>
            Random
          </button>
          <input
            type="color"
            .value=${this.color}
            @input=${this.onColor.bind(this)}
          />
        </div>
      </header>
      <div class="editor"></div>
    </main>`;
  }

  firstUpdated() {
    const root = this.shadowRoot!.querySelector(".editor") as HTMLElement;
    const editor = CodeMirror(root, {
      value: this.value,
      mode: "javascript",
      lineNumbers: true,
      lineWrapping: true,
      indentUnit: 4,
      tabSize: 4,
      indentWithTabs: true,
      autofocus: true,
    });
    console.debug(editor);
    editor.setSize("100%", `100%`);
    this.updateTheme();
    window
      .matchMedia("(prefers-color-scheme: dark)")
      .addEventListener("change", (e) => {
        this.dark = e.matches;
        this.updateTheme();
      });
  }

  private updateTheme() {
   // TODO: Generate Theme
  }

  private setColor(val: string) {
    this.color = val;
    this.updateTheme();
  }

  private onColor(e: Event) {
    const target = e.target as HTMLInputElement;
    this.setColor(target.value);
  }

  private randomColor() {
    const letters = "0123456789ABCDEF";
    let color = "#";
    for (let i = 0; i < 6; i++) {
      color += letters[Math.floor(Math.random() * 16)];
    }
    this.setColor(color);
  }

  private toggleDark() {
    this.dark = !this.dark;
    this.updateTheme();
  }

}

declare global {
  interface HTMLElementTagNameMap {
    "code-window": CodeWindow;
  }
}

ここでは、基本的なレイアウトに必要なスタイルを読み込むため、エディターの基本設定を行っています。

また、toggleDark や setColor など、要素のプロパティ更新を処理するメソッドもいくつかあります。アプリケーションを実行すると、次のように表示されます。

README の画像

まだ見栄えはよくありませんが、CodeMirror のテーマを追加して読み込めます。src/theme.ts ファイルを作成し、次のように更新します。

import { css } from "lit";

export const codeMirrorTheme = css`
  .CodeMirror {
    background-color: var(--md-sys-color-background);
    color: var(--md-sys-color-on-background);
  }

  .CodeMirror-gutters {
    background: var(--md-sys-color-surface-variant);
    color: var(--md-sys-color-on-surface-variant);
    border: none;
  }

  .CodeMirror-guttermarker,
  .CodeMirror-guttermarker-subtle,
  .CodeMirror-linenumber {
    color: var(--md-sys-color-on-background);
  }

  .CodeMirror-cursor {
    border-left: 1px solid var(--md-sys-color-primary);
  }
  .cm-fat-cursor .CodeMirror-cursor {
    background-color: var(--md-sys-color-background);
  }
  .cm-animate-fat-cursor {
    background-color: var(--md-sys-color-background);
  }

  div.CodeMirror-selected {
    background: var(--md-sys-color-surface-variant);
  }

  .CodeMirror-focused div.CodeMirror-selected {
    background: var(--md-sys-color-surface-variant);
  }

  .CodeMirror-line::selection,
  .CodeMirror-line > span::selection,
  .CodeMirror-line > span > span::selection {
    background: var(--md-sys-color-surface-variant);
  }

  .CodeMirror-line::-moz-selection,
  .CodeMirror-line > span::-moz-selection,
  .CodeMirror-line > span > span::-moz-selection {
    background: var(--md-sys-color-surface-variant);
  }

  .CodeMirror-activeline-background {
    background: var(--md-sys-color-surface);
  }

  .cm-keyword {
    color: var(--md-custom-color-keyword) !important;
  }

  .cm-operator {
    color: var(--md-custom-color-operator) !important;
  }

  .cm-variable-2 {
    color: var(--md-custom-color-variable-2) !important;
  }

  .cm-variable-3,
  .cm-type {
    color: var(--md-custom-color-variable-3) !important;
  }

  .cm-builtin {
    color: var(--md-custom-color-builtin) !important;
  }

  .cm-atom {
    color: var(--md-custom-color-atom) !important;
  }

  .cm-number {
    color: var(--md-custom-color-number) !important;
  }

  .cm-def {
    color: var(--md-custom-color-def) !important;
  }

  .cm-string {
    color: var(--md-custom-color-string) !important;
  }

  .cm-string-2 {
    color: var(--md-custom-color-string-2) !important;
  }

  .cm-comment {
    color: var(--md-custom-color-comment) !important;
  }

  .cm-variable {
    color: var(--md-custom-color-variable) !important;
  }

  .cm-tag {
    color: var(--md-custom-color-tag) !important;
  }

  .cm-meta {
    color: var(--md-custom-color-meta) !important;
  }

  .cm-attribute {
    color: var(--md-custom-color-attribute) !important;
  }

  .cm-property {
    color: var(--md-custom-color-property) !important;
  }

  .cm-qualifier {
    color: var(--md-custom-color-qualifier) !important;
  }

  .cm-variable-3,
  .cm-type {
    color: var(--md-custom-color-variable-3) !important;
  }

  .cm-error {
    color: var(--md-sys-color-on-error);
    background-color: var(--md-sys-color-error);
  }

  .CodeMirror-matchingbracket {
    text-decoration: underline;
    color: var(--md-sys-color-on-surface);
  }
`;

ここでは、簡単に更新できるよう、すべてのスタイルを CSS カスタムプロパティ として定義しています。

次に、テーマをインポートし、コンポーネントにスタイルを適用します。

import { codeMirrorTheme } from "./theme";

@customElement("code-window")
export class CodeWindow extends LitElement {
  static styles = css`
    ${unsafeCSS(codemirrorStyles)}
    ${codeMirrorTheme}
    ...

続いて、要素に updateTheme メソッドを実装します。

updateTheme() {
    const source = this.color;
    const dark = this.dark;
    const target = this.shadowRoot!.querySelector("main") as HTMLElement;
    const properties = [
        `--md-custom-color-keyword: #c75779;`,
        `--md-custom-color-operator: #008800;`,
        `--md-custom-color-variable: #90ccff;`,
        `--md-custom-color-variable-2: #dd7700;`,
        `--md-custom-color-variable-3: #3333bb;`,
        `--md-custom-color-variable-3: #decb6b;`,
        `--md-custom-color-builtin: #003388;`,
        `--md-custom-color-atom: #bb4646;`,
        `--md-custom-color-number: #b4c5ff;`,
        `--md-custom-color-def: #82aaff;`,
        `--md-custom-color-string: #ffb4a9;`,
        `--md-custom-color-string-2: #ffb4a9;`,
        `--md-custom-color-comment: #888888;`,
        `--md-custom-color-tag: #000080;`,
        `--md-custom-color-meta: #a9c7ff;`,
        `--md-custom-color-attribute: #008080;`,
        `--md-custom-color-property: #336699;`,
        `--md-custom-color-qualifier: #690;`,
    ];
    const customColors = properties.map((property) => {
        const [key, value] = property.split(":");
        const name = key.trim().replace(/^--md-custom-color-/, "");
        const color = argbFromHex(value.trim().replace(";", ""));
        return {
        name,
        value: color,
        blend: true,
        };
    });
    const theme = themeFromSourceColor(argbFromHex(source), customColors);
    applyTheme(theme, { target, dark });
    for (const custom of theme.customColors) {
        const name = custom.color.name;
        const section = dark ? custom.dark : custom.light;
        target.style.setProperty(
        `--md-custom-color-${name}`,
        hexFromArgb(section.color)
        );
    }
}

ここでは Material Color Utilities パッケージを使用し、元の色からテーマを生成しています。カスタムカラーを利用して、テーマに値を調和させることができます。

テーマを生成したら、ルート要素に適用し、カスタムプロパティによってエディターが更新されるようにします。

README の画像

元の色を変更すると、テーマを更新できます。

README の画像

明るさを変更して、色を設定することもできます。

README の画像

README の画像

まとめ

Lit を使った構築について詳しく知りたい場合は、こちらのドキュメントをお読みください(リンク)。

この例のソースはこちらにあります(リンク)。

VSCode 用の動的テーマを試してみたい場合は、こちらで拡張機能を作成しました(リンク)。

コメント