tangled
alpha
login
or
join now
nonbinary.computer
/
weaver
atproto blogging
24
fork
atom
overview
issues
2
pulls
pipelines
[weaver-app] add InlineThemeEditor component
Orual
3 weeks ago
7e01a8b3
88bb15d1
+311
3 changed files
expand all
collapse all
unified
split
crates
weaver-app
assets
styling
inline-theme-editor.css
src
components
inline_theme_editor.rs
mod.rs
+89
crates/weaver-app/assets/styling/inline-theme-editor.css
···
1
1
+
/* Inline theme editor for notebook settings */
2
2
+
3
3
+
.inline-theme-editor {
4
4
+
display: flex;
5
5
+
flex-direction: column;
6
6
+
gap: 1rem;
7
7
+
padding: 1rem;
8
8
+
background: var(--color-surface);
9
9
+
border: 1px solid var(--color-border);
10
10
+
}
11
11
+
12
12
+
.inline-theme-editor-heading {
13
13
+
margin: 0;
14
14
+
font-size: 1rem;
15
15
+
font-weight: 600;
16
16
+
color: var(--color-text);
17
17
+
}
18
18
+
19
19
+
.inline-theme-editor-presets {
20
20
+
display: flex;
21
21
+
flex-direction: column;
22
22
+
gap: 0.25rem;
23
23
+
}
24
24
+
25
25
+
.inline-theme-editor-presets label {
26
26
+
font-size: 0.875rem;
27
27
+
color: var(--color-muted);
28
28
+
}
29
29
+
30
30
+
.inline-theme-editor-presets select {
31
31
+
padding: 0.5rem;
32
32
+
border: 1px solid var(--color-border);
33
33
+
background: var(--color-base);
34
34
+
color: var(--color-text);
35
35
+
font-family: var(--font-ui);
36
36
+
font-size: 0.875rem;
37
37
+
}
38
38
+
39
39
+
.inline-theme-editor-presets select:focus {
40
40
+
outline: none;
41
41
+
border-color: var(--color-primary);
42
42
+
}
43
43
+
44
44
+
.inline-theme-editor-colours {
45
45
+
display: grid;
46
46
+
grid-template-columns: repeat(2, 1fr);
47
47
+
gap: 0.75rem;
48
48
+
}
49
49
+
50
50
+
.inline-theme-editor-code-theme,
51
51
+
.inline-theme-editor-mode {
52
52
+
display: flex;
53
53
+
flex-direction: column;
54
54
+
gap: 0.25rem;
55
55
+
}
56
56
+
57
57
+
.inline-theme-editor-code-theme label,
58
58
+
.inline-theme-editor-mode label {
59
59
+
font-size: 0.875rem;
60
60
+
color: var(--color-muted);
61
61
+
}
62
62
+
63
63
+
.inline-theme-editor-code-theme select {
64
64
+
padding: 0.5rem;
65
65
+
border: 1px solid var(--color-border);
66
66
+
background: var(--color-base);
67
67
+
color: var(--color-text);
68
68
+
font-family: var(--font-ui);
69
69
+
font-size: 0.875rem;
70
70
+
}
71
71
+
72
72
+
.inline-theme-editor-code-theme select:focus {
73
73
+
outline: none;
74
74
+
border-color: var(--color-primary);
75
75
+
}
76
76
+
77
77
+
.inline-theme-editor-mode {
78
78
+
gap: 0.5rem;
79
79
+
}
80
80
+
81
81
+
.inline-theme-editor-full-link {
82
82
+
font-size: 0.875rem;
83
83
+
color: var(--color-link);
84
84
+
text-decoration: none;
85
85
+
}
86
86
+
87
87
+
.inline-theme-editor-full-link:hover {
88
88
+
text-decoration: underline;
89
89
+
}
+219
crates/weaver-app/src/components/inline_theme_editor.rs
···
1
1
+
//! Inline theme editor for notebook settings.
2
2
+
//!
3
3
+
//! Provides core 4 colour pickers (background, text, primary, link) plus code theme selector.
4
4
+
5
5
+
use dioxus::prelude::*;
6
6
+
use weaver_renderer::themes::{BUILTIN_CODE_THEMES, BUILTIN_COLOUR_SCHEMES};
7
7
+
8
8
+
use crate::components::toggle_group::{ToggleGroup, ToggleItem};
9
9
+
use crate::components::HexColourInput;
10
10
+
11
11
+
/// Strip leading # or 0x from hex colour strings.
12
12
+
fn strip_hex_prefix(s: &str) -> String {
13
13
+
s.trim_start_matches('#')
14
14
+
.trim_start_matches("0x")
15
15
+
.trim_start_matches("0X")
16
16
+
.to_uppercase()
17
17
+
}
18
18
+
19
19
+
/// Convert mode string to toggle index.
20
20
+
fn mode_to_index(mode: &str) -> std::collections::HashSet<usize> {
21
21
+
let idx = match mode {
22
22
+
"light" => 1,
23
23
+
"dark" => 2,
24
24
+
_ => 0, // "auto" or default
25
25
+
};
26
26
+
std::collections::HashSet::from([idx])
27
27
+
}
28
28
+
29
29
+
/// Convert toggle index to mode string.
30
30
+
fn index_to_mode(idx: usize) -> &'static str {
31
31
+
match idx {
32
32
+
1 => "light",
33
33
+
2 => "dark",
34
34
+
_ => "auto",
35
35
+
}
36
36
+
}
37
37
+
38
38
+
const INLINE_THEME_EDITOR_CSS: Asset = asset!("/assets/styling/inline-theme-editor.css");
39
39
+
40
40
+
/// Theme values being edited.
41
41
+
#[derive(Debug, Clone, PartialEq, Default)]
42
42
+
pub struct InlineThemeValues {
43
43
+
pub background: String,
44
44
+
pub text: String,
45
45
+
pub primary: String,
46
46
+
pub link: String,
47
47
+
pub code_theme: String,
48
48
+
pub default_mode: String, // "light", "dark", or "auto"
49
49
+
}
50
50
+
51
51
+
/// Props for InlineThemeEditor.
52
52
+
#[derive(Props, Clone, PartialEq)]
53
53
+
pub struct InlineThemeEditorProps {
54
54
+
/// Current theme values.
55
55
+
pub values: InlineThemeValues,
56
56
+
/// Callback when any value changes.
57
57
+
pub onchange: EventHandler<InlineThemeValues>,
58
58
+
}
59
59
+
60
60
+
/// Inline theme editor with core colour pickers and code theme selector.
61
61
+
#[component]
62
62
+
pub fn InlineThemeEditor(props: InlineThemeEditorProps) -> Element {
63
63
+
let values = props.values.clone();
64
64
+
65
65
+
// Filter code themes by current mode for the selector.
66
66
+
let mode = &values.default_mode;
67
67
+
let relevant_code_themes: Vec<_> = BUILTIN_CODE_THEMES
68
68
+
.iter()
69
69
+
.filter(|t| mode == "auto" || t.variant == mode)
70
70
+
.collect();
71
71
+
72
72
+
rsx! {
73
73
+
document::Stylesheet { href: INLINE_THEME_EDITOR_CSS }
74
74
+
75
75
+
div { class: "inline-theme-editor",
76
76
+
h4 { class: "inline-theme-editor-heading", "Theme" }
77
77
+
78
78
+
// Preset selector.
79
79
+
div { class: "inline-theme-editor-presets",
80
80
+
label { "Start from preset:" }
81
81
+
select {
82
82
+
onchange: {
83
83
+
let onchange = props.onchange.clone();
84
84
+
let code_theme = props.values.code_theme.clone();
85
85
+
move |e: Event<FormData>| {
86
86
+
let preset_id = e.value();
87
87
+
if let Some(scheme) = BUILTIN_COLOUR_SCHEMES.iter().find(|s| s.id == preset_id) {
88
88
+
onchange.call(InlineThemeValues {
89
89
+
background: strip_hex_prefix(&scheme.colours.base),
90
90
+
text: strip_hex_prefix(&scheme.colours.text),
91
91
+
primary: strip_hex_prefix(&scheme.colours.primary),
92
92
+
link: strip_hex_prefix(&scheme.colours.link),
93
93
+
code_theme: code_theme.clone(),
94
94
+
default_mode: scheme.variant.to_string(),
95
95
+
});
96
96
+
}
97
97
+
}
98
98
+
},
99
99
+
option { value: "", "Custom" }
100
100
+
for scheme in BUILTIN_COLOUR_SCHEMES.iter() {
101
101
+
option {
102
102
+
value: "{scheme.id}",
103
103
+
"{scheme.name}"
104
104
+
}
105
105
+
}
106
106
+
}
107
107
+
}
108
108
+
109
109
+
// Colour pickers.
110
110
+
div { class: "inline-theme-editor-colours",
111
111
+
HexColourInput {
112
112
+
label: Some("Background".to_string()),
113
113
+
value: values.background.clone(),
114
114
+
onchange: {
115
115
+
let values = props.values.clone();
116
116
+
let onchange = props.onchange.clone();
117
117
+
move |val| {
118
118
+
let mut v = values.clone();
119
119
+
v.background = val;
120
120
+
onchange.call(v);
121
121
+
}
122
122
+
},
123
123
+
}
124
124
+
HexColourInput {
125
125
+
label: Some("Text".to_string()),
126
126
+
value: values.text.clone(),
127
127
+
onchange: {
128
128
+
let values = props.values.clone();
129
129
+
let onchange = props.onchange.clone();
130
130
+
move |val| {
131
131
+
let mut v = values.clone();
132
132
+
v.text = val;
133
133
+
onchange.call(v);
134
134
+
}
135
135
+
},
136
136
+
}
137
137
+
HexColourInput {
138
138
+
label: Some("Primary".to_string()),
139
139
+
value: values.primary.clone(),
140
140
+
onchange: {
141
141
+
let values = props.values.clone();
142
142
+
let onchange = props.onchange.clone();
143
143
+
move |val| {
144
144
+
let mut v = values.clone();
145
145
+
v.primary = val;
146
146
+
onchange.call(v);
147
147
+
}
148
148
+
},
149
149
+
}
150
150
+
HexColourInput {
151
151
+
label: Some("Link".to_string()),
152
152
+
value: values.link.clone(),
153
153
+
onchange: {
154
154
+
let values = props.values.clone();
155
155
+
let onchange = props.onchange.clone();
156
156
+
move |val| {
157
157
+
let mut v = values.clone();
158
158
+
v.link = val;
159
159
+
onchange.call(v);
160
160
+
}
161
161
+
},
162
162
+
}
163
163
+
}
164
164
+
165
165
+
// Code theme selector.
166
166
+
div { class: "inline-theme-editor-code-theme",
167
167
+
label { "Code theme:" }
168
168
+
select {
169
169
+
value: "{values.code_theme}",
170
170
+
onchange: {
171
171
+
let values = props.values.clone();
172
172
+
let onchange = props.onchange.clone();
173
173
+
move |e: Event<FormData>| {
174
174
+
let mut v = values.clone();
175
175
+
v.code_theme = e.value();
176
176
+
onchange.call(v);
177
177
+
}
178
178
+
},
179
179
+
for theme in relevant_code_themes {
180
180
+
option {
181
181
+
value: "{theme.id}",
182
182
+
"{theme.name}"
183
183
+
}
184
184
+
}
185
185
+
}
186
186
+
}
187
187
+
188
188
+
// Default mode toggle group.
189
189
+
// Indexes: 0 = auto, 1 = light, 2 = dark
190
190
+
div { class: "inline-theme-editor-mode",
191
191
+
label { "Default mode:" }
192
192
+
ToggleGroup {
193
193
+
horizontal: true,
194
194
+
default_pressed: mode_to_index(&values.default_mode),
195
195
+
on_pressed_change: {
196
196
+
let values = props.values.clone();
197
197
+
let onchange = props.onchange.clone();
198
198
+
move |pressed: std::collections::HashSet<usize>| {
199
199
+
if let Some(&idx) = pressed.iter().next() {
200
200
+
let mut v = values.clone();
201
201
+
v.default_mode = index_to_mode(idx).to_string();
202
202
+
onchange.call(v);
203
203
+
}
204
204
+
}
205
205
+
},
206
206
+
ToggleItem { index: 0usize, "Auto" }
207
207
+
ToggleItem { index: 1usize, "Light" }
208
208
+
ToggleItem { index: 2usize, "Dark" }
209
209
+
}
210
210
+
}
211
211
+
212
212
+
// Link to full editor.
213
213
+
// TODO: Enable once theme page exists
214
214
+
// a { href: "/{ident}/themes", class: "inline-theme-editor-full-link",
215
215
+
// "Edit full theme ->"
216
216
+
// }
217
217
+
}
218
218
+
}
219
219
+
}
+3
crates/weaver-app/src/components/mod.rs
···
362
362
363
363
mod hex_colour_input;
364
364
pub use hex_colour_input::HexColourInput;
365
365
+
366
366
+
mod inline_theme_editor;
367
367
+
pub use inline_theme_editor::{InlineThemeEditor, InlineThemeValues};