Open8

fresh (deno) で shadcn/ui を使う 実践編

nikogolinikogoli

モチベーション

https://ui.shadcn.com/
Radix UI と Tailwind CSS を用いたコンポーネントライブラリ shadcn/ui を fresh で使う。

デモのページ。読み込みに時間がかかるので注意。
https://testing-shadcn-ui-in-fresh.deno.dev/


↓ の続きのようなもの
https://zenn.dev/nikogoli/scraps/f80e1d4688d6b0

Github (上のスクラップのリポジトリとは異なる) と Deno.land
https://github.com/nikogoli/testing_shadcn_ui_for_deno

https://deno.land/x/testing_shadcn_ui_for_deno

nikogolinikogoli

このスクラップをちゃんと書いて、そこから記事にしたいという気持ちはある。気持ちは。

nikogolinikogoli

進捗

v0.1.2

型もつけて、とりあえずの利用には十分耐えられる水準にはなったはず。
デモページの表示も、比較的まともになったと思う。

nikogolinikogoli
メモ

Cannot read properties of undefined (reading '__H') エラー

esm.sh\stable\preact@10.19.6\denonext\compat.js における preact と hook の読み込みを ../../../stable/preact@10.19.6/denonext/preact.mjs から https://esm.sh/preact@10.19.6?target=denonextに変更すると回避できた。(hook は preact@10.19.6/hooks?target=denonext に)
とにかく、fresh が使う preact と preact/compat が使う preact が異なる (どのレベルで?) と不味いらしい

nikogolinikogoli

使用方法


  1. fresh の index.tsx に CSS values を追加する。
index.tsx
import { Head } from "$fresh/runtime.ts";
import App from "../islands/App.tsx"

export default function Home(){
  return (
    <>
      <Head>
        <style>{`
          :root{
            --background: 0 0% 100%;
            --foreground: 240 10% 3.9%;
            --card: 0 0% 100%;
            --card-foreground: 240 10% 3.9%;
            --popover: 0 0% 100%;
            --popover-foreground: 240 10% 3.9%;
            --primary: 240 5.9% 10%;
            --primary-foreground: 0 0% 98%;
            --secondary: 240 4.8% 95.9%;
            --secondary-foreground: 240 5.9% 10%;
            --muted: 240 4.8% 95.9%;
            --muted-foreground: 240 3.8% 46.1%;
            --accent: 240 4.8% 95.9%;
            --accent-foreground: 240 5.9% 10%;
            --destructive: 0 72.22% 50.59%;
            --destructive-foreground: 0 0% 98%;
            --border: 240 5.9% 90%;
            --input: 240 5.9% 90%;
            --ring: 240 5% 64.9%;
            --radius: 0.5rem
          }
      `}</style>
      </Head>
      <div>
        <App />
      </div>
    </>
  )
}
nikogolinikogoli
  1. twind.config.ts に色とアニメーションの設定を追加する。
追加内容 (とても長いので閲覧注意)
twind.config.ts
import { defineConfig, Preset } from "twind";
import presetTailwind from "https://esm.sh/@twind/preset-tailwind@1.1.4";
import presetAutoprefix from "https://esm.sh/@twind/preset-autoprefix@1.0.7";

