التوثيق
Tailwind
تتبع أنماط NSH إطار Tailwind CSS 4. في Rust، تكون أدوات Tailwind المساعدة (utilities) دوال أنماط مُنمَّطة بالأسماء والقيم نفسها (.p_4() هي p-4، و.rounded_2xl() هي rounded-2xl، وZINC.shade(900) هي zinc-900)؛ أما في TSX فيقبل كل عنصر الفئات (classes) نفسها في class، وتُحلّ أصلاً مقابل سمة Tailwind 4 الافتراضية. يعرض كل مثال أدناه الدوال المُنمَّطة أولاً ثم الفئات:
import { mount, Scroll, signal, Text, View } from "nsh";
function App() {
const count = signal(0);
const busy = signal(false);
const tab = signal("Day");
const tabs = ["Day", "Week", "Month"];
const buttons = () => (
<View class="flex-row flex-wrap gap-2">
<View
onPress={() => count.update((n) => n + 1)}
class="rounded-lg bg-lime-300 px-4 py-2 transition-colors duration-150 motion-reduce:transition-none hover:bg-lime-200 active:bg-lime-400 focus-visible:outline-2 focus-visible:outline-sky-500"
>
<Text class="font-medium text-lime-950">Pressed {count()}×</Text>
</View>
<View
onPress={() => busy.set(!busy())}
class="rounded-lg border border-black/10 bg-white px-4 py-2 shadow-sm transition motion-reduce:transition-none hover:bg-zinc-50 active:scale-95 active:bg-zinc-100 dark:border-[#2c2e33] dark:bg-[#24262a] dark:hover:bg-[#2a2c31] dark:active:bg-[#2d2f34] focus-visible:outline-2 focus-visible:outline-sky-500"
>
<Text class="font-medium text-[#15161a] dark:text-[#eceef1]">{busy() ? "Enable" : "Disable"}</Text>
</View>
<View disabled={busy()} onPress={() => count.set(0)} class="rounded-lg bg-emerald-400 px-4 py-2 enabled:hover:bg-emerald-300 active:bg-emerald-500 disabled:opacity-40 focus-visible:outline-2 focus-visible:outline-sky-500">
<Text class="font-medium text-emerald-950">Reset</Text>
</View>
</View>
);
const tabsRow = () => (
<View class="flex-row rounded-xl border border-black/5 bg-[#ecece7] p-1 dark:border-[#1f2124] dark:bg-[#0c0d0f]">
<Each of={tabs}>
{(t) => (
<View
onPress={() => tab.set(t())}
class="flex-1 items-center rounded-lg py-2 transition-colors motion-reduce:transition-none active:opacity-70 focus-visible:outline-2 focus-visible:outline-sky-500"
classList={{ "bg-white shadow-sm dark:bg-[#2a2c31]": tab() === t(), "hover:bg-black/5 dark:hover:bg-white/5": tab() !== t() }}
>
<Text class="text-sm font-medium" classList={{ "text-[#15161a] dark:text-[#eceef1]": tab() === t(), "text-[#565a61] dark:text-[#8d9198]": tab() !== t() }}>
{t()}
</Text>
</View>
)}
</Each>
</View>
);
const gradient = () => (
<View class="overflow-hidden rounded-2xl bg-linear-to-br from-lime-300 via-emerald-300 to-sky-400 p-5 shadow-xl shadow-lime-300/20">
<Text class="text-xl font-bold text-zinc-950">{tab()} view</Text>
<Text class="mt-1 text-sm text-zinc-950/70">bg-linear-to-br from-lime-300 via-emerald-300 to-sky-400</Text>
</View>
);
const grid = () => (
<View class="grid grid-cols-3 gap-2">
<View class="col-span-2 h-14 items-center justify-center rounded-lg bg-sky-950">
<Text class="text-xs text-sky-200">col-span-2</Text>
</View>
<View class="h-14 rounded-lg bg-sky-900" />
<View class="h-14 rounded-lg bg-sky-800" />
<View class="row-span-2 rounded-lg bg-sky-400" />
<View class="h-14 rounded-lg bg-sky-700" />
<View class="h-14 rounded-lg bg-sky-600" />
<View class="h-14 rounded-lg bg-sky-500" />
</View>
);
const list = () => (
<View class="overflow-hidden rounded-xl border border-black/10 bg-white shadow-sm dark:border-[#2c2e33] dark:bg-[#18191c]">
<Each of={["Inbox", "Drafts", "Sent", "Archive"]}>
{(item) => (
<View class="flex-row items-center justify-between border-t border-black/5 px-4 py-3 first:border-t-0 even:bg-[#fafaf7] active:bg-lime-50 dark:border-[#1f2124] dark:even:bg-[#141518] dark:active:bg-[#24262a]">
<Text class="text-[#15161a] dark:text-[#eceef1]">{item()}</Text>
<Text class="text-[#8c9097] dark:text-[#5a5e65]">›</Text>
</View>
)}
</Each>
</View>
);
const motion = () => (
<View class="flex-row items-center justify-around py-2">
<View class="size-8 animate-spin motion-reduce:animate-none rounded-md bg-lime-300" />
<View class="size-8 animate-bounce motion-reduce:animate-none rounded-full bg-sky-400" />
<View class="h-8 w-14 animate-pulse motion-reduce:animate-none rounded-md bg-[#e1e1db] dark:bg-[#2c2e33]" />
</View>
);
return (
<Scroll class="flex-1 bg-[#f3f3ef] dark:bg-[#101113]">
<View class="gap-5 p-5 pt-6">
<Text class="text-2xl font-bold tracking-tight text-[#15161a] dark:text-[#eceef1]">Tailwind classes</Text>
{/* Two columns from `md:` on (768 points): breakpoints follow the window. */}
<View class="gap-5 md:flex-row md:items-start">
<View class="gap-5 md:min-w-0 md:flex-1">
{buttons()}
{tabsRow()}
{gradient()}
</View>
<View class="gap-5 md:min-w-0 md:flex-1">
{grid()}
{list()}
{motion()}
</View>
</View>
</View>
</Scroll>
);
}
mount(App);use nsh_core::element::palette::ZINC;
view()
.flex_row()
.items_center()
.gap_3()
.rounded_2xl()
.bg(AdaptiveColor::new(white(), ZINC.shade(800)))
.p_4()
.shadow_lg()
.active(|s| s.scale(0.98))
.child(
text("Tailwind on native views")
.text_base()
.font_weight(FontWeight::SEMIBOLD)
.text_color(AdaptiveColor::new(ZINC.shade(900), white())),
)<View class="flex-row items-center gap-3 rounded-2xl bg-white p-4 shadow-lg active:scale-[0.98] dark:bg-zinc-800">
<Text class="text-base font-semibold text-zinc-900 dark:text-white">Tailwind on native views</Text>
</View>تُحوَّل الدوال المُنمَّطة إلى قواعد الأنماط نفسها التي تُترجَم إليها الفئات، دون أي نص يلزم تحليله. أما الفئات، فلا يُولَّد أي CSS ولا يُنفَّذ شيء في JavaScript: يُمرَّر النص إلى النواة، التي تحلّل كل سلسلة فئات مختلفة مرة واحدة (وتخزّنها مؤقتًا)، وتحوّلها إلى قواعد أنماط، ثم تطبّقها. أما المتغيّرات المعتمدة على الحالة (الضغط، الوضع الداكن، عرض النافذة) فتعيد النواة تطبيقها عند تغيّر تلك الحالة، وتُتتبَّع حالتا الضغط والتركيز أصلاً، لذا فإن ردّ الفعل البصري لـactive: (.active(..) في Rust) لا يتطلب أي ذهاب وإياب إلى شيفرتك.
تنتج الفئات والخاصية style الأنماط الأصلية نفسها ويمكن الجمع بينهما؛ وتتناول هذه الصفحة ما يخص الفئات ونظائرها المُنمَّطة. ويسرد مرجع Tailwind كل أداة مدعومة.
#class and classList
view()
.rounded_lg()
.px_4()
.py_2()
.transition_colors()
.bg(move || if selected.get() { INDIGO.shade(600) } else { ZINC.shade(200) })
.style_when(busy, |s| s.opacity(0.5))<View
class="rounded-lg px-4 py-2 transition-colors"
classList={{ "bg-indigo-600": selected(), "bg-zinc-200": !selected(), "opacity-50": busy() }}
/>classهي سلسلة من الفئات. ويمكن أن تكون ديناميكية:class={`h-6 rounded bg-${color()}-500`}تعمل، لأن الفئات تُحلّ وقت التشغيل.classListتربط الفئات بشروط: تُطبَّق الفئة ما دامت قيمتها صحيحة (truthy). وتُطبَّق بعدclass.- في Rust، تتبعها الدالة التي تُمرَّر إليها دالة مغلقة (closure) أو إشارة (signal) (
.bg(move || ..))، وتطبّق.style_when(condition, |s| ..)الأنماط ما دام الشرط قائمًا، وتحسم.when(condition, |v| ..)الأمر مرة واحدة عند إنشاء العنصر.
كيف تُحسم التعارضات:
- الفئات اللاحقة تتغلب على السابقة في السلسلة نفسها:
"p-2 p-4"تعنيp-4. ولا حاجة إلىtailwind-merge: يمكن للمكوّن أن يلحق بسلسلتهclassالتي يمرّرها المستدعي. وتتغلب الدوال اللاحقة كذلك في Rust. - فئات المتغيّرات تُطبَّق بعد الفئات العادية، بحسب ترتيب المتغيّرات في Tailwind (
hover:قبلactive:، وmax-*:الأوسع أولاً، ونقاط التوقف تصاعديًا، وdark:أخيرًا)، أيًّا كان موضعها في السلسلة. وكما في Tailwind، يحسم أعلى متغيّر في الفئة، ثم الذي يليه، وتأتي الفئة ذات المتغيّر الإضافي لاحقًا:dark:hover:bg-a dark:bg-bتعنيbg-aعند التحويم في الوضع الداكن، وmax-md:p-2 max-lg:p-4تعنيp-2دونmd. وكما في CSS، تمنحodd:bg-a last:bg-bالصف الأخير الفرديbg-a، لأن Tailwind يرتّبoddبعدlast. styleتتغلب على الفئات. مفتاح النمط هو خاصية CSS التي تحمل اسمه، وهي التي تضبطها الفئات، لذا فهو يستبدل تلك الخاصية وحدها:style={{ paddingLeft: 8 }}تستبدلpl-4وتُبقيpr-4.!تجعل الفئة مهمّة (important):!p-0.
تقبل مكوّنات Rust (مكوّنات nsh/base وتلك الموجودة في حزمة (crate) تطبيقك) class وclassList من TSX أيضًا؛ وتُدمج فئاتك بعد أنماط المكوّن نفسه وتتغلب عند تساوي الرتبة.
#Variants
| المتغيّر | يُطبَّق عندما | Rust |
|---|---|---|
active: | يكون العرض (view) مضغوطًا | .active(|s| ..) |
hover: | يكون مؤشر فوق العرض (مؤشرات iPad وMac) | .hover(|s| ..) |
focus:، focus-visible: | يكون التركيز على حقل إدخال | .focus(|s| ..) |
focus-within: | يكون التركيز على العرض أو أحد أبنائه (محتوى Overlay هو من أبناء العرض الموجود فيه) | .focus_within(..) |
disabled:، enabled: | تكون خاصية disabled للعنصر true / false | .disabled_style(|s| ..) |
selected: | يكون العنصر محدّدًا (selected، accessibilityState.selected الخاصة بـButton) | .selected_style(..) |
checked: | يكون العنصر مؤشَّرًا عليه (accessibilityState.checked هي true؛ "mixed" غير مؤشَّر عليه) | .checked_style(..) |
indeterminate: | يكون العنصر في حالة غير محسومة (accessibilityState.checked هي "mixed") | .indeterminate_style(..) |
aria-expanded: | يكون العنصر موسَّعًا (accessibilityState.expanded هي true): زرّ التشغيل الخاص بـPopover أو Menu ما دامت نافذته المنبثقة مفتوحة | .expanded_style(..) |
aria-pressed: | يكون العنصر زرّ تبديل في وضع التشغيل (accessibilityState.pressed هي true؛ "mixed" غير مضغوط). ليس active:، وهو ضغط قيد التنفيذ | .aria_pressed(..)، .pressed_style(..) |
aria-busy:، aria-checked:، aria-selected: | accessibilityState.busy، checked (true، وليس "mixed") أو selected هي true، كما في متغيّرات aria-* في Tailwind: بعد الفئات الزائفة (pseudo-classes)، بترتيب أسمائها | |
data-<name>: | يحمل العنصر السمة data-* بالقيمة name، كما في متغيّرات data-* المنطقية في Tailwind (&[data-range-start]): ما يُظهره جزء من المكوّن زيادةً على حالاته، مثل يوم في التقويم عند بداية نطاق (data-range-start:). بعد متغيّرات aria-*، بترتيب أسمائها | Interactive::project_data(name, ..) تضبط واحدة على جزء من مكوّن |
group-active:، group-hover:، group-focus:، group-focus-within:، group-disabled:، group-selected:، group-checked:، group-indeterminate:، group-aria-expanded:، group-aria-pressed:، group-aria-busy:، group-aria-checked:، group-aria-selected:، group-data-<name>: | يكون أقرب سلف يحمل الفئة group في تلك الحالة، أو يحمل تلك السمة data-* | .group() على السلف؛ و.group_active(..)، .group_hover(..)، .group_focus_within(..) في الداخل |
group-active/name: … | الأمر نفسه، لسلف موسوم بـgroup/name | .group_named("name")؛ .group_active_named("name", ..)، … |
first:، last:، odd:، even:، only: | موضع العنصر نفسه بين أشقائه. لتلوين نص الصف بحسب موضع الصف، ضع last:text-* على الصف: فيرثها النص | Rust عادي انطلاقًا من فهرس الصف (at.is_first()، at.is_last()، at.index() % 2) |
placeholder: | ينسّق النص النائب (placeholder) لحقل إدخال (placeholder:text-zinc-400) | .placeholder_color(..) |
selection: | يلوّن تمييز التحديد في حقل إدخال أو نص قابل للتحديد، وفي تلك الموجودة داخل العنصر (selection:bg-violet-200) | .selection_color(..) |
dark: | يكون مظهر النظام داكنًا | AdaptiveColor، أو .dark(|s| ..) |
sm:، md:، lg:، xl:، 2xl: | يبلغ عرض النافذة 640 أو 768 أو 1024 أو 1280 أو 1536 نقطة على الأقل | .style_when(|| window().width >= px(768.0), |s| ..) |
max-sm:، max-md:، … | تكون النافذة أضيق من نقطة التوقف | الأمر نفسه، مع < |
min-[600px]:، max-[600px]: | نقاط توقف اعتباطية | الأمر نفسه |
portrait:، landscape: | اتجاه النافذة (orientation) | window().width > window().height |
ios:، android:، web: | المنصة | .when(os() == Os::Ios, |v| ..) |
rtl:، ltr: | اتجاه التخطيط | .direction(LayoutDirection::..) على عرض |
motion-safe:، motion-reduce: | إعداد المستخدم لتقليل الحركة | reduce_motion() |
تُدمج المتغيّرات: dark:active:bg-zinc-700، md:landscape:flex-row. في Rust، يكون AdaptiveColor داخل نمط .active(..) هو dark:active:.
view()
.group()
.flex_row()
.items_center()
.gap_3()
.rounded_xl()
.bg(white())
.p_3()
.active(|s| s.bg(ZINC.shade(50)))
.child(text("Settings").flex_1().text_color(ZINC.shade(900)))
.child(
text("›")
.text_color(ZINC.shade(300))
.transition_transform()
.group_active(|s| s.translate_x(px(4.0))),
)<View class="group flex-row items-center gap-3 rounded-xl bg-white p-3 active:bg-zinc-50">
<Text class="flex-1 text-zinc-900">Settings</Text>
<Text class="text-zinc-300 transition-transform group-active:translate-x-1">›</Text>
</View>disabled على أي عنصر (.disabled(..) في Rust) تطبّق فئات disabled: (.disabled_style(..)) وتزيل معالجات الضغط من المحتوى المركَّب، فلا يُظهر أي ردّ فعل بصري عند الضغط أيضًا:
view()
.disabled(saving)
.on_press(save)
.rounded_lg()
.bg(EMERALD.shade(500))
.px_4()
.py_2()
.active(|s| s.bg(EMERALD.shade(600)))
.disabled_style(|s| s.opacity(0.4))
.child(text("Save").font_weight(FontWeight::MEDIUM).text_color(white()))<View disabled={saving()} onPress={save} class="rounded-lg bg-emerald-500 px-4 py-2 active:bg-emerald-600 disabled:opacity-40">
<Text class="font-medium text-white">Save</Text>
</View>تُقارَن نقاط التوقف بالنافذة (السطح الذي يرسم عليه NSH)، لا بالجهاز: ففي وضع تقسيم الشاشة على iPad أو في نافذة قابلة لتغيير الحجم، تتبع عرض النافذة، كما تفعل window().
#What differs from Tailwind on the web
- الوحدات هي النقاط.
p-4(.p_4()) تساوي 16 نقطة (خطوة تباعد واحدة = 4 نقاط، و1rem = 16 نقطة). والقيم الاعتباطية فيpxبالنقاط أيضًا:p-[13px](.p(px(13.0))) تساوي 13 نقطة. flexهو صف، كما في CSS:class="flex"يرتّب أبناءه في صف ما لم تقل فئة اتجاه غير ذلك (في أي موضع من السلسلة:flex-col flexهو عمود). وكذلكdisplay: "flex"في النمط و.flex()في Rust صفّان بالطريقة نفسها، أما العرض الذي لا يضبط display فيحتفظ بالعمود الافتراضي في NSH.blockوflow-rootتستخدمان تخطيط الكتلة (block)، وكذلكinlineوinline-block، كما يرتّبها المتصفح داخل أب من نوع flex أو grid. وفي Rust، تضبط.flex_row()و.flex_col()الاتجاه.- فئات النص على العروض تُورَّث.
text-*وfont-*وleading-*وtracking-*وغيرها من أدوات النص على عرض (.text_color(..)،.font_weight(..)، … في Rust) تنتقل إلى عناصر النص والإدخال داخله، كما يفعل الوراثة في CSS:<View class="text-white">تجعل نصوصه بيضاء. - كل عرض صندوق مستقل.
overflow-x-hiddenوoverflow-y-hiddenتقصّان المحورين معًا، كما تفعلoverflow-hidden(التمرير من اختصاص عنصر scroll)؛ وinvisibleتخفي العرض وكل ما فيه؛ وmix-blend-*تمزج العرض بكل ما رُسم خلفه؛ وصناديقblockتُبقي هوامشها وهوامش أبنائها منفصلة؛ والعرضabsoluteيوضع في أقرب سلف مُموضَع، كما في CSS؛ وfixedيرتّب العرض في عرضه الجذر (تُوضع العناصر الأخرى كما فيabsolute). - النص يحتفظ بمسافاته البيضاء. تُرسم المسافات وفواصل الأسطر في النص كما كُتبت، وتضبط
whitespace-*التفاف الأسطر؛ ويطوي مترجم JSX المسافات البيضاء في نص JSX الحرفي، كما يفعل JSX. - الانتقالات تحرّك مجموعات خصائص:
transitionتحرّك الألوان والشفافية والظلال والتحويلات، وtransition-allتحرّك أيضًا أنصاف أقطار الزوايا وعروض الحدود؛ أما المرشّحات (filters) ومرشّحات الخلفية والتدرجات والتخطيط فتأخذ قيمها الجديدة دفعة واحدة. focus-visible:هيfocus:: تُطبَّق كلما كان التركيز على العنصر.- خلفيات الحالة تظهر فوق التدرجات. في CSS يغطي التدرج
background-color، لذا لن يُرىactive:bg-*على سطحbg-linear-*أبدًا؛ أما في NSH فإن لون خلفية منactive:أوhover:أوfocus:أوgroup-*:يحلّ محل التدرج ما دامت تلك الحالة قائمة (ما لم تضبط الحالة تدرجًا خاصًا بها، مثلactive:from-*). وكذلكactiveStyleوسائر أنماط الحالة، وألوان الخلفية المتحركة. - إعدادات النص واللمس مفاتيح أنماط:
line-clamp-*تضبطnumberOfLines، وtext-ellipsisوtext-clipتضبطانtextOverflow، وtruncateتضبطهما معًا، وselect-*تضبطuserSelectوpointer-events-*تضبطpointerEvents، لذا فهي تعمل في متغيّرات الحالة وتخسر أمامstyleالمضمَّنة كما تخسر كل فئة؛ وcaret-*وplaceholder:text-*وselection:bg-*تضبط أنماطcaretColorوplaceholderColorوselectionColorبالطريقة نفسها. وفي Rust هذه دوالStyled(.number_of_lines(..)،.truncate()،.text_overflow(..)،.select_text()،.pointer_events(..)، …).whitespace-nowrapتُبقي النص في سطر واحد كفئة. وobject-*تضبط نمطيobjectFitوobjectPositionللصور ومقاطع الفيديو، وتستبدلهما مفاتيح النمط كما تستبدل أي مفتاح آخر. - المجموعة المدعومة: أدوات ومتغيّرات Tailwind 4 التي لها معنى أصلي. متغيّرات
peer-*والعناصر الزائفة (pseudo-elements) (before:،after:) والخصائص الاعتباطية ([mask-type:luminance]) يُبلَّغ عنها كفئات غير معروفة، وكذلك كل أداة لا معنى أصليًا لها في NSH، حتى لا تعني الفئة شيئًا مختلفًا عمّا تعنيه في المتصفح. أما تلميحات التصيير (antialiased،will-change-transform،appearance-none،box-border، …) فتُقبل ولا تغيّر شيئًا.
وكل ما عدا ذلك يعمل كما يوثّقه Tailwind 4: لوحة الألوان، ومعدّلات الشفافية (bg-black/40)، والقيم الاعتباطية (bg-[#34c759]، w-[72px]، grid-cols-[1fr_2fr]؛ وعلى p وpx وpy وpt وpr وpb وpl وأدوات m أيضًا متغيّرات البيئة، pt-[env(safe-area-inset-top)]، pb-[calc(env(keyboard-inset-height)+49px)]، انظر المنطقة الآمنة ولوحة المفاتيح)، والكسور (w-1/2، translate-x-1/2)، والظلال والحلقات، والتدرجات (bg-linear-to-r، bg-radial، bg-conic مع from- / via- / to-)، والمرشّحات، وbackdrop-blur-*، وmix-blend-*، والتحويلات، وtransition-* وduration-*، وanimate-spin / ping / pulse / bounce، والشبكة (grid)، ولكل منها دالتها المُنمَّطة في Rust. وتنطبق قيود المنصات الخاصة بـالمرشّحات والمزج على الفئات أيضًا.
#Theme
تُحلّ الفئات مقابل سمة Tailwind 4 الافتراضية. وسّعها في nsh.json تحت "tailwind"؛ تُضاف مدخلاتك إلى الافتراضيات (أو تستبدل الافتراضي الذي يحمل الاسم نفسه):
{
"tailwind": {
"colors": {
"brand": { "50": "#f5f3ff", "500": "#7c3aed", "600": "#6d28d9", "DEFAULT": "#7c3aed" },
"ink": "#1f2328"
},
"radius": { "card": 14 },
"fontFamily": { "display": "Pacifico" },
"breakpoints": { "tablet": 600 }
}
}
مع هذه السمة، تكون bg-brand-500 وtext-brand (درجة DEFAULT) وborder-ink وrounded-card وfont-display وtablet:flex-row فئات صالحة.
| المفتاح | القيم | ينتج |
|---|---|---|
colors | لون CSS، أو كائن من الدرجات ("DEFAULT" للاسم المجرد) | bg-*، text-*، border-*، from-*، ring-*، … |
spacing | نقاط لكل خطوة تباعد (الافتراضي 4) | p-*، m-*، gap-*، w-*، … |
rem | نقاط لكل rem في القيم الاعتباطية (الافتراضي 16) | |
breakpoints | نقاط | متغيّرات name: |
container | نقاط | أحجام max-w-* |
fontSize | نقاط، أو [size, lineHeight] مع ارتفاع سطر يساوي مضاعفًا للحجم | text-* |
fontWeight | 1–1000 | font-* |
fontFamily | اسم عائلة خطوط (عائلة نظام أو اسم خط مستورد) | font-* |
tracking | em | tracking-* |
leading | مضاعفات حجم الخط | leading-* |
radius | نقاط | rounded-* |
shadow، insetShadow، dropShadow | قيمة CSS من نوع box-shadow | shadow-*، inset-shadow-*، drop-shadow-* |
blur | نقاط | blur-*، backdrop-blur-* |
perspective | نقاط | perspective-* |
aspect | نسبة (1.5) | aspect-* |
ease | [x1, y1, x2, y2] | ease-* |
المفاتيح غير المعروفة والقيم غير الصالحة هي أخطاء بناء (nsh.json: tailwind: unknown theme key 'colours'). وتُترجَم السمة ضمن الحزمة وتُطبَّق قبل الإطار الأول.
تشكّل سمة Tailwind فئاتك. أما الدوال المُنمَّطة فتحتفظ بمقاييس Tailwind الافتراضية، لذا يبدو مكوّن Rust متماثلًا في كل تطبيق، وتأخذ مظهر التطبيق من رموز التصميم (design tokens) في سمته، "theme" في nsh.json (Rust).
#رموز السمة
تحتوي سمة التطبيق ("theme" في nsh.json) على رموز التصميم الخاصة به: ألوان shadcn/ui (background، foreground، primary، muted-foreground، border، ring، chart-1، sidebar، …)، وradius، وخطوطه font-sans وfont-mono، وأي رمز يضيفه التطبيق. تسمّي الأصناف هذه الرموز بأسماء Tailwind كما تسمّي متغيرات @theme في CSS، وتقرؤها مكونات Rust باستخدام theme() من nsh_base، ويقرؤها TSX باستخدام token() من nsh، فتتبع شاشة TSX ومكونات Rust السمة نفسها:
use nsh_base::theme;
view()
.rounded(move || theme().radius)
.border_1()
.border_color(move || theme().border)
.bg(move || theme().card)
.p_4()
.child(text("Balance").text_color(move || theme().card_foreground))
.child(text("Updated just now").text_color(move || theme().muted_foreground))
.child(
view()
.rounded(move || theme().radius)
.bg(move || theme().primary)
.px_4()
.py_2()
.active(|s| s.bg(move || theme().primary.opacity(0.9)))
.child(text("Continue").text_color(move || theme().primary_foreground)),
)<View class="rounded-(--radius) border border-border bg-card p-4">
<Text class="text-card-foreground">Balance</Text>
<Text class="text-muted-foreground">Updated just now</Text>
<View class="rounded-(--radius) bg-primary px-4 py-2 active:bg-primary/90">
<Text class="text-primary-foreground">Continue</Text>
</View>
</View>اسم الرمز هو اسم متغير CSS الخاص به دون --، كما يكتبه ملف السمة: muted-foreground، radius، chart-1. في Rust، رموز shadcn هي دوال في theme() (primary()، muted_foreground()، radius()، font_mono())، وكل رمز هو theme().color("up") أو theme().number("radius-card") أو theme().font("font-display").
| الصيغة | ما تأخذه | مثال |
|---|---|---|
| اسم لون في أي أداة لونية | لون من السمة، مع إمكانية تحديد الشفافية | bg-primary، text-muted-foreground/70، bg-up/30، border-t-border، ring-ring، from-accent، placeholder:text-muted-foreground |
(--name)، اختصار المتغيرات في Tailwind 4 | لون من السمة أو رقم | bg-(--primary)، rounded-(--radius)، p-(--gap-card) |
(color:--name)، (length:--name)، (number:--name)، (family-name:--name) | الشيء نفسه، لكن بنوع محدد حيث تقبل الأداة عدة أنواع | text-(color:--up)، text-(length:--text-title)، font-(family-name:--font-mono) |
font-sans، font-mono | خطوط السمة، إن حددتها | font-mono |
- رموزك الخاصة. كل اسم في ملف السمة هو رمز، كما أن كل متغير في
@themeفي CSS كذلك:"up": "#16a34a"ينشئbg-upوtext-upوbg-up/30؛ و"radius-card": "14px"ينشئrounded-(--radius-card)؛ و"font-display": "Pacifico"ينشئfont-(family-name:--font-display). الأطوال بالنقاط (points)؛ و(number:--name)يمرّر الرقم كما هو. - الخطوط.
font-sansهو خط التطبيق: كل نص لا يحدد عائلة خط خاصة به يُرسم به، كما تُرسم الصفحة بالخط--font-sansفي Tailwind.font-monoهو خطfont-monoوالشيفرة فيTextView. إن لم يُحدَّدا، فكلاهما من خطوط النظام. - المظهران معًا. للرمز قيمة فاتحة وقيمة داكنة في السمة:
bg-primaryوtheme().primaryوtoken("primary")تتبدل مع المظهر تلقائيًا، دون الحاجة إلى الصنفdark:. اللون الذي تحدده السمة في مظهر واحد فقط يُستخدم في المظهرين. - ألوان Tailwind الخاصة بك لها الأولوية. الاسم في
"tailwind"colorsأو في لوحة ألوان Tailwind هو ذلك اللون، لذا يكونbg-primaryرمزًا من السمة عندما لا يحتوي سمة Tailwind على اللونprimary. - يمكن أن تتغير السمة أثناء تشغيل التطبيق. عندما يُعطى تطبيق مضمّن في تطبيق أصلي سمةَ ذلك التطبيق، وكلما تغيّرت السمة، تُحدَّث الأصناف التي تسمّي الرموز، وروابط Rust التي تقرأ
theme()، وشيفرة TSX التي تقرأtoken()، علمًا بأن الأصناف والروابط تُحدَّث بشكل أصلي. الرموز التي لا توجد في سمة التطبيق المضيف تحتفظ بقيم"theme"الخاصة بالتطبيق (ثم قيم السمة الافتراضية). المضيف الذي يضيف علامته التجارية فقط يعطي تجاوزات للسمة، وهي الرموز التي تغيّرها علامته التجارية بصيغة الملف نفسها (iOS، Android، وAppView::set_theme_overridesعلى أنظمة سطح المكتب). - تُفحص كبقية الأصناف. الاسم الذي ليس رمزًا في السمة الافتراضية ولا في
"theme"الخاص بالتطبيق هو صنف غير معروف عند البناء (unknown class 'bg-chart-up'). لاستخدام رمز يوفره التطبيق المضيف فقط، أعطِه قيمة في"theme"الخاص بالتطبيق أيضًا: ستظهر هذه القيمة عندما يعمل التطبيق بمفرده.
تُحَلّ الأصناف التي تسمّي الرموز مرة واحدة لكل سمة، مثل أي صنف آخر: ورسمها يكلّف ما يكلّفه لون من اللوحة.
#token() في TSX
token(name) من nsh هو قيمة الرمز في المظهر المعروض: اللون على هيئة #rrggbbaa، والطول كعدد من النقاط، والخط كاسم عائلته. وهو تفاعلي (reactive)، فالشيفرة التي تقرؤه داخل مكوّن أو memo أو effect تُعاد عند تغيّر السمة أو المظهر، وهذا مناسب للقيم التي تذهب إلى أماكن لا يصلها صنف، مثل canvas أو مخطط بياني:
import { Canvas, token } from "nsh";
<Canvas
style={{ height: 160 }}
draw={(cx, { width, height }) => {
cx.beginPath();
cx.moveTo(0, height);
cx.lineTo(width, 0);
cx.strokeStyle = token("chart-1");
cx.lineWidth = 2;
cx.stroke();
}}
/>
الأسماء محددة الأنواع: يكتب NSH رموز التطبيق في .nsh/types/nsh/tokens.d.ts انطلاقًا من سمته، فيكون token("up") من النوع string ما دام "up" موجودًا في السمة، والاسم غير الموجود في السمة يُعدّ خطأ في النوع.
#فحص الأصناف
nsh build وnsh dev وnsh run وnsh test تفحص أسماء الأصناف الثابتة في JSX (السلاسل النصية الحرفية في class ومفاتيح classList) باستخدام المحلل والسمة نفسيهما اللذين يستخدمهما وقت التشغيل، وتحذّر من الأصناف غير الموجودة:
warn nsh::build App.tsx:12:36: unknown class `bg-brand-550`
التحذيرات لا تفشل البناء. أما الأصناف التي تُبنى وقت التشغيل (bg-${color()}-500) فتُفحص عندما يراها وقت التشغيل لأول مرة، ويسجّل كل صنف غير معروف مرة واحدة (unknown classes: ...). الدوال محددة الأنواع يفحصها مترجم Rust.
#الأصناف أم الأنماط؟
كلاهما يُحَلّ بشكل أصلي وبكلفة متقاربة. الأصناف أقصر وتمنحك المتغيرات (حالات الضغط، والوضع الداكن، ونقاط التوقف) دون كتابة شيفرة. أما الأنماط المضمنة فتقبل القيم المحسوبة في JavaScript وتدعم القيم المتحركة الأصلية (opacity: fade، translate: [x, 0]). تستخدم معظم تطبيقات TSX الأصناف للمظهر الثابت وstyle لما هو محسوب أو متحرك. أما مكونات Rust فتستخدم الدوال محددة الأنواع للحالتين، وتربط القيم المتحركة بـ.bind_opacity(..) و.bind_translate_x(..) وبقية الروابط.
#مواضيع ذات صلة
- التنسيق: خاصية style، والدوال محددة الأنواع، وحدود المنصات في التأثيرات.
- مرجع Tailwind: كل الأدوات والمتغيرات.
- التخطيط: flexbox وgrid، وهما متاحان أيضًا كأصناف.