1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
//! Token-derived engine surface styling for Snora Design (RFC-039).
//!
//! [`render`] is a sibling to [`crate::render::render`], not a
//! replacement: with `design` inactive, [`crate::render::render`]'s
//! output is byte-for-byte unchanged (RFC-037's gating invariant).
//! Applications opt in by calling the other function; nothing here runs
//! unless they do.
//!
//! # Why a sibling entry point, not a field on `AppLayout`
//!
//! `AppLayout` lives in `snora-core`, which has **no dependencies at
//! all** — adding a `Tokens` field would pull `snora-design` into every
//! engine-only build, defeating the opt-in size discipline (DEC-11) and
//! inverting the documented crate-dependency direction. `snora` already
//! depends on both `snora-core` and (behind `design`) `snora-design`, so
//! the sibling function lives here instead.
//!
//! # Two surfaces, both within the frozen token surface
//!
//! RFC-036's additive-only covenant freezes `snora-design`'s `Palette`
//! (18 roles) and `Tokens` (no shadow/elevation scale). Neither surface
//! this module styles has a purpose-built token — deriving from existing
//! roles, not extending the frozen surface, is the owner-confirmed
//! approach (RFC-039 §"The covenant bites here").
//!
//! ## The dialog card
//!
//! Fill `surface_raised`, edge `border`, radius `radius.lg`, padding
//! `spacing.lg` — reusing [`snora_style::container::card_raised`]
//! (RFC-029, relocated from `snora-widgets` by RFC-055) directly rather
//! than recomputing the same color/border
//! mapping, with its drop shadow zeroed out. **Border-defined, not
//! shadow-defined**, deliberately: shadows are close to meaningless in
//! the high-contrast presets (`high_contrast_light`'s shadow color and
//! its background are both near-white; `high_contrast_dark`'s near-black
//! against near-black), and a border already renders correctly there.
//!
//! ## The modal dim
//!
//! `iced::Color::from_rgba(0.0, 0.0, 0.0, 0.4)` — opaque black at 40%
//! alpha — is the unstyled default. Composited over a **dark** page
//! background, black-on-black is close to a no-op: the exact class of
//! defect RFC-038's `shift_away_from` was built to prevent for derived
//! theme tiers, here for a fixed constant instead of a derived one.
//!
//! [`dim_color`] instead picks the dim's base color from
//! **`background`'s own darkness**, not a fixed pole — `Color::WHITE` if
//! dark, `Color::BLACK` if light. This has no clamping edge case (unlike
//! `shift_away_from`'s OKLCH-lightness shift): alpha-compositing a color
//! chosen to be the *opposite* pole from the background's own category
//! can never degenerate to a no-op, because the two poles cannot both
//! describe the same background. Safe at both luminance extremes —
//! `light`'s pure-white background and `high_contrast_dark`'s pure-black
//! one, the two cases that broke RFC-038's first attempt — precisely
//! because the derivation never tries to move a color away from its own
//! tone; it only ever chooses between two fixed, maximally-distinct
//! poles.
//!
//! **The derivation itself lives in [`snora_design::surfaces::modal_dim`]
//! (RFC-065), not here.** `dim_color` is a thin adapter: it calls that
//! function and converts the result to [`iced::Color`]. This is not the
//! same alpha as the unstyled path's `0.4` any more —
//! [`snora_design::surfaces::DIM_ALPHA`] is `0.44`, repaired because the
//! `light` preset's dialog card was measured at 2.85:1 against its own
//! dimmed backdrop, below SC 1.4.11's 3:1 floor, by either signal (border
//! or fill). The two paths were symmetric at `0.40` by coincidence, not
//! design; RFC-065 lets them diverge on purpose. See
//! `crates/snora-design/src/tests.rs` for the either-signal assertion
//! this repair answers, and `render/tests.rs` for the alpha and pole
//! tests on this crate's own adapter.
use Responsive;
use ;
use AppLayout;
use Tokens;
// RFC-055: relocated from snora_widgets::design::style::color — the
// engine surface reaches snora-style directly, without depending on
// snora-widgets.
use to_iced_color;
use crateDialogCardStyle;
use crate;
/// Derives a complete, token-styled render from an [`AppLayout`]: the
/// dialog gets a real card (fill, border, radius, padding), and the
/// modal dim is derived from the token bundle instead of a fixed
/// constant. See the module documentation for the derivation and its
/// rationale.
///
/// Snora does not call this on the application's behalf; it is a sibling
/// to [`crate::render::render`], not a replacement. Applications opt in
/// explicitly:
///
/// ```rust,no_run
/// use iced::{Element, widget::text};
/// use snora::{AppLayout, design::{Tokens, render}};
///
/// #[derive(Debug, Clone)]
/// enum Message {}
///
/// let tokens = Tokens::light();
/// let body: Element<'_, Message> = text("Hello, snora!").into();
/// let layout = AppLayout::new(body);
/// let element = render(layout, &tokens);
/// ```
/// Renders an [`AppLayout`] that may depend on the available width,
/// through the `design` path — the styled dialog card and the
/// token-derived modal dim survive, unlike [`crate::responsive::responsive_render`]
/// (RFC-053).
///
/// `build` receives the width available to the layout (in logical
/// pixels) and returns the `AppLayout` to render at that width. It may
/// be called again whenever the available size changes — see
/// [`iced::widget::Responsive`]'s own documentation for the underlying
/// mechanism. Mirrors [`crate::responsive::responsive_render`] exactly,
/// with [`render`] in place of [`crate::render::render`]; it is a
/// wrapper around the existing composition path, not a second one — see
/// this module's documentation for why that duplication is exactly what
/// RFC-039 built [`render`] to avoid.
///
/// `&'a Tokens`, matching [`render`]'s own `(layout, &tokens)` shape:
/// the borrow is natural in the usual `fn view(&self) -> Element<'_,
/// Message>`, where the returned element already borrows `&self`.
///
/// ```rust,no_run
/// use iced::{Element, widget::text};
/// use snora::{AppLayout, design::{Tokens, responsive_render}};
///
/// struct State;
/// #[derive(Debug, Clone)]
/// enum Message {}
///
/// fn body(_state: &State) -> Element<'_, Message> {
/// text("body").into()
/// }
/// fn sidebar(_state: &State) -> Element<'_, Message> {
/// text("sidebar").into()
/// }
///
/// fn view<'a>(state: &'a State, tokens: &'a Tokens) -> Element<'a, Message> {
/// responsive_render(
/// move |width| {
/// let layout = AppLayout::new(body(state));
/// if width < 600.0 {
/// layout
/// } else {
/// layout.side_bar(sidebar(state))
/// }
/// },
/// tokens,
/// )
/// }
/// ```
/// See the module documentation's "The modal dim" section for the full
/// derivation rationale. A thin adapter over
/// [`snora_design::surfaces::modal_dim`] (RFC-065) — the derivation
/// itself, and [`snora_design::surfaces::DIM_ALPHA`], live there as the
/// single source; this function only converts the result to
/// [`iced::Color`].
/// See the module documentation's "The dialog card" section.