export default {
...defineConfig({
  presets: [presetTailwind() as Preset, presetAutoprefix()],
  theme:{
    extend: {
      colors:{
        border: "hsl(var(--border) / <alpha-value>)",
        input: "hsl(var(--input) / <alpha-value>)",
        ring: "hsl(var(--ring) / <alpha-value>)",
        background: "hsl(var(--background) / <alpha-value>)",
        foreground: "hsl(var(--foreground) / <alpha-value>)",
        primary: {
          DEFAULT: "hsl(var(--primary) / <alpha-value>)",
          foreground: "hsl(var(--primary-foreground) / <alpha-value>)",
        },
        secondary: {
          DEFAULT: "hsl(var(--secondary) / <alpha-value>)",
          foreground: "hsl(var(--secondary-foreground) / <alpha-value>)",
        },
        destructive: {
          DEFAULT: "hsl(var(--destructive) / <alpha-value>)",
          foreground: "hsl(var(--destructive-foreground) / <alpha-value>)",
        },
        muted: {
          DEFAULT: "hsl(var(--muted) / <alpha-value>)",
          foreground: "hsl(var(--muted-foreground) / <alpha-value>)",
        },
        accent: {
          DEFAULT: "hsl(var(--accent) / <alpha-value>)",
          foreground: "hsl(var(--accent-foreground) / <alpha-value>)",
        },
        popover: {
          DEFAULT: "hsl(var(--popover) / <alpha-value>)",
          foreground: "hsl(var(--popover-foreground) / <alpha-value>)",
        },
        card: {
          DEFAULT: "hsl(var(--card) / <alpha-value>)",
          foreground: "hsl(var(--card-foreground) / <alpha-value>)",
        },
      },
      borderRadius: {
        xl: "calc(var(--radius) + 4px)",
        lg: "var(--radius)",
        md: "calc(var(--radius) - 2px)",
        sm: "calc(var(--radius) - 4px)",
      },
      keyframes: {
        accordion:{
          down:{
            '0%': { height: '0' },
            '100%': { height: 'var(--radix-accordion-content-height)' },
          },
          up:{
            '0%': { height: 'var(--radix-accordion-content-height)' },
            '100%': { height: '0' },
          }
        },
        dialog: {
          in: {
            '0%':{
              opacity: '0',
              transform: 'translate3d(-50%,-48%,0) scale3d(.95,.95,.95) rotate(0)',
            }
          }, 
          out: {
            'to':{
              opacity: '0',
              transform: 'translate3d(-50%,-48%,0) scale3d(.95,.95,.95) rotate(0)',
            }
          }
        },
        enter: {
          100: {
            '0%':{
              opacity: '0',
              transform: 'translate3d(0,0,0) scale3d(1,1,1) rotate(0)',
            }
          },
          90: {
            '0%':{
              opacity: '0',
              transform: 'translate3d(0,0,0) scale3d(.9,.9,.9) rotate(0)',
            }
          },
        },
        exit: {
          100: {
            "to":{
              opacity: '0',
              transform: 'translate3d(0,0,0) scale3d(1,1,1) rotate(0)',
            }
          },
          95: {
            'to':{
              opacity: '0',
              transform: 'translate3d(0,0,0) scale3d(.95,.95,.95) rotate(0)',
            }
          },
        },
        slidein: {
          fromright: {
            half95: {
              '0%':{
                opacity: '0',
                transform: 'translate3d(0.5rem,0,0) scale3d(.95,.95,.95) rotate(0)',
              }
            },
            52: {
              '0%':{
                opacity: '0',
                transform: 'translate3d(13rem,0,0) scale3d(1,1,1) rotate(0)',
              }
            },
            full: {
              '0%':{
                opacity: '0',
                transform: 'translate3d(100%,0,0) scale3d(1,1,1) rotate(0)',
              }
            }
          },
          fromleft: {
            half95: {
              '0%':{
                opacity: '0',
                transform: 'translate3d(-0.5rem,0,0) scale3d(.95,.95,.95) rotate(0)',
              }
            },
            52: {
              '0%':{
                opacity: '0',
                transform: 'translate3d(-13rem,0,0) scale3d(1,1,1) rotate(0)',
              }
            },
            full: {
              '0%':{
                opacity: '0',
                transform: 'translate3d(-100%,0,0) scale3d(1,1,1) rotate(0)',
              }
            }
          },
          fromtop: {
            half95: {
              '0%':{
                opacity: '0',
                transform: 'translate3d(0,-0.5rem,0) scale3d(.95,.95,.95) rotate(0)',
              }
            },
            full: {
              '0%':{
                opacity: '0',
                transform: 'translate3d(0,-100%,0) scale3d(1,1,1) rotate(0)',
              }
            }
          },
          frombottom: {
            half95: {
              '0%':{
                opacity: '0',
                transform: 'translate3d(0,0.5rem,0) scale3d(.95,.95,.95) rotate(0)',
              }
            },
            full: {
              '0%':{
                opacity: '0',
                transform: 'translate3d(0,100%,0) scale3d(1,1,1) rotate(0)',
              }
            }
          }
        },
        slideout: {
          toright: {
            52: {
              'to':{
                opacity: '0',
                transform: 'translate3d(13rem,0,0) scale3d(1,1,1) rotate(0)',
              }
            },
            full: {
              'to':{
                opacity: '0',
                transform: 'translate3d(100%,0,0) scale3d(1,1,1) rotate(0)',
              }
            }
          },
          toleft: {
            52: {
              'to':{
                opacity: '0',
                transform: 'translate3d(-13rem,0,0) scale3d(1,1,1) rotate(0)',
              }
            },
            full: {
              'to':{
                opacity: '0',
                transform: 'translate3d(-100%,0,0) scale3d(1,1,1) rotate(0)',
              }
            }
          },
          totop: {
            full: {
              'to':{
                opacity: '0',
                transform: 'translate3d(0,-100%,0) scale3d(1,1,1) rotate(0)',
              }
            }
          },
          tobottom: {
            full: {
              'to':{
                opacity: '0',
                transform: 'translate3d(0,100%,0) scale3d(1,1,1) rotate(0)',
              }
            }
          }
        }
      },
      animation: {
        "accordion-down": 'accordion-down .2s ease-out',
        "accordion-up": 'accordion-up .2s ease-out',
        "dialog-in": 'dialog-in .15s',
        "dialog-out": 'dialog-out .15s',
        "in": 'enter-100 .15s',
        "out": 'exit-100 .15s',
        "zoomin-90": 'enter-90 .15s',
        "zoomout-95": 'exit-95 .15s',
        "slidein-from-right-50": 'slidein-fromright-half95 .15s',
        "slidein-from-left-50": 'slidein-fromleft-half95 .15s',
        "slidein-from-top-50": 'slidein-fromtop-half95 .15s',
        "slidein-from-bottom-50": 'slidein-frombottom-half95 .15s',
        "slidein-from-right-52": 'slidein-fromright-52 .15s',
        "slidein-from-left-52": 'slidein-fromleft-52 .15s',
        "slidein-from-right-full": 'slidein-fromright-full .15s',
        "slidein-from-left-full": 'slidein-fromleft-full .15s',
        "slidein-from-top-full": 'slidein-fromtop-full .15s',
        "slidein-from-bottom-full": 'slidein-frombottom-full .15s',
        "slideout-to-right-52": 'slideout-toright-52 .15s',
        "slideout-to-left-52": 'slideout-toleft-52 .15s',
        "slideout-to-right-full": 'slideout-toright-full .15s',
        "slideout-to-left-full": 'slideout-toleft-full .15s',
        "slideout-to-top-full": 'slideout-totop-full .15s',
        "slideout-to-bottom-full": 'slideout-tobottom-full .15s',
      }
    }
  }
}),
selfURL: import.meta.url,
};
nikogolinikogoli
  1. コンポーネントを import して利用する
