fresh (deno) で shadcn/ui を使う 実践編
モチベーション
Radix UI と Tailwind CSS を用いたコンポーネントライブラリ shadcn/ui を fresh で使う。
デモのページ。読み込みに時間がかかるので注意。
↓ の続きのようなもの
Github (上のスクラップのリポジトリとは異なる) と Deno.land
このスクラップをちゃんと書いて、そこから記事にしたいという気持ちはある。気持ちは。
進捗
v0.1.2
型もつけて、とりあえずの利用には十分耐えられる水準にはなったはず。
デモページの表示も、比較的まともになったと思う。
メモ
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 が異なる (どのレベルで?) と不味いらしい
使用方法
- fresh の
index.tsx
に CSS values を追加する。
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>
</>
)
}
-
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,
};
- コンポーネントを 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>
)
}
-
コンポーネントのファイルをコピペする
-
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'
-
cn
およびElementRef, ComponentPropsWithoutRef
を独自のパスに差し替えるimport { cn } from "../modules/lib/utils.ts" import { ElementRef, ComponentPropsWithoutRef, } from "../modules/lib/type-utils.ts"
-
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"
-
vendor する
deno vendor ./components/scroll-area.tsx
-
vendor した各ファイル内のパスを import_map に依存しない形式に変更する
-
コンポーネントのファイル内の module のパスを vendor したものに変更する
-
型定義ファイルをコピペ&修正し、読み込む
-
class ↔ className の互換を調整する