Dokumentation
Tailwind
NSHs Styles folgen Tailwind CSS 4. In Rust sind die Utilities von Tailwind typisierte Style-Methoden mit denselben Namen und Werten (.p_4() ist p-4, .rounded_2xl() ist rounded-2xl, ZINC.shade(900) ist zinc-900). In TSX akzeptiert jedes Element die Klassen selbst in class, nativ aufgelöst gegen das Standard-Theme von Tailwind 4. Jedes Beispiel unten zeigt zuerst die typisierten Methoden und danach die Klassen:
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>Typisierte Methoden werden in dieselben Style-Regeln übersetzt, zu denen auch die Klassen kompiliert werden, ohne dass ein String geparst werden muss. Bei Klassen wird kein CSS erzeugt und nichts läuft in JavaScript: Der String wird an den Core übergeben, der jeden verschiedenen Klassen-String einmal parst (und cacht), in Style-Regeln umwandelt und anwendet. Varianten, die vom Zustand abhängen (pressed, Dark Mode, Fensterbreite), wendet der Core erneut an, wenn sich dieser Zustand ändert. Pressed- und Focused-Zustand werden nativ verfolgt, sodass das active:-Feedback (.active(..) in Rust) keinen Round Trip zu deinem Code benötigt.
Klassen und die style-Prop erzeugen dieselben nativen Styles und lassen sich kombinieren. Diese Seite behandelt, was für Klassen und ihre typisierten Entsprechungen spezifisch ist. Die Tailwind-Referenz listet jedes unterstützte Utility auf.
#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() }}
/>classist ein String aus Klassen. Er kann dynamisch sein:class={`h-6 rounded bg-${color()}-500`}funktioniert, weil Klassen zur Laufzeit aufgelöst werden.classListordnet Klassen Bedingungen zu: Eine Klasse gilt, solange ihr Wert truthy ist. Sie wird nachclassangewendet.- In Rust folgt eine Methode, der eine Closure oder ein Signal übergeben wird, diesem (
.bg(move || ..))..style_when(condition, |s| ..)wendet Styles an, solange eine Bedingung gilt, und.when(condition, |v| ..)entscheidet einmal, wenn das Element erzeugt wird.
So werden Konflikte aufgelöst:
- Spätere Klassen gewinnen gegenüber früheren im selben String:
"p-2 p-4"istp-4.tailwind-mergeist nicht nötig: Eine Komponente kann dasclassanhängen, das ihr Aufrufer übergibt. Auch in Rust gewinnen spätere Methoden. - Variantenklassen werden nach einfachen Klassen angewendet, geordnet nach der Variantenreihenfolge von Tailwind (
hover:voractive:,max-*:am weitesten zuerst, Breakpoints aufsteigend,dark:zuletzt), unabhängig von ihrer Position im String. Wie in Tailwind entscheidet die höchste Variante einer Klasse, dann ihre nächsthöhere, und eine Klasse mit einer Variante mehr kommt später:dark:hover:bg-a dark:bg-bistbg-abei Hover im Dark Mode, undmax-md:p-2 max-lg:p-4istp-2unterhalb vonmd. Wie in CSS gibtodd:bg-a last:bg-beiner ungeraden letzten Zeilebg-a, weil Tailwindoddnachlastsortiert. stylegewinnt gegenüber Klassen. Ein Style-Schlüssel ist die CSS-Eigenschaft seines Namens, also dieselbe, die auch die Klassen setzen, und ersetzt deshalb nur diese Eigenschaft:style={{ paddingLeft: 8 }}ersetztpl-4und behältpr-4.!macht eine Klasse important:!p-0.
Rust-Komponenten (die von nsh/base und die aus dem eigenen Crate einer App) akzeptieren class und classList auch aus TSX. Deine Klassen werden nach den eigenen Styles der Komponente zusammengeführt und gewinnen bei gleichem Rang.
#Varianten
| Variante | Gilt, wenn | Rust |
|---|---|---|
active: |
Die View gedrückt wird | .active(|s| ..) |
hover: |
Ein Zeiger über der View ist (Zeiger von iPad und Mac) | .hover(|s| ..) |
focus:, focus-visible: |
Ein Input den Fokus hat | .focus(|s| ..) |
focus-within: |
Die View oder ein Nachfahre den Fokus hat (der Inhalt eines Overlay ist ein Nachfahre der View, in der er sich befindet) |
.focus_within(..) |
disabled:, enabled: |
Die disabled-Prop des Elements true / false ist |
.disabled_style(|s| ..) |
selected: |
Das Element ausgewählt ist (selected, accessibilityState.selected eines Button) |
.selected_style(..) |
checked: |
Das Element angehakt ist (accessibilityState.checked ist true; "mixed" ist nicht angehakt) |
.checked_style(..) |
indeterminate: |
Das Element unbestimmt ist (accessibilityState.checked ist "mixed") |
.indeterminate_style(..) |
aria-expanded: |
Das Element aufgeklappt ist (accessibilityState.expanded ist true): der Trigger eines Popover oder Menu, solange sein Popup geöffnet ist |
.expanded_style(..) |
aria-pressed: |
Das Element ein Toggle-Button ist, der eingeschaltet ist (accessibilityState.pressed ist true; "mixed" ist nicht pressed). Nicht active:, ein laufender Druck |
.aria_pressed(..), .pressed_style(..) |
aria-busy:, aria-checked:, aria-selected: |
accessibilityState.busy, checked (true, nicht "mixed") oder selected ist true, wie die aria-*-Varianten von Tailwind: nach den Pseudoklassen, in der Reihenfolge ihrer Namen |
|
data-<name>: |
Das Element das data-*-Attribut name hat, wie die booleschen data-*-Varianten von Tailwind (&[data-range-start]): was ein Part einer Komponente über seine Zustände hinaus zeigt, etwa der Tag eines Kalenders am Anfang eines Bereichs (data-range-start:). Nach den aria-*-Varianten, in der Reihenfolge ihrer Namen |
Interactive::project_data(name, ..) setzt eines auf einem Part einer Komponente |
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>: |
Der nächste Vorfahre mit der Klasse group in diesem Zustand ist oder dieses data-*-Attribut hat |
.group() am Vorfahren; .group_active(..), .group_hover(..), .group_focus_within(..) innen |
group-active/name: … |
Dasselbe für einen Vorfahren, der mit group/name markiert ist |
.group_named("name"); .group_active_named("name", ..), … |
first:, last:, odd:, even:, only: |
Die eigene Position des Elements unter seinen Geschwistern. Um das Label einer Zeile nach der Position der Zeile einzufärben, setze last:text-* auf die Zeile: Das Label erbt es |
Einfaches Rust anhand des Index der Zeile (at.is_first(), at.is_last(), at.index() % 2) |
placeholder: |
Gestaltet den Platzhalter eines Inputs (placeholder:text-zinc-400) |
.placeholder_color(..) |
selection: |
Färbt die Auswahlhervorhebung eines Inputs oder eines auswählbaren Texts sowie die der Inputs und Texte innerhalb des Elements (selection:bg-violet-200) |
.selection_color(..) |
dark: |
Die Systemdarstellung dunkel ist | Ein AdaptiveColor oder .dark(|s| ..) |
sm:, md:, lg:, xl:, 2xl: |
Das Fenster mindestens 640, 768, 1024, 1280, 1536 Punkte breit ist | .style_when(|| window().width >= px(768.0), |s| ..) |
max-sm:, max-md:, … |
Das Fenster schmaler als der Breakpoint ist | Dasselbe, mit < |
min-[600px]:, max-[600px]: |
Beliebige Breakpoints | Dasselbe |
portrait:, landscape: |
Die Ausrichtung des Fensters | window().width > window().height |
ios:, android:, web: |
Die Plattform | .when(os() == Os::Ios, |v| ..) |
rtl:, ltr: |
Die Layoutrichtung | .direction(LayoutDirection::..) auf einer View |
motion-safe:, motion-reduce: |
Die Einstellung „Bewegung reduzieren“ des Nutzers | reduce_motion() |
Varianten lassen sich kombinieren: dark:active:bg-zinc-700, md:landscape:flex-row. In Rust ist ein AdaptiveColor in einem .active(..)-Style 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 auf einem beliebigen Element (.disabled(..) in Rust) wendet disabled:-Klassen an (.disabled_style(..)) und entfernt seine Press-Handler aus dem, was gemountet ist, sodass es auch kein Press-Feedback zeigt:
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>Breakpoints werden mit dem Fenster verglichen (der Fläche, in die NSH rendert), nicht mit dem Gerät: In einer iPad-Split-View oder einem in der Größe veränderbaren Fenster folgen sie der Fensterbreite, wie es window() tut.
#Was sich von Tailwind im Web unterscheidet
- Einheiten sind Punkte.
p-4(.p_4()) sind 16 Punkte (1 Spacing-Schritt = 4 pt, 1rem = 16 pt). Beliebige Werte inpxsind ebenfalls Punkte:p-[13px](.p(px(13.0))) sind 13 pt. flexist eine Zeile, wie in CSS:class="flex"ordnet seine Kinder in einer Zeile an, sofern keine Richtungsklasse etwas anderes vorgibt (an beliebiger Stelle im String:flex-col flexist eine Spalte). Dasdisplay: "flex"eines Styles und.flex()in Rust sind auf dieselbe Weise Zeilen, und eine View, die kein display setzt, behält NSHs Standard-Spalte.blockundflow-rootverwenden Block-Layout, ebensoinlineundinline-block, so wie ein Browser sie in einem Flex- oder Grid-Parent layoutet. In Rust legen.flex_row()und.flex_col()die Richtung fest.- Textklassen auf Views werden vererbt.
text-*,font-*,leading-*,tracking-*und die anderen Text-Utilities auf einer View (.text_color(..),.font_weight(..), … in Rust) werden an die Text- und Input-Elemente darin weitergegeben, wie es die CSS-Vererbung tut:<View class="text-white">macht seine Labels weiß. - Jede View ist eine eigene Box.
overflow-x-hiddenundoverflow-y-hiddenbeschneiden beide Achsen, wie esoverflow-hiddentut (Scrollen ist Sache des scroll-Elements).invisibleblendet die View und alles darin aus.mix-blend-*vermischt eine View mit allem, was dahinter gezeichnet ist.block-Boxen halten ihre eigenen Margins und die ihrer Kinder getrennt. Eineabsolute-View wird in ihrem nächsten positionierten Vorfahren platziert, wie in CSS.fixedlayoutet eine View in ihrer Root-View (andere Elemente werden wieabsoluteplatziert). - Text behält seine Leerraumzeichen. Leerzeichen und Zeilenumbrüche in einem String werden so gezeichnet, wie sie geschrieben sind, und
whitespace-*legt den Umbruch fest. Der JSX-Compiler reduziert den Leerraum von literalem JSX-Text, wie es JSX tut. - Transitions animieren Eigenschaftsgruppen:
transitionanimiert Farben, Opacity, Schatten und Transforms,transition-allzusätzlich Eckenradien und Rahmenbreiten. Filter, Backdrop-Filter, Verläufe und Layout übernehmen ihre neuen Werte sofort. focus-visible:istfocus:: Es gilt, sobald das Element den Fokus hat.- State-Hintergründe werden über Verläufen angezeigt. In CSS bedeckt ein Verlauf
background-color, sodassactive:bg-*auf einerbg-linear-*-Fläche nie zu sehen wäre. In NSH ersetzt eine Hintergrundfarbe ausactive:,hover:,focus:odergroup-*:den Verlauf, solange dieser Zustand gilt (es sei denn, der Zustand setzt einen eigenen Verlauf, etwaactive:from-*). Dasselbe gilt füractiveStyleund die anderen State-Styles sowie für animierte Hintergrundfarben. - Text- und Touch-Einstellungen sind Style-Schlüssel:
line-clamp-*setztnumberOfLines,text-ellipsisundtext-clipsetzentextOverflow,truncatebeides,select-*setztuserSelectundpointer-events-*setztpointerEvents. Sie funktionieren daher in State-Varianten und unterliegen wie jede Klasse dem Inline-style.caret-*,placeholder:text-*undselection:bg-*setzen die StylescaretColor,placeholderColorundselectionColorauf dieselbe Weise. In Rust sind diesStyled-Methoden (.number_of_lines(..),.truncate(),.text_overflow(..),.select_text(),.pointer_events(..), …).whitespace-nowraphält einen Text als Klasse in einer Zeile.object-*setzen die StylesobjectFitundobjectPositionvon Bildern und Videos, die die Schlüssel eines Styles wie jeden anderen ersetzen. - Der unterstützte Umfang: Utilities und Varianten von Tailwind 4, die eine native Bedeutung haben.
peer-*-Varianten, Pseudo-Elemente (before:,after:) und beliebige Eigenschaften ([mask-type:luminance]) werden als unbekannte Klassen gemeldet, ebenso jedes Utility, für das NSH keine native Bedeutung hat. So bedeutet eine Klasse nie etwas anderes als im Browser. Rendering-Hinweise (antialiased,will-change-transform,appearance-none,box-border, …) werden akzeptiert und ändern nichts.
Alles andere funktioniert wie in Tailwind 4 dokumentiert: die Farbpalette, Opacity-Modifier (bg-black/40), beliebige Werte (bg-[#34c759], w-[72px], grid-cols-[1fr_2fr]; bei p, px, py, pt, pr, pb, pl und den m-Utilities auch die Umgebungsvariablen, pt-[env(safe-area-inset-top)], pb-[calc(env(keyboard-inset-height)+49px)], siehe Safe Area und Tastatur), Brüche (w-1/2, translate-x-1/2), Schatten und Ringe, Verläufe (bg-linear-to-r, bg-radial, bg-conic mit from- / via- / to-), Filter, backdrop-blur-*, mix-blend-*, Transforms, transition-* und duration-*, animate-spin / ping / pulse / bounce und Grid, jeweils mit ihrer typisierten Methode in Rust. Die Plattformgrenzen von Filtern und Blending gelten auch für Klassen.
#Theme
Klassen werden gegen das Standard-Theme von Tailwind 4 aufgelöst. Erweitere es in nsh.json unter "tailwind". Deine Einträge werden zu den Standardwerten hinzugefügt (oder ersetzen den Standardwert gleichen Namens):
{
"tailwind": {
"colors": {
"brand": { "50": "#f5f3ff", "500": "#7c3aed", "600": "#6d28d9", "DEFAULT": "#7c3aed" },
"ink": "#1f2328"
},
"radius": { "card": 14 },
"fontFamily": { "display": "Pacifico" },
"breakpoints": { "tablet": 600 }
}
}
Mit diesem Theme sind bg-brand-500, text-brand (der DEFAULT-Farbton), border-ink, rounded-card, font-display und tablet:flex-row gültige Klassen.
| Schlüssel | Werte | Erzeugt |
|---|---|---|
colors |
Eine CSS-Farbe oder ein Objekt aus Farbtönen ("DEFAULT" für den bloßen Namen) |
bg-*, text-*, border-*, from-*, ring-*, … |
spacing |
Punkte pro Spacing-Schritt (Standard 4) | p-*, m-*, gap-*, w-*, … |
rem |
Punkte pro rem in beliebigen Werten (Standard 16) |
|
breakpoints |
Punkte | name:-Varianten |
container |
Punkte | max-w-*-Größen |
fontSize |
Punkte oder [size, lineHeight], wobei die Zeilenhöhe ein Vielfaches der Größe ist |
text-* |
fontWeight |
1–1000 | font-* |
fontFamily |
Ein Familienname (eine Systemfamilie oder der Name einer importierten Schrift) | font-* |
tracking |
Ems | tracking-* |
leading |
Vielfache der Schriftgröße | leading-* |
radius |
Punkte | rounded-* |
shadow, insetShadow, dropShadow |
Ein CSS-box-shadow |
shadow-*, inset-shadow-*, drop-shadow-* |
blur |
Punkte | blur-*, backdrop-blur-* |
perspective |
Punkte | perspective-* |
aspect |
Ein Verhältnis (1.5) |
aspect-* |
ease |
[x1, y1, x2, y2] |
ease-* |
Unbekannte Schlüssel und ungültige Werte sind Build-Fehler (nsh.json: tailwind: unknown theme key 'colours'). Das Theme wird in das Bundle kompiliert und vor dem ersten Frame angewendet.
Das Tailwind-Theme prägt deine Klassen. Typisierte Methoden behalten die Standardskalen von Tailwind, sodass eine Rust-Komponente in jeder App gleich aussieht, und übernehmen den Look der App aus den Design-Tokens ihres Themes, "theme" in nsh.json (Rust).
#Theme-Tokens
Das Theme der App ("theme" in nsh.json) enthält ihre Design-Tokens: die Farben von shadcn/ui (background, foreground, primary, muted-foreground, border, ring, chart-1, sidebar, …), ihr radius, ihre Schriften font-sans und font-mono sowie jedes Token, das die App hinzufügt. Klassen benennen sie als Tailwind-Namen der Variablen eines CSS-@theme, Rust-Komponenten lesen sie mit theme() aus nsh_base, und TSX liest sie mit token() aus nsh, sodass ein TSX-Screen und die Rust-Komponenten demselben Theme folgen:
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>Der Name eines Tokens entspricht dem Namen seiner CSS-Variable ohne --, so wie ihn die Theme-Datei schreibt: muted-foreground, radius, chart-1. In Rust sind die Tokens von shadcn Methoden von theme() (primary(), muted_foreground(), radius(), font_mono()), und jedes Token ist theme().color("up"), theme().number("radius-card") oder theme().font("font-display").
| Form | Nimmt | Beispiel |
|---|---|---|
| Ein Farbname in einer beliebigen Farb-Utility | Eine Theme-Farbe, auch mit Deckkraft | bg-primary, text-muted-foreground/70, bg-up/30, border-t-border, ring-ring, from-accent, placeholder:text-muted-foreground |
(--name), die Variablen-Kurzschreibweise von Tailwind 4 |
Eine Theme-Farbe oder eine Zahl | bg-(--primary), rounded-(--radius), p-(--gap-card) |
(color:--name), (length:--name), (number:--name), (family-name:--name) |
Dasselbe, mit Typangabe, wo eine Utility mehrere Arten akzeptiert | text-(color:--up), text-(length:--text-title), font-(family-name:--font-mono) |
font-sans, font-mono |
Die Schriften des Themes, sofern es sie setzt | font-mono |
- Eigene Tokens. Jeder Name in der Theme-Datei ist ein Token, so wie jede Variable eines CSS-
@theme:"up": "#16a34a"erzeugtbg-up,text-upundbg-up/30;"radius-card": "14px"erzeugtrounded-(--radius-card);"font-display": "Pacifico"erzeugtfont-(family-name:--font-display). Längen sind in Punkten angegeben;(number:--name)übergibt die reine Zahl. - Schriften.
font-sansist die Schrift der App: Jeder Text, der keine eigene Schriftfamilie setzt, wird damit gezeichnet, so wie eine Seite mit Tailwinds--font-sans.font-monoist die Schrift vonfont-monound von Code inTextView. Sind sie nicht gesetzt, sind beide die Systemschriften. - Beide Darstellungen. Ein Token hat den hellen und den dunklen Wert des Themes:
bg-primary,theme().primaryundtoken("primary")wechseln von selbst mit der Darstellung, ohnedark:-Klasse. Eine Farbe, die das Theme nur in einer Darstellung setzt, wird in beiden verwendet. - Deine Tailwind-Farben haben Vorrang. Ein Name in
"tailwind"colorsoder in der Tailwind-Palette ist diese Farbe, sodassbg-primarynur dann ein Theme-Token ist, wenn das Tailwind-Theme keine Farbeprimaryhat. - Das Theme kann sich ändern, während die App läuft. Wenn eine in eine native App eingebettete App das Theme dieser App erhält und jedes Mal, wenn sich das Theme ändert, werden die Klassen, die Tokens benennen, die Rust-Bindings, die
theme()lesen, und der TSX-Code, dertoken()liest, aktualisiert, die Klassen und Bindings nativ. Tokens, die das Theme der einbettenden App nicht hat, behalten die Werte des eigenen"theme"der App (danach die des Standard-Themes). Ein Host, der nur seine Marke ergänzt, liefert Theme-Overrides, also die Tokens, die seine Marke ändert, im selben Dateiformat (iOS, Android,AppView::set_theme_overridesauf den Desktop-Plattformen). - Geprüft wie andere Klassen. Ein Name, der kein Token des Standard-Themes oder des
"theme"der App ist, ist beim Build eine unbekannte Klasse (unknown class 'bg-chart-up'). Um ein Token zu verwenden, das nur die einbettende App bereitstellt, gib ihm zusätzlich einen Wert im"theme"der App: Dieser Wert wird angezeigt, wenn die App eigenständig läuft.
Klassen, die Tokens benennen, werden wie alle anderen Klassen einmal pro Theme aufgelöst: Sie zu zeichnen kostet so viel wie eine Palettenfarbe.
#token() in TSX
token(name) aus nsh ist der Wert eines Tokens in der aktuell angezeigten Darstellung: eine Farbe als #rrggbbaa, eine Länge als Anzahl von Punkten, eine Schrift als ihr Familienname. Der Wert ist reaktiv: Code, der ihn in einer Komponente, einem Memo oder einem Effect liest, läuft erneut, wenn sich das Theme oder die Darstellung ändert. Das eignet sich für Werte, die dorthin gehen, wo keine Klasse hinpasst, etwa für ein Canvas oder ein Diagramm:
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();
}}
/>
Die Namen sind typisiert: NSH schreibt die Tokens der App aus ihrem Theme in .nsh/types/nsh/tokens.d.ts, sodass token("up") ein string ist, sobald "up" im Theme steht, und ein Name, den das Theme nicht hat, ein Typfehler ist.
#Die Klassenprüfung
nsh build, nsh dev, nsh run und nsh test prüfen die statischen Klassennamen in deinem JSX (String-Literale in class und die Schlüssel von classList) mit demselben Parser und demselben Theme wie die Laufzeit und warnen vor Klassen, die nicht existieren:
warn nsh::build App.tsx:12:36: unknown class `bg-brand-550`
Warnungen lassen den Build nicht fehlschlagen. Klassen, die zur Laufzeit zusammengesetzt werden (bg-${color()}-500), werden geprüft, wenn die Laufzeit sie zum ersten Mal sieht; sie protokolliert jede unbekannte Klasse einmal (unknown classes: ...). Typisierte Methoden werden vom Rust-Compiler geprüft.
#Klassen oder Styles?
Beide werden nativ aufgelöst und kosten etwa gleich viel. Klassen sind kürzer und bieten Varianten (gedrückte Zustände, Dark Mode, Breakpoints) ohne Code. Inline-Styles nehmen in JavaScript berechnete Werte und akzeptieren native animierte Werte (opacity: fade, translate: [x, 0]). Die meisten TSX-Apps verwenden Klassen für das statische Aussehen und style für alles Berechnete oder Animierte. Rust-Komponenten verwenden für beides typisierte Methoden und binden animierte Werte mit .bind_opacity(..), .bind_translate_x(..) und den anderen Bindings.
#Siehe auch
- Styling: die Style-Prop, die typisierten Methoden und die plattformspezifischen Grenzen von Effekten.
- Tailwind-Referenz: jede Utility und jede Variante.
- Layout: Flexbox und Grid, auch als Klassen verfügbar.