import { Label } from "https://deno.land/x/testing_shadcn_ui_for_deno@0.1.0/components/label.tsx"
import { Switch } from "https://deno.land/x/testing_shadcn_ui_for_deno@0.1.0/components/switch.tsx"

export function SwitchDemo() {
  return (
    <div class="flex items-center space-x-2">
      <Switch id="airplane-mode" />
      <Label htmlFor="airplane-mode">Airplane Mode</Label>
    </div>
  )
}
nikogolinikogoli
  1. コンポーネントのファイルをコピペする

  2. React のパスを pract/compat に置き換える

    // @deno-types="https://esm.sh/v128/preact@10.19.6/compat/src/index.d.ts"
    import * as React from '../modules/esm.sh/preact@10.19.6/compat.js'
    
  3. cn および ElementRef, ComponentPropsWithoutRef を独自のパスに差し替える

    import { cn } from "../modules/lib/utils.ts"
    import {
      ElementRef,
      ComponentPropsWithoutRef,
    } from "../modules/lib/type-utils.ts"
    
  4. radix-ui系の module のパスを esm.sh に変更し、依存モジュールを extarnal にし、さらに react の alias を設定する

    import * as ScrollAreaPrimitive from "https://esm.sh/*@radix-ui/react-scroll-area@1.1.0?alias=react:preact/compat"
    
  5. vendor する deno vendor ./components/scroll-area.tsx

  6. vendor した各ファイル内のパスを import_map に依存しない形式に変更する

  7. コンポーネントのファイル内の module のパスを vendor したものに変更する

  8. 型定義ファイルをコピペ&修正し、読み込む

  9. class ↔ className の互換を調整する