Документация
Tailwind
Стили NSH следуют Tailwind CSS 4. В Rust утилиты Tailwind — это типизированные методы стиля с теми же именами и значениями (.p_4() — это p-4, .rounded_2xl() — это rounded-2xl, ZINC.shade(900) — это zinc-900); в TSX каждый элемент принимает сами классы в 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 и 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сопоставляет классам условия: класс применяется, пока его значение истинно. Применяется послеclass.- В Rust метод, которому передано замыкание или сигнал, следует за ним (
.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.!делает класс важным:!p-0.
Компоненты на Rust (из nsh/base и из собственного крейта приложения) принимают class и classList и из TSX; ваши классы объединяются после собственных стилей компонента и при равном приоритете побеждают.
#Варианты
| Вариант | Когда применяется | Rust |
|---|---|---|
active: | Представление нажато | .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: после псевдоклассов, в порядке их имён | |
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: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: | Ориентация окна | 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), а не с устройством: в режиме Split View на iPad или в окне с изменяемым размером они следуют ширине окна, как и window().
#Отличия от Tailwind в вебе
- Единицы измерения — пункты.
p-4(.p_4()) равен 16 пунктам (1 шаг интервала = 4pt, 1rem = 16pt). Произвольные значения вpxтоже задаются в пунктах:p-[13px](.p(px(13.0))) равно 13pt. flex— это строка, как в CSS:class="flex"располагает дочерние элементы в строку, если класс направления не говорит иного (в любом месте строки:flex-col flex— это столбец).display: "flex"стиля и.flex()в Rust точно так же являются строками, а представление, не задающее display, сохраняет столбец NSH по умолчанию.blockиflow-rootиспользуют блочный макет, как и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также радиусы скругления и ширину границ; фильтры, фильтры фона, градиенты и макет принимают новые значения сразу. 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-*, псевдоэлементы (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. Платформенные ограничения фильтров и смешивания распространяются и на классы.
#Тема
Классы разрешаются относительно темы 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 выглядит одинаково в каждом приложении, а внешний вид приложения берут из дизайн-токенов его темы, "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 называет переменные CSS @theme, компоненты 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 |
- Собственные токены. Каждое имя в файле темы является токеном, как и каждая переменная CSS
@theme:"up": "#16a34a"создаётbg-up,text-upиbg-up/30;"radius-card": "14px"создаётrounded-(--radius-card);"font-display": "Pacifico"создаётfont-(family-name:--font-display). Длины задаются в пунктах;(number:--name)передаёт обычное число. - Шрифты.
font-sans— это шрифт приложения: им рисуется любой текст, у которого не задано собственное семейство, как страница рисуется шрифтом Tailwind--font-sans.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, длина в виде числа пунктов, шрифт в виде названия семейства. Оно реактивно, поэтому код, который читает его в компоненте, мемо или эффекте, выполняется снова при смене темы или оформления. Это удобно для значений, которые попадают туда, где классы не работают, например в 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, также доступные в виде классов.