ساخت یک باد دم ایمن با عصاره وانیل

Tailwind CSS یک فریم ورک CSS اولین ابزار محبوب است که یک گردش کار عالی برای ایجاد رابط کاربری فراهم می کند. من شما را تشویق می کنم که آن را امتحان کنید اگر قبلاً این کار را نکرده اید. دلیلی وجود دارد که به سرعت در حال افزایش محبوبیت است.
اگرچه Tailwind از بسیاری جهات گام بزرگی به جلو بوده است، اما همیشه در برخی زمینهها کمی کمبود داشته است. یکی از مسائل کلیدی این است که ایمنی نوع وجود ندارد، که می تواند منجر به ارجاع یک مقدار نادرست و عدم درک آن تا زمان اجرا شود. مشکل دیگری که من دیده ام این است که مردم در دریای کلاس های خارج از جعبه گم می شوند. در نهایت، ایجاد رشته ای از کلاس هایی که باید به صورت مشروط اعمال شوند، می تواند بسیار کثیف شود.
ما بر این باوریم که با استفاده از Vanilla-extract’s Sprinkles برای ساختن برخی ابزارها که میتوانند برای تولید و اعمال کلاسهای ابزار از طریق یک API مبتنی بر prop استفاده شوند، میانه خوبی برای این کار پیدا کردهایم.<Card pt={10}>…</Card>
بجای <Card className="pt-10">...</Card>
). این به ما کنترل کامل بر روی API خود را میدهد تا مطمئن شویم که فقط کلاسهایی را معرفی میکنیم که نیاز داریم و تضمین میکند که توسعهدهندگان بهطور تصادفی کلاسی را که وجود ندارد اعمال نمیکنند. همچنین هوشمندی خوبی برای نامها و ارزشهای پایه ارائه میکند.
در سطح بالا، روند ما برای ساختن این است:
- از نشانه های طراحی برای ایجاد یک موضوع استفاده کنید
- از مقادیر موضوع ما برای ایجاد مجموعه ای از کلاس های کاربردی از طریق Sprinkles استفاده کنید
- آن را در یک API مبتنی بر پایه از طریق بسته بندی کنید
Box
جزء
بیایید هر یک از جنبه های این را بررسی کنیم. میتوانید کدی را که در این مقاله در آدرس https://stackblitz.com/edit/typesafe-tailwind مرور میکنیم، با آن بازی کنید و آن را فورک کنید.
ایجاد یک تم
ما توکنهای طراحی خود را از Figma از طریق Tokens Studio برای Figma صادر میکنیم، سپس از آن مقادیر برای ایجاد یک تم با استخراج وانیلی استفاده میکنیم. createTheme
. با این حال، برای این مثال، اجازه دهید با چیزی بسیار ابتدایی شروع کنیم که فقط چند نشانه اولیه را برای رنگهای پسزمینه و متن میگیرد.
import { createTheme } from '@vanilla-extract/css';
import { createTheme } from '@vanilla-extract/css';
export const [themeClass, themeVars] = createTheme({
backgroundColors: {
primary: 'white',
secondary: 'lightgray',
brand: '#744ed4',
good: 'lightgreen',
bad: 'pink',
},
textColors: {
default: 'black',
secondary: 'gray',
white: 'white',
brand: '#744ed4',
good: 'green',
bad: 'red',
},
spaces: {
auto: 'auto',
'0': '0',
'4': '4px',
'8': '8px',
'12': '12px',
'16': '16px',
},
});
این دو چیز به ما می دهد:
- یک رشته کلاس (
themeClass
) که می تواند در جایی نزدیک بالای درخت DOM ما اعمال شود. در نهایت یک رشته است که چیزی شبیه به آن استtheme_themeClass__prfuqw0
. - یک “قرارداد موضوعی” (
themeVars
) که شیئی است که با ساختار شیء ارسال شده مطابقت داردcreateTheme
، اما مقادیر با نام متغیرهای CSS جایگزین می شوند (به عنوان مثالbad: "var(--backgroundColors-bad__prfuqw5)"
.
قرارداد موضوع در واقع می تواند برای ایجاد تم دیگری استفاده شود تا تم جدید با شکل موضوع موجود مطابقت داشته باشد، اما ما آن را برای مقاله دیگری ذخیره می کنیم. در حال حاضر، ما می توانیم به ایجاد کلاس های کاربردی خود ادامه دهیم.
کلاس های کاربردی ما را ایجاد کنید
تم مفید است، و احتمالاً میتوانید ببینید که چگونه میتوانید به سرعت پشتیبانی برای قالببندی اضافه کنید و به لطف createTheme
. این یک روش خوب برای مدیریت توکن های طراحی ما در کد است، اما ما هنوز به تمام کلاس های کاربردی خود نیاز داریم که بتوانند این مقادیر را به عنوان ویژگی های CSS اعمال کنند. اینجاست که Sprinkles وارد می شود 🧁
import { createSprinkles, defineProperties } from '@vanilla-extract/sprinkles';
import { themeVars } from './theme.css';
export const spaceProperties = defineProperties({
properties: {
margin: themeVars.spaces,
marginTop: themeVars.spaces,
marginRight: themeVars.spaces,
marginBottom: themeVars.spaces,
marginLeft: themeVars.spaces,
padding: themeVars.spaces,
paddingTop: themeVars.spaces,
paddingRight: themeVars.spaces,
paddingBottom: themeVars.spaces,
paddingLeft: themeVars.spaces,
},
shorthands: {
m: ['margin'],
mx: ['marginLeft', 'marginRight'],
my: ['marginTop', 'marginBottom'],
p: ['padding'],
px: ['paddingLeft', 'paddingRight'],
py: ['paddingTop', 'paddingBottom'],
},
});
export const colorProperties = defineProperties({
properties: {
backgroundColor: themeVars.backgroundColors,
color: themeVars.textColors,
},
});
export const sprinkles = createSprinkles(spaceProperties, colorProperties);
استفاده كردن defineProperties
و createSprinkles
، ما به تازگی یک را ایجاد کرده ایم sprinkles
ابزاری که می تواند برای تولید یک رشته نام کلاس استفاده شود که می تواند به اجزای ما منتقل شود. مثلا:
sprinkles({ px: 8, backgroundColor: 'good' })
یک رشته نام کلاس ایجاد می کند که می تواند به عنصر DOM شما منتقل شود class
صفت:
sprinkles_paddingLeft_8__m095qc1m sprinkles_paddingRight_8__m095qc1a sprinkles_backgroundColor_good__m095qc1r
اگر می خواهید از آن استفاده کنید، این قبلاً مفید است sprinkles
ابزار به طور مستقیم در یک stylesheet:
// button.css.ts
export const buttonClass = sprinkles({
color: 'brand',
px: '8',
py: '4',
})
// button.tsx
<button className={buttonClass} onClick={...}>
Click Me
</button>
شما همچنین می توانید استفاده کنید sprinkles
در زمان اجرا درست در داخل یک کامپوننت:
// button.tsx
<button
className={sprinkles({
color: 'brand',
px: '8',
py: '4',
})}
onClick={...}
>
Click Me
</button>
این در حال حاضر به ما یک روش خوب و ایمن برای استفاده از تمام کلاس های کاربردی خود بدون نیاز به ایجاد شیوه نامه برای هر مؤلفه می دهد. با این حال، ما دوست نداشتیم مجبور به واردات باشیم sprinkles
در همه جا و هنوز احساس نمی کردم که رابط کاربری فوق العاده بصری است. ما میخواستیم یک API مبتنی بر prop برای دسترسی به کلاسهای ابزار ارائه کنیم.
را Box
جزء
یک الگوی رایج در کتابخانه های مؤلفه مانند MUI و چاکرا ایجاد یک است Box
مؤلفه ای که کلاس های کاربردی شما را می پوشاند و به عنوان بلوک اساسی برای همه اجزای دیگر عمل می کند.
احتمالاً می توانید تصور کنید که چگونه می توانیم وسایل را مستقیماً به دستگاه خود منتقل کنیم sprinkles
عملکرد از یک پایه (مثلا paddingTop="8"
منتقل می شود به sprinkles({ paddingTop: "8" })
. با این حال، اعلام همه این لوازم و نگهداری آنها با دست نسبتاً خسته کننده خواهد بود. خوشبختانه، ما میتوانیم پارامترهای تابع sprinkles خود را بهعنوان نوعی صادر کنیم که میتواند در رابط برای لوازم خود استفاده شود! ما فقط باید خط زیر را در زیر جایی که تعریف می کنیم اضافه کنیم sprinkles
:
export type Sprinkles = Parameters<typeof sprinkles>[0];
با این نوع ما می توانیم رابط اجزای Box خود را ایجاد کنیم.
import { Sprinkles } from './sprinkles.css';
export type BoxProps = React.PropsWithChildren &
Sprinkles &
Omit<React.AllHTMLAttributes<HTMLElement>, 'color'> & {
as?: React.ElementType;
};
در اینجا ما چند نوع را با هم ترکیب میکنیم تا برای تمام ویژگیهایی که احتمالاً میخواهیم به عناصر زیربنایی منتقل کنیم، props ایجاد میکنیم:
-
React.PropsWithChildren
– اساسا فقط به ما می دهدchildren
پشتیبانی -
Sprinkles
– تمام مقادیر props که می تواند به آنها منتقل شود را پوشش می دهدsprinkles
. -
React.AllHTMLAttributes<HTMLElement>
– یک رابط خصوصیت عمومی HTML که تمام ویژگیهای استاندارد DOM را که ممکن است بخواهیم به آنها منتقل کنیم، پوشش میدهدid
،className
،aria-*
و غیره توجه داشته باشید که حذف می کنیمcolor
زیرا ما می خواهیم آن را با کلاس های کاربردی خود کنترل کنیم. - آخرین نوع سفارشی است، اضافه کردن
as
پشتیبانی این به ما امکان می دهد تا کامپوننت را رندر کنیم مانند هر نوع عنصر معتبر به عنوان مثالas="button"
یاas="span"
.
حالا می توانیم کامپوننت را بنویسیم:
export const Box: React.FC<BoxProps> = ({ as = 'div', className, ...props }) => {
const sprinklesProps: Record<string, unknown> = {};
const nativeProps: Record<string, unknown> = {};
Object.entries(props).forEach(([key, value]) => {
if (sprinkles.properties.has(key as keyof Sprinkles)) {
sprinklesProps[key] = value;
} else {
nativeProps[key] = value;
}
});
return React.createElement(as, {
className: clsx([sprinkles(sprinklesProps), className]),
...nativeProps,
});
};
ما روی همه قطعات را تکرار می کنیم و بررسی می کنیم که آیا می توان آنها را به آنها منتقل کرد یا خیر sprinkles
. اگر بتوانند، آنها را در یک شی قرار می دهیم که در نهایت به آن ارسال می شود sprinkles
برای ایجاد رشته کلاس برای کامپوننت. یک عرف هم قبول داریم className
که می تواند کلاس های اضافی را اعمال کند. بقیه پایه ها در یک شی متفاوت قرار می گیرند که به عنوان پایه روی جزء زیرین پخش می شود.
یکی از مواردی که در مثال ما گم شده است، پشتیبانی است ref
. اجازه دهید به سرعت از طریق آن پشتیبانی اضافه کنیم forwardRef
. این به ما فینال می دهد Box
کد جزء:
import React from 'react';
import clsx from 'clsx';
import { Sprinkles, sprinkles } from './sprinkles.css';
export type BoxProps = React.PropsWithChildren &
Sprinkles &
Omit<React.AllHTMLAttributes<HTMLElement>, 'color' | 'height' | 'width'> & {
as?: React.ElementType;
};
export const Box = React.forwardRef<unknown, BoxProps>(
({ as = 'div', className, ...props }, ref) => {
const sprinklesProps: Record<string, unknown> = {};
const nativeProps: Record<string, unknown> = {};
Object.entries(props).forEach(([key, value]) => {
if (sprinkles.properties.has(key as keyof Sprinkles)) {
sprinklesProps[key] = value;
} else {
nativeProps[key] = value;
}
});
return React.createElement(as, {
className: clsx([sprinkles(sprinklesProps), className]),
ref,
...nativeProps,
});
}
);
// Required because of forwardRef
Box.displayName = 'Box';
اضافه کردن سبک های سفارشی
استفاده كردن sprinkles
و ما Box
کامپوننت بیشتر آنچه را که برای ساخت UI در Highlight نیاز داریم به ما می دهد. با این حال، اجتنابناپذیر است که بخواهید در برخی موارد سبکهای سفارشی بنویسید، و عصاره وانیل نیز شما را در آنجا پوشش میدهد.
می توانید استفاده کنید [style](https://vanilla-extract.style/documentation/api/style/)
برای ایجاد یک نام کلاس هش شده جدید که می تواند وارد شود و روی یک جزء اعمال شود، درست مانند آنچه که معمولاً CSS می نویسید، اما با نوع ایمنی و قابلیت استفاده از sprinkles
در کنار سایر ویژگی های CSS سفارشی.
import { style } from '@vanilla-extract/css';
export const button = style([
sprinkles({
px: '4',
py: '8',
}),
{
outline: '2px solid currentColor'
}
])
یا حتی میتوانید برای بازنشانی استایلها یا اعمال قوانین عمومی مانند، استایلها را به صورت جهانی اعمال کنید font-family
در سراسر برنامه شما:
import { globalStyle } from '@vanilla-extract/css';
globalStyle('body', {
fontFamily: 'arial',
margin: 0,
});
استفاده از دستور العمل ها
یکی از متداول ترین کارهایی که ما باید در کتابخانه کامپوننت انجام دهیم، تعریف مجموعه ای از ویژگی ها برای انواع مختلف یک جزء است. برای مثال می خواهیم برای دکمه ها اندازه و رنگ های مختلفی داشته باشیم. عصاره وانیل با استفاده از دستور العمل ها این کار را برای ما آسان می کند.
دستور العمل ها به ما اجازه می دهد تا برخی از سبک های پایه را برای یک جزء و همچنین ترکیبی از سبک ها ارائه دهیم که بسته به نوع مورد استفاده به صورت مشروط اعمال می شوند. در اینجا یک مثال ساده برای a Button
جزء:
import { recipe, RecipeVariants } from '@vanilla-extract/recipes';
import { themeVars } from './theme.css';
export const button = recipe({
base: {
border: 0,
borderRadius: 6,
},
variants: {
kind: {
primary: {
background: themeVars.backgroundColors.brand,
color: themeVars.textColors.white,
},
secondary: {
background: themeVars.backgroundColors.secondary,
},
},
size: {
small: { padding: themeVars.spaces['4'] },
medium: { padding: themeVars.spaces['8'] },
large: { padding: themeVars.spaces['16'] },
},
rounded: {
true: { borderRadius: 999 },
},
},
defaultVariants: {
kind: 'primary',
size: 'medium',
},
});
export type ButtonVariants = RecipeVariants<typeof button>;
حالا می توانیم آن را بگیریم button
دستور غذا و ButtonVariants
برای ایجاد کامپوننت ما تایپ کنید.
import * as React from 'react';
import { Box } from './Box';
import * as styles from './Button.css';
type Props = React.PropsWithChildren &
styles.ButtonVariants & {
onClick: React.ButtonHTMLAttributes<HTMLButtonElement>['onClick'];
};
export const Button: React.FC<Props> = ({ children, onClick, ...rest }) => {
return (
<Box as="button" onClick={onClick} className={styles.button(rest)}>
{children}
</Box>
);
};
همانطور که ما دستور العمل های بیشتری را اضافه می کنیم، آنها به طور خودکار به عنوان لوازم جانبی در کامپوننت پشتیبانی می شوند. استفاده چیزی شبیه به این خواهد بود:
<Button size="small" kind="secondary" onClick={...}>Dismiss</Button>
مبادلات
در حالی که ما به وضوح طرفدار عصاره وانیل هستیم، هنوز هم چیزهایی وجود دارد که دوست نداریم یا احساس می کنیم می توان آنها را در استفاده ما بهبود بخشید. به عنوان مثال، ما کلاس های کاربردی برای مواردی مانند این نداریم hover
یا focus
از پاشیدن از نظر فنی، میتوانیم این موارد را با شرایط اضافه کنیم، اما نمیخواستیم قوانین سبک زیادی ایجاد کنیم که احتمال استفاده از آنها کم است. ما راه خوبی برای پاکسازی کلاسها/سبکهای استفاده نشده پیدا نکردهایم، اما تعداد بسیار کمتری از آنها را نیز تولید میکنیم، زیرا فقط در صورت نیاز آنچه را که نیاز داریم به سیستم اضافه میکنیم.
یکی دیگر از ملاحظات منحنی یادگیری برای عصاره وانیل است. بسیاری از مردم با Tailwind آشنا هستند، اما VE آنقدرها جریان اصلی نیست و به زمان کمی نیاز دارد تا مهندسان به آن عادت کنند. چیزهای بیشتری در زیر هود در حال انجام است و شما باید پیکربندی هایی را برای ابزار ساخت خود نیز اضافه کنید.
نتیجه
ما می دانیم که عصاره وانیل برای همه مناسب نیست، اما اگر در حال ساختن یک سیستم طراحی در TypeScript هستید، فکر می کنیم ارزش دیدن دارد. این به ما کمک کرد تا یک تجربه توسعهدهنده عالی در Highlight ارائه دهیم و به ما کمک کرد تا با سرعت بیشتری در رابط کاربری خود تکرار کنیم.
ما در اینجا فقط سطح عصاره وانیل را خراشیدیم، بنابراین اگر علاقه مند به کسب اطلاعات بیشتر هستید، مستندات را بررسی کنید. ما به اشتراک گذاری در مورد نحوه استفاده از آن برای ساختن سیستم طراحی هایلایت ادامه خواهیم داد و اگر مایل به کاوش بیشتر در استفاده از ما هستید، تمام کدهای ما منبع باز هستند. تمام کدهای مثال های این مقاله نیز برای هر کسی در دسترس است تا بتواند با آن ها بازی کند.
ما دوست داریم در مورد تجربیات شما در ساخت کتابخانه های مؤلفه بشنویم. لطفاً در Discord یا به ما بپیوندید به ما در توییتر ضربه بزنید برای چت بیشتر