1use crate::core::{
4 assets,
5 attrs::{theme_attributes, theme_pairs},
6 id::IdAllocator,
7 platform::{Capabilities, DomBridge},
8};
9use dioxus::prelude::*;
10use std::{
11 cell::Cell,
12 rc::Rc,
13 sync::atomic::{AtomicU64, Ordering},
14};
15
16#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
26#[non_exhaustive]
27pub enum Design {
28 #[default]
30 Future,
31 Clarity,
33 Helix,
35 Forest,
37 Editorial,
39 Graphite,
41 Ocean,
43 Ember,
45 Blueprint,
47 Bloom,
49 Aurora,
51 Candy,
53 Solar,
55 Nocturne,
57 Arcade,
59 Parchment,
61 Bubble,
63 Garnet,
65 Origami,
67 Sundown,
69 Pride,
71}
72impl Design {
73 pub const ALL: &'static [Self] = &[
75 Self::Future,
76 Self::Clarity,
77 Self::Helix,
78 Self::Forest,
79 Self::Editorial,
80 Self::Graphite,
81 Self::Ocean,
82 Self::Ember,
83 Self::Blueprint,
84 Self::Bloom,
85 Self::Aurora,
86 Self::Candy,
87 Self::Solar,
88 Self::Nocturne,
89 Self::Arcade,
90 Self::Parchment,
91 Self::Bubble,
92 Self::Garnet,
93 Self::Origami,
94 Self::Sundown,
95 Self::Pride,
96 ];
97 #[must_use]
99 pub const fn as_str(self) -> &'static str {
100 match self {
101 Self::Future => "future",
102 Self::Clarity => "clarity",
103 Self::Helix => "helix",
104 Self::Forest => "forest",
105 Self::Editorial => "editorial",
106 Self::Graphite => "graphite",
107 Self::Ocean => "ocean",
108 Self::Ember => "ember",
109 Self::Blueprint => "blueprint",
110 Self::Bloom => "bloom",
111 Self::Aurora => "aurora",
112 Self::Candy => "candy",
113 Self::Solar => "solar",
114 Self::Nocturne => "nocturne",
115 Self::Arcade => "arcade",
116 Self::Parchment => "parchment",
117 Self::Bubble => "bubble",
118 Self::Garnet => "garnet",
119 Self::Origami => "origami",
120 Self::Sundown => "sundown",
121 Self::Pride => "pride",
122 }
123 }
124 #[must_use]
126 pub const fn name(self) -> &'static str {
127 match self {
128 Self::Future => "Future",
129 Self::Clarity => "Clarity",
130 Self::Helix => "Helix",
131 Self::Forest => "Forest",
132 Self::Editorial => "Editorial",
133 Self::Graphite => "Graphite",
134 Self::Ocean => "Ocean",
135 Self::Ember => "Ember",
136 Self::Blueprint => "Blueprint",
137 Self::Bloom => "Bloom",
138 Self::Aurora => "Aurora",
139 Self::Candy => "Candy",
140 Self::Solar => "Solar",
141 Self::Nocturne => "Nocturne",
142 Self::Arcade => "Arcade",
143 Self::Parchment => "Parchment",
144 Self::Bubble => "Bubble",
145 Self::Garnet => "Garnet",
146 Self::Origami => "Origami",
147 Self::Sundown => "Sundown",
148 Self::Pride => "Pride",
149 }
150 }
151 #[must_use]
153 pub const fn description(self) -> &'static str {
154 match self {
155 Self::Future => "Balanced corners, quiet surfaces and soft elevation.",
156 Self::Clarity => "Crisp edges, flat surfaces and outlined actions.",
157 Self::Helix => "Charcoal chrome, mint actions and violet feature surfaces.",
158 Self::Forest => "Organic curves, green accents and calm surfaces.",
159 Self::Editorial => "Serif reading, restrained corners and ink-like controls.",
160 Self::Graphite => "Monochrome surfaces with distinct, structured edges.",
161 Self::Ocean => "Cool teal, rounded panels and a tinted header.",
162 Self::Ember => "Warm accents, substantial elevation and squared panels.",
163 Self::Blueprint => "Square geometry, blue accents and precise outlines.",
164 Self::Bloom => "Playful curves, rose accents and floating colored panels.",
165 Self::Aurora => "Violet ribbons, luminous curves and floating surfaces.",
166 Self::Candy => "Rose candy stripes, pillowy panels and playful elevation.",
167 Self::Solar => "Amber sunshine, square panels and decisive offset shadows.",
168 Self::Nocturne => "Ink chrome, violet spotlights and elegant serif reading.",
169 Self::Arcade => "Green scanlines, square geometry and terminal-inspired type.",
170 Self::Parchment => "Warm ruled paper, serif type and quiet outlined controls.",
171 Self::Bubble => "Teal bubbles, generous curves and buoyant layered shadows.",
172 Self::Garnet => "Rose jewel facets, crisp serif panels and charcoal chrome.",
173 Self::Origami => "Blue folded ribbons, angular outlines and paper shadows.",
174 Self::Sundown => "Ember horizons, rounded reading panels and long soft shadows.",
175 Self::Pride => "Rainbow ribbons, joyful rounded panels and clear violet actions.",
176 }
177 }
178 #[must_use]
180 #[allow(clippy::match_same_arms)]
183 pub const fn palette(self) -> Palette {
184 match self {
185 Self::Future => Palette::Default,
186 Self::Clarity => Palette::Default,
187 Self::Helix => Palette::Helix,
188 Self::Forest => Palette::Forest,
189 Self::Editorial => Palette::Ink,
190 Self::Graphite => Palette::Ink,
191 Self::Ocean => Palette::Teal,
192 Self::Ember => Palette::Ember,
193 Self::Blueprint => Palette::Blue,
194 Self::Bloom => Palette::Rose,
195 Self::Aurora => Palette::Violet,
196 Self::Candy => Palette::Rose,
197 Self::Solar => Palette::Amber,
198 Self::Nocturne => Palette::Violet,
199 Self::Arcade => Palette::Forest,
200 Self::Parchment => Palette::Amber,
201 Self::Bubble => Palette::Teal,
202 Self::Garnet => Palette::Rose,
203 Self::Origami => Palette::Blue,
204 Self::Sundown => Palette::Ember,
205 Self::Pride => Palette::Violet,
206 }
207 }
208 #[must_use]
210 pub fn from_attribute(value: &str) -> Option<Self> {
211 Self::ALL
212 .iter()
213 .copied()
214 .find(|design| design.as_str() == value)
215 }
216}
217
218#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
220#[non_exhaustive]
221pub enum Palette {
222 #[default]
224 Default,
225 Blue,
227 Violet,
229 Forest,
231 Ink,
233 Teal,
235 Ember,
237 Rose,
239 Amber,
241 Helix,
243}
244impl Palette {
245 pub const ALL: &'static [Self] = &[
247 Self::Default,
248 Self::Blue,
249 Self::Violet,
250 Self::Forest,
251 Self::Ink,
252 Self::Teal,
253 Self::Ember,
254 Self::Rose,
255 Self::Amber,
256 Self::Helix,
257 ];
258 #[must_use]
260 pub const fn as_str(self) -> &'static str {
261 match self {
262 Self::Default => "default",
263 Self::Blue => "blue",
264 Self::Violet => "violet",
265 Self::Forest => "forest",
266 Self::Ink => "ink",
267 Self::Teal => "teal",
268 Self::Ember => "ember",
269 Self::Rose => "rose",
270 Self::Amber => "amber",
271 Self::Helix => "helix",
272 }
273 }
274 #[must_use]
276 pub const fn name(self) -> &'static str {
277 match self {
278 Self::Default => "Future blue",
279 Self::Blue => "Blueprint blue",
280 Self::Violet => "Violet",
281 Self::Forest => "Forest",
282 Self::Ink => "Ink",
283 Self::Teal => "Ocean teal",
284 Self::Ember => "Ember",
285 Self::Rose => "Rose",
286 Self::Amber => "Amber",
287 Self::Helix => "Helix violet & mint",
288 }
289 }
290 #[must_use]
292 pub fn from_attribute(value: &str) -> Option<Self> {
293 Self::ALL
294 .iter()
295 .copied()
296 .find(|palette| palette.as_str() == value)
297 }
298}
299
300#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
302#[non_exhaustive]
303pub enum Theme {
304 #[default]
306 System,
307 Light,
309 Dark,
311 HighContrastLight,
313 HighContrastDark,
315 FollowSystem,
318}
319impl Theme {
320 #[must_use]
322 pub const fn attribute(self) -> Option<&'static str> {
323 match self {
324 Self::System => None,
325 Self::Light => Some("light"),
326 Self::Dark => Some("dark"),
327 Self::HighContrastLight => Some("high-contrast-light"),
328 Self::HighContrastDark => Some("high-contrast-dark"),
329 Self::FollowSystem => Some("system"),
330 }
331 }
332}
333#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
335#[non_exhaustive]
336pub enum Brand {
337 Forest,
339}
340#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
342#[non_exhaustive]
343pub enum Direction {
344 #[default]
346 Ltr,
347 Rtl,
349}
350impl Direction {
351 #[must_use]
353 pub const fn as_str(self) -> &'static str {
354 match self {
355 Self::Ltr => "ltr",
356 Self::Rtl => "rtl",
357 }
358 }
359}
360#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
362pub enum Stylesheets {
363 #[default]
365 Automatic,
366 External,
368}
369
370#[derive(Debug, Clone, Copy, PartialEq, Eq)]
372struct DirectionContext(Direction);
373
374#[must_use]
377pub fn use_direction() -> Direction {
378 try_consume_context::<Signal<DirectionContext>>()
379 .map_or(Direction::Ltr, |direction| direction.read().0)
380}
381
382pub(crate) fn use_stylesheets() -> bool {
383 try_consume_context::<ReadSignal<Stylesheets>>()
384 .is_none_or(|policy| *policy.read() == Stylesheets::Automatic)
385}
386
387#[derive(Props, Clone, PartialEq)]
389pub struct FutureCssProps {
390 #[props(default)]
392 pub design: Option<Design>,
393 #[props(default)]
396 pub palette: Option<Palette>,
397 #[props(default)]
399 pub theme: Theme,
400 #[props(default)]
402 pub brand: Option<Brand>,
403 #[props(default)]
405 pub direction: Option<Direction>,
406 #[props(default)]
408 pub stylesheets: Stylesheets,
409 pub children: Element,
411}
412
413static NEXT_ROOT: AtomicU64 = AtomicU64::new(0);
415
416#[component]
418pub fn FutureCss(props: FutureCssProps) -> Element {
419 let outermost = use_hook(|| try_consume_context::<Signal<DirectionContext>>().is_none());
422 use_context_provider(IdAllocator::new);
423 let inherited = use_direction();
425 let effective = DirectionContext(props.direction.unwrap_or(inherited));
426 let mut direction = use_context_provider(|| Signal::new(effective));
427 if *direction.peek() != effective {
428 direction.set(effective);
429 }
430 let mut policy = use_signal(|| props.stylesheets);
431 if *policy.peek() != props.stylesheets {
432 policy.set(props.stylesheets);
433 }
434 use_context_provider(|| ReadSignal::from(policy));
435 let capabilities = use_signal(Capabilities::default);
436 use_context_provider(|| capabilities);
437 let mut capabilities = capabilities;
438 let pairs = theme_pairs(
439 props.theme.attribute(),
440 props.brand.map(|brand| match brand {
441 Brand::Forest => "forest",
442 }),
443 props.design.map(Design::as_str),
444 props
447 .palette
448 .or(props.design.map(Design::palette))
449 .map(Palette::as_str),
450 );
451 let attrs = theme_attributes(&pairs);
452 let token = use_hook(|| NEXT_ROOT.fetch_add(1, Ordering::Relaxed));
457 let generation = use_hook(|| Rc::new(Cell::new(0_u64)));
458 let next = generation.clone();
459 use_effect(use_reactive!(|pairs| {
460 if !outermost {
461 return;
462 }
463 next.set(next.get() + 1);
464 let generation = next.get();
465 spawn(async move {
466 let _ = DomBridge
467 .mirror_document_theme(token, generation, Some(&pairs))
468 .await;
469 });
470 }));
471 use_drop(move || {
472 if outermost {
473 DomBridge.release_document_theme(token, generation.get() + 1);
474 }
475 });
476 rsx! {
477 if props.stylesheets == Stylesheets::Automatic {
478 document::Stylesheet { href: assets::LAYERS_CSS }
479 document::Stylesheet { href: assets::TOKENS_CSS }
480 document::Stylesheet { href: assets::RESET_CSS }
481 document::Stylesheet { href: assets::BASE_CSS }
482 document::Stylesheet { href: assets::LAYOUT_CSS }
483 document::Stylesheet { href: assets::TYPE_CSS }
484 document::Stylesheet { href: assets::UTILITIES_CSS }
485 document::Stylesheet { href: assets::THEMES_CSS }
486 }
487 div {
488 dir: props.direction.map(Direction::as_str),
489 onmounted: move |_| async move { capabilities.set(DomBridge.capabilities().await); },
490 ..attrs,
491 {props.children}
492 }
493 }
494}