Add complete keyboard navigation.

This commit is contained in:
2026-08-07 17:00:49 +02:00
parent 87a161058f
commit 5709c80209
38 changed files with 1943 additions and 355 deletions

View File

@@ -8,7 +8,7 @@ The project is under active development. Core blogging workflows are broadly ava
## Available Features ## Available Features
- Native Iced desktop workspace with a localized native macOS menu and in-window Linux/Windows menu bar, tabs, anchored editor popovers, automatically paged post/media sidebars with locale-aware post dates, calendar months, and relative-dated entity lists, row deletion, dialogs, embedded Wry previews, a live Pico CSS theme editor, direct Preferences navigation, and per-project restart restoration of the active activity, shell visibility, and open editor tabs. The in-window menu supports Alt mnemonics and arrow-key navigation. Its shared task UI keeps queued work off blocking workers, presents active local and remote work plus recent history in bDS2-style grouped progress cards, and supports cooperative cancellation without oversubscribing background work. - Native Iced desktop workspace with a localized native macOS menu and in-window Linux/Windows menu bar, tabs, anchored editor popovers, automatically paged post/media sidebars with locale-aware post dates, calendar months, and relative-dated entity lists, row deletion, dialogs, embedded Wry previews, a live Pico CSS theme editor, direct Preferences navigation, and per-project restart restoration of the active activity, shell visibility, and open editor tabs. Every enabled control participates in Tab/Shift-Tab focus traversal and Enter/Space activation; holding Control+Option on macOS or Control+Alt elsewhere reveals direct shortcuts plus stable two/three-letter codes for visible generic controls, filters those codes as they are typed, and lets Escape cancel while Option/Alt alone remains available for character entry. The in-window menu supports Alt mnemonics and arrow-key navigation. Its shared task UI keeps queued work off blocking workers, presents active local and remote work plus recent history in bDS2-style grouped progress cards, and supports cooperative cancellation without oversubscribing background work.
- Post and translation authoring with change-aware draft/published/archive lifecycle, file-backed change discard, canonical draft reopening after manual translation edits, non-disruptive automatic translation, desktop archive/unarchive actions, in-place published-frontmatter updates, metadata, tags, categories, cursor-preserving link and media insertion, live link/backlink graphs, media, and batch gallery-image import. - Post and translation authoring with change-aware draft/published/archive lifecycle, file-backed change discard, canonical draft reopening after manual translation edits, non-disruptive automatic translation, desktop archive/unarchive actions, in-place published-frontmatter updates, metadata, tags, categories, cursor-preserving link and media insertion, live link/backlink graphs, media, and batch gallery-image import.
- Media import including HEIC/HEIF decoding, q80 WebP thumbnails (plus q85 AI JPEGs), metadata translations, filters, validation, post assignment, and sequential drag-and-drop insertion into post editors. - Media import including HEIC/HEIF decoding, q80 WebP thumbnails (plus q85 AI JPEGs), metadata translations, filters, validation, post assignment, and sequential drag-and-drop insertion into post editors.
- WordPress WXR migration with saved analyses, HTML-to-Markdown and shortcode conversion, conflict/taxonomy review, recoverable 500-item execution batches, media-parent linking, progress reporting, and optional AI-assisted taxonomy mapping. - WordPress WXR migration with saved analyses, HTML-to-Markdown and shortcode conversion, conflict/taxonomy review, recoverable 500-item execution batches, media-parent linking, progress reporting, and optional AI-assisted taxonomy mapping.

View File

@@ -238,6 +238,7 @@ pub enum Message {
// Settings // Settings
SetOfflineMode(bool), SetOfflineMode(bool),
SetUiLocale(UiLocale), SetUiLocale(UiLocale),
KeyboardNavigation(crate::components::keyboard::Navigation),
ToggleLocaleDropdown, ToggleLocaleDropdown,
ToggleProjectDropdown, ToggleProjectDropdown,
@@ -1457,6 +1458,10 @@ impl BdsApp {
pub fn update(&mut self, message: Message) -> Task<Message> { pub fn update(&mut self, message: Message) -> Task<Message> {
match message { match message {
Message::KeyboardNavigation(navigation) => match navigation {
crate::components::keyboard::Navigation::Next => iced::widget::focus_next(),
crate::components::keyboard::Navigation::Previous => iced::widget::focus_previous(),
},
Message::WindowCloseRequested => { Message::WindowCloseRequested => {
self.persist_project_ui_state(); self.persist_project_ui_state();
flush_embeddings_and_exit(std::process::exit) flush_embeddings_and_exit(std::process::exit)
@@ -3869,7 +3874,11 @@ impl BdsApp {
) && (action != MenuAction::DisconnectServer || self.remote_client.is_some()) ) && (action != MenuAction::DisconnectServer || self.remote_client.is_some())
}); });
native_edit::native_edit(content, Arc::clone(&self.native_edit_commands)).into() crate::components::keyboard::scope(native_edit::native_edit(
content,
Arc::clone(&self.native_edit_commands),
))
.into()
} }
pub fn subscription(&self) -> Subscription<Message> { pub fn subscription(&self) -> Subscription<Message> {
@@ -3894,6 +3903,15 @@ impl BdsApp {
_ => None, _ => None,
}); });
let window_close_sub = window::close_requests().map(|_| Message::WindowCloseRequested); let window_close_sub = window::close_requests().map(|_| Message::WindowCloseRequested);
let keyboard_navigation_sub = iced::event::listen_with(|event, status, _id| {
let iced::Event::Keyboard(iced::keyboard::Event::KeyPressed { key, modifiers, .. }) =
event
else {
return None;
};
crate::components::keyboard::tab_navigation(&key, modifiers, status)
.map(Message::KeyboardNavigation)
});
// Global mouse tracking for sidebar resize dragging. // Global mouse tracking for sidebar resize dragging.
// The 4px drag handle mouse_area only fires on_press; move/release // The 4px drag handle mouse_area only fires on_press; move/release
@@ -3950,6 +3968,7 @@ impl BdsApp {
toast_tick, toast_tick,
file_drop_sub, file_drop_sub,
window_close_sub, window_close_sub,
keyboard_navigation_sub,
drag_sub, drag_sub,
menu_interaction_sub, menu_interaction_sub,
menu_expand_tick, menu_expand_tick,

View File

@@ -5,6 +5,8 @@ use iced::widget::{
}; };
use iced::{Alignment, Background, Border, Color, Element, Length, Shadow, Theme, Vector}; use iced::{Alignment, Background, Border, Color, Element, Length, Shadow, Theme, Vector};
use super::keyboard;
/// Standard form field label color. /// Standard form field label color.
pub const LABEL_COLOR: Color = rgb8(0xB5, 0xBA, 0xC4); pub const LABEL_COLOR: Color = rgb8(0xB5, 0xBA, 0xC4);
pub const SECTION_COLOR: Color = rgb8(0x9D, 0xA5, 0xB4); pub const SECTION_COLOR: Color = rgb8(0x9D, 0xA5, 0xB4);
@@ -243,10 +245,13 @@ where
.size(12) .size(12)
.color(LABEL_COLOR) .color(LABEL_COLOR)
.shaping(Shaping::Advanced), .shaping(Shaping::Advanced),
keyboard::focusable(
pick_list(list, selected.cloned(), on_select) pick_list(list, selected.cloned(), on_select)
.padding([8, 10]) .padding([8, 10])
.width(Length::Fill) .width(Length::Fill)
.style(select_style), .style(select_style),
true,
),
] ]
.spacing(6) .spacing(6)
.width(Length::Fill) .width(Length::Fill)
@@ -259,10 +264,13 @@ pub fn labeled_checkbox<'a, Message: Clone + 'a>(
is_checked: bool, is_checked: bool,
on_toggle: impl Fn(bool) -> Message + 'a, on_toggle: impl Fn(bool) -> Message + 'a,
) -> Element<'a, Message> { ) -> Element<'a, Message> {
keyboard::focusable(
checkbox(label, is_checked) checkbox(label, is_checked)
.on_toggle(on_toggle) .on_toggle(on_toggle)
.size(16) .size(16)
.text_size(14) .text_size(14),
true,
)
.into() .into()
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,5 @@
pub mod inputs; pub mod inputs;
pub mod keyboard;
pub mod native_edit; pub mod native_edit;
pub mod popover; pub mod popover;
pub mod webview; pub mod webview;

View File

@@ -1,7 +1,7 @@
use std::collections::HashMap; use std::collections::HashMap;
use iced::widget::{ use iced::widget::{
Column, button, column, container, horizontal_space, mouse_area, row, scrollable, text, Column, column, container, horizontal_space, mouse_area, row, scrollable, text,
}; };
use iced::{ use iced::{
Alignment, Background, Border, Color, Element, Length, Shadow, Subscription, Theme, Vector, Alignment, Background, Border, Color, Element, Length, Shadow, Subscription, Theme, Vector,
@@ -10,7 +10,7 @@ use muda::accelerator::{Accelerator, CMD_OR_CTRL, Code, Modifiers};
use muda::{Menu, MenuEvent, MenuId, MenuItem, PredefinedMenuItem, Submenu}; use muda::{Menu, MenuEvent, MenuId, MenuItem, PredefinedMenuItem, Submenu};
use crate::app::Message; use crate::app::Message;
use crate::components::{inputs, popover}; use crate::components::{inputs, keyboard, popover};
use crate::state::tabs::TabType; use crate::state::tabs::TabType;
use bds_core::i18n::{UiLocale, translate}; use bds_core::i18n::{UiLocale, translate};
@@ -640,9 +640,12 @@ fn window_menu_popup<'a>(
] ]
.align_y(Alignment::Center) .align_y(Alignment::Center)
.spacing(16); .spacing(16);
let mut item = button(content).padding([6, 8]).width(Length::Fill).style( let mut item = keyboard::button(content)
move |theme, status| menu_item_style(selected == Some(action), theme, status), .padding([6, 8])
); .width(Length::Fill)
.style(move |theme, status| {
menu_item_style(selected == Some(action), theme, status)
});
if is_enabled { if is_enabled {
item = item.on_press(Message::WindowMenu(WindowMenuEvent::Action(action))); item = item.on_press(Message::WindowMenu(WindowMenuEvent::Action(action)));
} }
@@ -692,7 +695,7 @@ pub fn window_menu_view<'a>(
label label
}; };
let trigger = mouse_area( let trigger = mouse_area(
button(text(label).size(12)) keyboard::button(text(label).size(12))
.padding([5, 8]) .padding([5, 8])
.style(move |theme, status| menu_button_style(active, theme, status)) .style(move |theme, status| menu_button_style(active, theme, status))
.on_press(Message::WindowMenu(WindowMenuEvent::Toggle( .on_press(Message::WindowMenu(WindowMenuEvent::Toggle(

View File

@@ -1,5 +1,7 @@
use crate::components::keyboard;
use iced::widget::button;
use iced::widget::text::Shaping; use iced::widget::text::Shaping;
use iced::widget::{Column, Space, button, column, container, svg, text, tooltip}; use iced::widget::{Column, Space, column, container, svg, text, tooltip};
use iced::{Background, Border, Color, Element, Length, Theme}; use iced::{Background, Border, Color, Element, Length, Theme};
use bds_core::i18n::UiLocale; use bds_core::i18n::UiLocale;
@@ -28,6 +30,21 @@ fn icon_svg(view: SidebarView) -> &'static [u8] {
} }
} }
pub fn access_key(view: SidebarView) -> char {
match view {
SidebarView::Posts => '1',
SidebarView::Pages => '2',
SidebarView::Media => '3',
SidebarView::Scripts => '4',
SidebarView::Templates => '5',
SidebarView::Tags => '6',
SidebarView::Chat => '7',
SidebarView::Import => '8',
SidebarView::Git => '9',
SidebarView::Settings => '0',
}
}
/// Top group of activity items. /// Top group of activity items.
const TOP_ACTIVITIES: &[SidebarView] = &[ const TOP_ACTIVITIES: &[SidebarView] = &[
SidebarView::Posts, SidebarView::Posts,
@@ -108,7 +125,7 @@ pub fn view(
.height(Length::Fixed(24.0)) .height(Length::Fixed(24.0))
.opacity(if is_active { 1.0_f32 } else { 0.4_f32 }); .opacity(if is_active { 1.0_f32 } else { 0.4_f32 });
let btn = button( let btn = iced::widget::button(
container(icon) container(icon)
.center_x(Length::Fixed(48.0)) .center_x(Length::Fixed(48.0))
.center_y(Length::Fixed(48.0)), .center_y(Length::Fixed(48.0)),
@@ -140,13 +157,18 @@ pub fn view(
// Wrap in tooltip per layout.allium ActivityButton.label_key // Wrap in tooltip per layout.allium ActivityButton.label_key
let tip_text = t(locale, view.i18n_key()); let tip_text = t(locale, view.i18n_key());
keyboard::focusable(
tooltip( tooltip(
btn_row, btn_row,
text(tip_text).size(12).shaping(Shaping::Advanced), text(tip_text).size(12).shaping(Shaping::Advanced),
tooltip::Position::Right, tooltip::Position::Right,
) )
.gap(4) .gap(4)
.style(inputs::tooltip_style) .style(inputs::tooltip_style),
true,
)
.hotkey(access_key(view))
.hint_position(keyboard::HintPosition::BottomTrailing)
.into() .into()
}; };

View File

@@ -1,3 +1,4 @@
use crate::components::keyboard;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::f32::consts::{FRAC_PI_2, TAU}; use std::f32::consts::{FRAC_PI_2, TAU};
@@ -7,9 +8,7 @@ use bds_core::engine::chat_surfaces::{
}; };
use bds_core::i18n::UiLocale; use bds_core::i18n::UiLocale;
use iced::widget::canvas::{self, Path, Stroke, path}; use iced::widget::canvas::{self, Path, Stroke, path};
use iced::widget::{ use iced::widget::{Space, checkbox, column, container, row, scrollable, text, text_editor};
Space, button, checkbox, column, container, row, scrollable, text, text_editor,
};
use iced::{ use iced::{
Alignment, Color, Element, Length, Point, Radians, Rectangle, Renderer, Size, Theme, mouse, Alignment, Color, Element, Length, Point, Radians, Rectangle, Renderer, Size, Theme, mouse,
}; };
@@ -76,7 +75,7 @@ fn surface_view<'a>(
} }
if dismissible { if dismissible {
header = header.push( header = header.push(
button(text(t(locale, "chat.surface.dismiss"))) keyboard::button(text(t(locale, "chat.surface.dismiss")))
.on_press(Message::ChatSurfaceDismissed(surface.id.clone())) .on_press(Message::ChatSurfaceDismissed(surface.id.clone()))
.padding([4, 8]) .padding([4, 8])
.style(inputs::secondary_button), .style(inputs::secondary_button),
@@ -113,7 +112,7 @@ fn surface_content<'a>(
.iter() .iter()
.fold(row![].spacing(8), |actions, item| { .fold(row![].spacing(8), |actions, item| {
actions.push( actions.push(
button(text(item.label.clone())) keyboard::button(text(item.label.clone()))
.on_press(Message::ChatSurfaceAction { .on_press(Message::ChatSurfaceAction {
surface_id: surface.id.clone(), surface_id: surface.id.clone(),
action: item.action.clone(), action: item.action.clone(),
@@ -196,14 +195,17 @@ fn form<'a>(
let surface_id = surface.id.clone(); let surface_id = surface.id.clone();
let key = field.key.clone(); let key = field.key.clone();
let control: Element<'a, Message> = match field.input_type { let control: Element<'a, Message> = match field.input_type {
FormInputType::Checkbox => checkbox(label, field.value.as_bool().unwrap_or(false)) FormInputType::Checkbox => keyboard::focusable(
checkbox(label, field.value.as_bool().unwrap_or(false))
.on_toggle(move |value| Message::ChatSurfaceFieldChanged { .on_toggle(move |value| Message::ChatSurfaceFieldChanged {
surface_id: surface_id.clone(), surface_id: surface_id.clone(),
field: key.clone(), field: key.clone(),
value: value.into(), value: value.into(),
}) })
.size(16) .size(16)
.text_size(13) .text_size(13),
true,
)
.into(), .into(),
FormInputType::Select => { FormInputType::Select => {
let selected = field.options.iter().find(|option| { let selected = field.options.iter().find(|option| {
@@ -278,7 +280,7 @@ fn form<'a>(
} }
if let Some(action) = &surface.submit_action { if let Some(action) = &surface.submit_action {
children.push( children.push(
button(text( keyboard::button(text(
surface surface
.submit_label .submit_label
.clone() .clone()
@@ -437,7 +439,7 @@ fn tabs<'a>(
.enumerate() .enumerate()
.fold(row![].spacing(6), |controls, (index, tab)| { .fold(row![].spacing(6), |controls, (index, tab)| {
controls.push( controls.push(
button(text(tab.label.clone())) keyboard::button(text(tab.label.clone()))
.on_press(Message::ChatSurfaceTabSelected { .on_press(Message::ChatSurfaceTabSelected {
surface_id: surface.id.clone(), surface_id: surface.id.clone(),
index, index,

View File

@@ -1,3 +1,4 @@
use crate::components::keyboard;
use std::collections::HashMap; use std::collections::HashMap;
use std::time::Instant; use std::time::Instant;
@@ -8,7 +9,7 @@ use bds_core::i18n::UiLocale;
use bds_core::model::{ChatConversation, ChatMessage, ChatRole}; use bds_core::model::{ChatConversation, ChatMessage, ChatRole};
use iced::widget::text::Shaping; use iced::widget::text::Shaping;
use iced::widget::{ use iced::widget::{
Space, button, column, container, markdown, row, scrollable, text, text_editor, text_input, Space, column, container, markdown, row, scrollable, text, text_editor, text_input,
}; };
use iced::{Alignment, Color, Element, Length}; use iced::{Alignment, Color, Element, Length};
@@ -212,7 +213,7 @@ pub fn view<'a>(
text(t(locale, "chat.unavailable.guidance")) text(t(locale, "chat.unavailable.guidance"))
.size(13) .size(13)
.color(inputs::SECTION_COLOR), .color(inputs::SECTION_COLOR),
button(text(t(locale, "chat.unavailable.openSettings"))) keyboard::button(text(t(locale, "chat.unavailable.openSettings")))
.on_press(Message::OpenSettingsSection( .on_press(Message::OpenSettingsSection(
crate::views::settings_view::SettingsSection::AI, crate::views::settings_view::SettingsSection::AI,
)) ))
@@ -254,11 +255,11 @@ pub fn view<'a>(
.size(18) .size(18)
.padding([7, 9]) .padding([7, 9])
.style(inputs::field_style), .style(inputs::field_style),
button(text(t(locale, "chat.rename.action"))) keyboard::button(text(t(locale, "chat.rename.action")))
.on_press(Message::ChatRename) .on_press(Message::ChatRename)
.padding([8, 12]) .padding([8, 12])
.style(inputs::secondary_button), .style(inputs::secondary_button),
button(text(t(locale, "common.delete"))) keyboard::button(text(t(locale, "common.delete")))
.on_press(Message::ChatDelete(state.conversation.id.clone())) .on_press(Message::ChatDelete(state.conversation.id.clone()))
.padding([8, 12]) .padding([8, 12])
.style(inputs::danger_button), .style(inputs::danger_button),
@@ -349,7 +350,8 @@ pub fn view<'a>(
} else { } else {
t(locale, "chat.send") t(locale, "chat.send")
}; };
let mut send_button = button(text(send_label)) let mut send_button =
keyboard::button(text(send_label))
.padding([8, 16]) .padding([8, 16])
.style(if state.streaming { .style(if state.streaming {
inputs::danger_button inputs::danger_button

View File

@@ -1,4 +1,5 @@
use iced::widget::{Space, button, column, container, row, scrollable, text, tooltip}; use crate::components::keyboard;
use iced::widget::{Space, column, container, row, scrollable, text, tooltip};
use iced::{Alignment, Background, Color, Element, Length, Theme}; use iced::{Alignment, Background, Color, Element, Length, Theme};
use bds_core::i18n::UiLocale; use bds_core::i18n::UiLocale;
@@ -339,7 +340,7 @@ fn recent_posts<'a>(posts: &'a [DashboardRecentPost], locale: UiLocale) -> Eleme
.map(|post| { .map(|post| {
container( container(
row![ row![
button( keyboard::button(
column![ column![
text(post.title.clone()).size(14).color(Color::WHITE), text(post.title.clone()).size(14).color(Color::WHITE),
text(post.date.clone()) text(post.date.clone())
@@ -357,7 +358,7 @@ fn recent_posts<'a>(posts: &'a [DashboardRecentPost], locale: UiLocale) -> Eleme
})) }))
.width(Length::Fill), .width(Length::Fill),
status_badge(&post.status), status_badge(&post.status),
button(text(t(locale, "dashboard.pin")).size(12)) keyboard::button(text(t(locale, "dashboard.pin")).size(12))
.on_press(Message::OpenTab(Tab { .on_press(Message::OpenTab(Tab {
id: post.post_id.clone(), id: post.post_id.clone(),
tab_type: TabType::Post, tab_type: TabType::Post,

View File

@@ -1,3 +1,4 @@
use crate::components::keyboard;
use std::collections::{HashMap, hash_map::DefaultHasher}; use std::collections::{HashMap, hash_map::DefaultHasher};
use std::fs; use std::fs;
use std::hash::{Hash, Hasher}; use std::hash::{Hash, Hasher};
@@ -5,7 +6,7 @@ use std::path::{Path, PathBuf};
use std::time::{Duration, Instant, UNIX_EPOCH}; use std::time::{Duration, Instant, UNIX_EPOCH};
use bds_core::i18n::UiLocale; use bds_core::i18n::UiLocale;
use iced::widget::{button, column, container, markdown, row, scrollable, text}; use iced::widget::{column, container, markdown, row, scrollable, text};
use iced::{Element, Length}; use iced::{Element, Length};
use crate::app::Message; use crate::app::Message;
@@ -170,7 +171,7 @@ pub fn view(state: &DocumentationState, locale: UiLocale) -> Element<'_, Message
.into(), .into(),
], ],
vec![ vec![
button(text(t(locale, "common.refresh")).size(13)) keyboard::button(text(t(locale, "common.refresh")).size(13))
.on_press(Message::DocumentationRefresh(state.kind)) .on_press(Message::DocumentationRefresh(state.kind))
.padding([6, 16]) .padding([6, 16])
.style(inputs::secondary_button) .style(inputs::secondary_button)

View File

@@ -1,9 +1,10 @@
use crate::components::keyboard;
use std::collections::HashSet; use std::collections::HashSet;
use bds_core::engine::embedding::DuplicateSearchResult; use bds_core::engine::embedding::DuplicateSearchResult;
use bds_core::i18n::UiLocale; use bds_core::i18n::UiLocale;
use iced::widget::text::Shaping; use iced::widget::text::Shaping;
use iced::widget::{Space, button, checkbox, column, container, row, scrollable, text}; use iced::widget::{Space, checkbox, column, container, row, scrollable, text};
use iced::{Color, Element, Length}; use iced::{Color, Element, Length};
use crate::app::Message; use crate::app::Message;
@@ -23,19 +24,20 @@ pub struct DuplicatesState {
pub fn view(state: &DuplicatesState, locale: UiLocale) -> Element<'_, Message> { pub fn view(state: &DuplicatesState, locale: UiLocale) -> Element<'_, Message> {
let refresh = if state.is_loading { let refresh = if state.is_loading {
button(text(t(locale, "duplicates.searching")).size(13)).style(inputs::secondary_button) keyboard::button(text(t(locale, "duplicates.searching")).size(13))
.style(inputs::secondary_button)
} else { } else {
button(text(t(locale, "common.refresh")).size(13)) keyboard::button(text(t(locale, "common.refresh")).size(13))
.on_press(Message::DuplicatesRefresh) .on_press(Message::DuplicatesRefresh)
.style(inputs::secondary_button) .style(inputs::secondary_button)
} }
.padding([6, 16]); .padding([6, 16]);
let dismiss_checked = if state.selected.is_empty() || state.is_loading { let dismiss_checked = if state.selected.is_empty() || state.is_loading {
button(text(t(locale, "duplicates.dismissChecked")).size(13)) keyboard::button(text(t(locale, "duplicates.dismissChecked")).size(13))
.style(inputs::secondary_button) .style(inputs::secondary_button)
} else { } else {
button( keyboard::button(
text(tw( text(tw(
locale, locale,
"duplicates.dismissCheckedCount", "duplicates.dismissCheckedCount",
@@ -64,12 +66,12 @@ pub fn view(state: &DuplicatesState, locale: UiLocale) -> Element<'_, Message> {
.into(), .into(),
], ],
vec![ vec![
button(text(t(locale, "duplicates.checkAll")).size(13)) keyboard::button(text(t(locale, "duplicates.checkAll")).size(13))
.on_press(Message::DuplicatesCheckAll) .on_press(Message::DuplicatesCheckAll)
.padding([6, 12]) .padding([6, 12])
.style(inputs::secondary_button) .style(inputs::secondary_button)
.into(), .into(),
button(text(t(locale, "duplicates.uncheckAll")).size(13)) keyboard::button(text(t(locale, "duplicates.uncheckAll")).size(13))
.on_press(Message::DuplicatesUncheckAll) .on_press(Message::DuplicatesUncheckAll)
.padding([6, 12]) .padding([6, 12])
.style(inputs::secondary_button) .style(inputs::secondary_button)
@@ -119,6 +121,7 @@ pub fn view(state: &DuplicatesState, locale: UiLocale) -> Element<'_, Message> {
}; };
pairs = pairs.push(inputs::card( pairs = pairs.push(inputs::card(
row![ row![
keyboard::focusable(
checkbox("", checked) checkbox("", checked)
.on_toggle({ .on_toggle({
let a = pair.post_id_a.clone(); let a = pair.post_id_a.clone();
@@ -126,7 +129,9 @@ pub fn view(state: &DuplicatesState, locale: UiLocale) -> Element<'_, Message> {
move |_| Message::DuplicatesToggle(a.clone(), b.clone()) move |_| Message::DuplicatesToggle(a.clone(), b.clone())
}) })
.size(16), .size(16),
button( true,
),
keyboard::button(
text(pair.title_a.clone()) text(pair.title_a.clone())
.size(13) .size(13)
.shaping(Shaping::Advanced) .shaping(Shaping::Advanced)
@@ -135,7 +140,7 @@ pub fn view(state: &DuplicatesState, locale: UiLocale) -> Element<'_, Message> {
.padding([5, 8]) .padding([5, 8])
.style(inputs::disclosure_button), .style(inputs::disclosure_button),
text("").size(14).color(inputs::LABEL_COLOR), text("").size(14).color(inputs::LABEL_COLOR),
button( keyboard::button(
text(pair.title_b.clone()) text(pair.title_b.clone())
.size(13) .size(13)
.shaping(Shaping::Advanced) .shaping(Shaping::Advanced)
@@ -149,7 +154,7 @@ pub fn view(state: &DuplicatesState, locale: UiLocale) -> Element<'_, Message> {
} else { } else {
Color::from_rgb(0.55, 0.76, 0.92) Color::from_rgb(0.55, 0.76, 0.92)
}), }),
button(text(t(locale, "duplicates.dismiss")).size(12)) keyboard::button(text(t(locale, "duplicates.dismiss")).size(12))
.on_press(Message::DuplicatesDismiss( .on_press(Message::DuplicatesDismiss(
pair.post_id_a.clone(), pair.post_id_a.clone(),
pair.post_id_b.clone() pair.post_id_b.clone()
@@ -163,7 +168,7 @@ pub fn view(state: &DuplicatesState, locale: UiLocale) -> Element<'_, Message> {
} }
if state.result.has_more { if state.result.has_more {
pairs = pairs.push( pairs = pairs.push(
button(text(t(locale, "duplicates.showMore")).size(13)) keyboard::button(text(t(locale, "duplicates.showMore")).size(13))
.on_press(Message::DuplicatesShowMore) .on_press(Message::DuplicatesShowMore)
.padding([7, 16]) .padding([7, 16])
.style(inputs::secondary_button), .style(inputs::secondary_button),

View File

@@ -1,3 +1,4 @@
use crate::components::keyboard;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use bds_core::engine::git::{ use bds_core::engine::git::{
@@ -6,7 +7,7 @@ use bds_core::engine::git::{
}; };
use bds_core::i18n::UiLocale; use bds_core::i18n::UiLocale;
use iced::widget::text::{Shaping, Wrapping}; use iced::widget::text::{Shaping, Wrapping};
use iced::widget::{Space, button, column, container, row, scrollable, text, text_input, tooltip}; use iced::widget::{Space, column, container, row, scrollable, text, text_input, tooltip};
use iced::{Alignment, Background, Color, Element, Font, Length}; use iced::{Alignment, Background, Color, Element, Font, Length};
use crate::app::Message; use crate::app::Message;
@@ -152,7 +153,7 @@ pub fn sidebar_view(
text(t(locale, "git.notRepository")) text(t(locale, "git.notRepository"))
.size(12) .size(12)
.color(Color::from_rgb(0.6, 0.6, 0.65)), .color(Color::from_rgb(0.6, 0.6, 0.65)),
button(text(t(locale, "git.initialize")).size(12)) keyboard::button(text(t(locale, "git.initialize")).size(12))
.on_press(Message::GitInitialize) .on_press(Message::GitInitialize)
.padding([5, 8]) .padding([5, 8])
.style(inputs::primary_button), .style(inputs::primary_button),
@@ -180,7 +181,7 @@ pub fn sidebar_view(
let network_running = state.network_run.is_some(); let network_running = state.network_run.is_some();
let network_button = let network_button =
|key: &'static str, icon: &'static str, message: Message| -> Element<'static, Message> { |key: &'static str, icon: &'static str, message: Message| -> Element<'static, Message> {
let mut control = button(text(icon).size(16).shaping(Shaping::Advanced)) let mut control = keyboard::button(text(icon).size(16).shaping(Shaping::Advanced))
.width(Length::Fixed(30.0)) .width(Length::Fixed(30.0))
.height(Length::Fixed(28.0)) .height(Length::Fixed(28.0))
.padding(0) .padding(0)
@@ -189,13 +190,16 @@ pub fn sidebar_view(
control = control.on_press(message); control = control.on_press(message);
} }
keyboard::focusable(
tooltip( tooltip(
control, control,
text(t(locale, key)).size(12), text(t(locale, key)).size(12),
tooltip::Position::Bottom, tooltip::Position::Bottom,
) )
.gap(4) .gap(4)
.style(inputs::tooltip_style) .style(inputs::tooltip_style),
!offline_mode && !network_running,
)
.into() .into()
}; };
let actions = row![ let actions = row![
@@ -240,7 +244,7 @@ pub fn sidebar_view(
.padding([5, 7]) .padding([5, 7])
.style(inputs::field_style), .style(inputs::field_style),
{ {
let commit = button(text(t(locale, "git.commit")).size(11)) let commit = keyboard::button(text(t(locale, "git.commit")).size(11))
.padding([5, 7]) .padding([5, 7])
.style(inputs::primary_button); .style(inputs::primary_button);
if state.files.is_empty() || state.commit_message.trim().is_empty() { if state.files.is_empty() || state.commit_message.trim().is_empty() {
@@ -265,7 +269,7 @@ pub fn sidebar_view(
content.extend(state.history.iter().take(20).map(history_button)); content.extend(state.history.iter().take(20).map(history_button));
} }
content.push( content.push(
button(text(t(locale, "git.pruneLfs")).size(11)) keyboard::button(text(t(locale, "git.pruneLfs")).size(11))
.on_press(Message::GitPruneLfs) .on_press(Message::GitPruneLfs)
.padding([4, 7]) .padding([4, 7])
.style(inputs::secondary_button) .style(inputs::secondary_button)
@@ -303,7 +307,7 @@ fn error_view(error: Option<&str>) -> Element<'static, Message> {
fn status_button(file: &GitFileStatus) -> Element<'static, Message> { fn status_button(file: &GitFileStatus) -> Element<'static, Message> {
let path = file.path.clone(); let path = file.path.clone();
button( keyboard::button(
row![ row![
text(path.clone()).size(11), text(path.clone()).size(11),
Space::with_width(Length::Fill), Space::with_width(Length::Fill),
@@ -322,7 +326,7 @@ fn history_button(commit: &GitCommit) -> Element<'static, Message> {
let hash = commit.hash.clone(); let hash = commit.hash.clone();
let subject = commit.subject.clone().unwrap_or_else(|| hash.clone()); let subject = commit.subject.clone().unwrap_or_else(|| hash.clone());
let short = hash.chars().take(7).collect::<String>(); let short = hash.chars().take(7).collect::<String>();
button( keyboard::button(
column![ column![
text(subject.clone()).size(11), text(subject.clone()).size(11),
row![ row![
@@ -399,7 +403,7 @@ fn network_output(run: &GitNetworkRunState, locale: UiLocale) -> Element<'static
.wrapping(Wrapping::Word) .wrapping(Wrapping::Word)
) )
.padding(6), .padding(6),
button(text(t(locale, "common.cancel")).size(11)) keyboard::button(text(t(locale, "common.cancel")).size(11))
.on_press(Message::CancelTask( .on_press(Message::CancelTask(
crate::state::navigation::TaskSource::Local, crate::state::navigation::TaskSource::Local,
run.task_id, run.task_id,
@@ -445,7 +449,7 @@ pub fn diff_view(
.iter() .iter()
.map(|change| { .map(|change| {
let selected = state.selected_path.as_deref() == Some(&change.path); let selected = state.selected_path.as_deref() == Some(&change.path);
let button = button(text(change.path.clone()).size(11)) let button = keyboard::button(text(change.path.clone()).size(11))
.padding([4, 7]) .padding([4, 7])
.style(if selected { .style(if selected {
inputs::primary_button inputs::primary_button

View File

@@ -1,3 +1,4 @@
use crate::components::keyboard;
use std::collections::HashSet; use std::collections::HashSet;
use bds_core::i18n::UiLocale; use bds_core::i18n::UiLocale;
@@ -5,7 +6,7 @@ use bds_core::model::{
ImportCandidate, ImportDefinition, ImportExecutionResult, ImportItemKind, ImportItemStatus, ImportCandidate, ImportDefinition, ImportExecutionResult, ImportItemKind, ImportItemStatus,
ImportPhase, ImportProgress, ImportReport, ImportResolution, TaxonomyKind, ImportPhase, ImportProgress, ImportReport, ImportResolution, TaxonomyKind,
}; };
use iced::widget::{Space, button, column, container, progress_bar, row, scrollable, text}; use iced::widget::{Space, column, container, progress_bar, row, scrollable, text};
use iced::{Alignment, Color, Element, Length}; use iced::{Alignment, Color, Element, Length};
use crate::app::Message; use crate::app::Message;
@@ -50,6 +51,25 @@ impl ImportEditorState {
error: None, error: None,
} }
} }
pub fn section_is_visible(&self, section: ImportSection) -> bool {
let Some(report) = &self.report else {
return false;
};
match section {
ImportSection::Conflicts => report
.posts
.iter()
.chain(&report.pages)
.chain(&report.media)
.any(|item| item.status == ImportItemStatus::Conflict),
ImportSection::Posts => !report.posts.is_empty(),
ImportSection::Pages => !report.pages.is_empty(),
ImportSection::Media => !report.media.is_empty(),
ImportSection::Taxonomy => !report.taxonomies.is_empty(),
ImportSection::Macros => !report.macros.is_empty(),
}
}
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
@@ -62,6 +82,32 @@ pub enum ImportSection {
Macros, Macros,
} }
impl ImportSection {
pub fn access_key(self) -> char {
match self {
Self::Conflicts => 'c',
Self::Posts => 'o',
Self::Pages => 'a',
Self::Media => 'm',
Self::Taxonomy => 't',
Self::Macros => 'r',
}
}
pub fn from_access_key(key: char) -> Option<Self> {
[
Self::Conflicts,
Self::Posts,
Self::Pages,
Self::Media,
Self::Taxonomy,
Self::Macros,
]
.into_iter()
.find(|section| section.access_key() == key)
}
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum ImportEditorMsg { pub enum ImportEditorMsg {
NameChanged(String), NameChanged(String),
@@ -120,13 +166,13 @@ pub fn view<'a>(state: &'a ImportEditorState, locale: UiLocale) -> Element<'a, M
column![ column![
row![ row![
name, name,
button(text(t(locale, "modal.confirmDelete.delete"))) keyboard::button(text(t(locale, "modal.confirmDelete.delete")))
.on_press_maybe( .on_press_maybe(
(!busy).then_some(Message::ImportEditor(ImportEditorMsg::DeleteRequested,)) (!busy).then_some(Message::ImportEditor(ImportEditorMsg::DeleteRequested,))
) )
.padding([8, 12]) .padding([8, 12])
.style(inputs::danger_button), .style(inputs::danger_button),
button(text(t(locale, "import.analyze"))) keyboard::button(text(t(locale, "import.analyze")))
.on_press_maybe( .on_press_maybe(
(!busy && state.definition.wxr_file_path.is_some()) (!busy && state.definition.wxr_file_path.is_some())
.then_some(Message::ImportEditor(ImportEditorMsg::Analyze),) .then_some(Message::ImportEditor(ImportEditorMsg::Analyze),)
@@ -290,7 +336,7 @@ fn path_row<'a>(
] ]
.spacing(5) .spacing(5)
.width(Length::Fill), .width(Length::Fill),
button(text(t(locale, "common.open"))) keyboard::button(text(t(locale, "common.open")))
.on_press(Message::ImportEditor(action)) .on_press(Message::ImportEditor(action))
.padding([7, 12]) .padding([7, 12])
.style(inputs::secondary_button), .style(inputs::secondary_button),
@@ -457,7 +503,7 @@ fn execute_toolbar<'a>(
.into(), .into(),
], ],
vec![ vec![
button(text(t(locale, "import.autoMap"))) keyboard::button(text(t(locale, "import.autoMap")))
.on_press_maybe( .on_press_maybe(
(!state.is_analyzing && !state.is_executing) (!state.is_analyzing && !state.is_executing)
.then_some(Message::ImportEditor(ImportEditorMsg::AutoMapTaxonomy)), .then_some(Message::ImportEditor(ImportEditorMsg::AutoMapTaxonomy)),
@@ -465,7 +511,7 @@ fn execute_toolbar<'a>(
.padding([8, 12]) .padding([8, 12])
.style(inputs::secondary_button) .style(inputs::secondary_button)
.into(), .into(),
button(text(tw( keyboard::button(text(tw(
locale, locale,
"import.execute", "import.execute",
&[("count", &count.to_string())], &[("count", &count.to_string())],
@@ -514,7 +560,7 @@ fn section<'a>(
) -> Vec<Element<'a, Message>> { ) -> Vec<Element<'a, Message>> {
let expanded = state.expanded.contains(&section); let expanded = state.expanded.contains(&section);
let header = inputs::card( let header = inputs::card(
button( keyboard::button(
row![ row![
text(if expanded { "" } else { "" }).size(12), text(if expanded { "" } else { "" }).size(12),
text(t(locale, title_key)).size(13), text(t(locale, title_key)).size(13),
@@ -524,6 +570,7 @@ fn section<'a>(
.on_press(Message::ImportEditor(ImportEditorMsg::ToggleSection( .on_press(Message::ImportEditor(ImportEditorMsg::ToggleSection(
section, section,
))) )))
.hotkey(section.access_key())
.padding([6, 8]) .padding([6, 8])
.width(Length::Fill) .width(Length::Fill)
.style(inputs::disclosure_button), .style(inputs::disclosure_button),
@@ -673,7 +720,7 @@ fn taxonomy_rows<'a>(
}), }),
)) ))
.width(Length::Fixed(260.0)), .width(Length::Fixed(260.0)),
button(text(t(locale, "common.clear"))) keyboard::button(text(t(locale, "common.clear")))
.on_press_maybe(item.mapped_to.is_some().then_some(Message::ImportEditor( .on_press_maybe(item.mapped_to.is_some().then_some(Message::ImportEditor(
ImportEditorMsg::SetTaxonomyMapping { ImportEditorMsg::SetTaxonomyMapping {
kind, kind,

View File

@@ -1,8 +1,9 @@
use crate::components::keyboard;
use std::collections::HashMap; use std::collections::HashMap;
use std::path::Path; use std::path::Path;
use iced::widget::text::Shaping; use iced::widget::text::Shaping;
use iced::widget::{Space, button, column, container, image, row, scrollable, text}; use iced::widget::{Space, column, container, image, row, scrollable, text};
use iced::{Color, Element, Length}; use iced::{Color, Element, Length};
use bds_core::i18n::{self, UiLocale}; use bds_core::i18n::{self, UiLocale};
@@ -271,7 +272,7 @@ pub fn view<'a>(
.style(status_bar::dropdown_bg) .style(status_bar::dropdown_bg)
.into(); .into();
let quick_actions_button: Element<'a, Message> = let quick_actions_button: Element<'a, Message> =
button(text(t(locale, "editor.quickActions")).size(13)) keyboard::button(text(t(locale, "editor.quickActions")).size(13))
.on_press_maybe( .on_press_maybe(
ai_enabled.then_some(Message::MediaEditor(MediaEditorMsg::ToggleQuickActions)), ai_enabled.then_some(Message::MediaEditor(MediaEditorMsg::ToggleQuickActions)),
) )
@@ -290,17 +291,17 @@ pub fn view<'a>(
vec![text(state.original_name.clone()).size(18).into()], vec![text(state.original_name.clone()).size(18).into()],
vec![ vec![
quick_actions, quick_actions,
button(text(t(locale, "editor.replaceFile")).size(13)) keyboard::button(text(t(locale, "editor.replaceFile")).size(13))
.on_press(Message::MediaEditor(MediaEditorMsg::ReplaceFile)) .on_press(Message::MediaEditor(MediaEditorMsg::ReplaceFile))
.style(inputs::secondary_button) .style(inputs::secondary_button)
.padding([6, 16]) .padding([6, 16])
.into(), .into(),
button(text(t(locale, "common.save")).size(13)) keyboard::button(text(t(locale, "common.save")).size(13))
.on_press(Message::MediaEditor(MediaEditorMsg::Save)) .on_press(Message::MediaEditor(MediaEditorMsg::Save))
.style(inputs::primary_button) .style(inputs::primary_button)
.padding([6, 16]) .padding([6, 16])
.into(), .into(),
button(text(t(locale, "modal.confirmDelete.delete")).size(13)) keyboard::button(text(t(locale, "modal.confirmDelete.delete")).size(13))
.on_press(Message::MediaEditor(MediaEditorMsg::Delete)) .on_press(Message::MediaEditor(MediaEditorMsg::Delete))
.style(inputs::danger_button) .style(inputs::danger_button)
.padding([6, 16]) .padding([6, 16])
@@ -324,7 +325,7 @@ pub fn view<'a>(
} else { } else {
Color::from_rgb(0.55, 0.58, 0.65) Color::from_rgb(0.55, 0.58, 0.65)
}; };
button(text(label).size(12).shaping(Shaping::Advanced).color(color)) keyboard::button(text(label).size(12).shaping(Shaping::Advanced).color(color))
.on_press(Message::MediaEditor(MediaEditorMsg::SwitchLanguage( .on_press(Message::MediaEditor(MediaEditorMsg::SwitchLanguage(
flag.language.clone(), flag.language.clone(),
))) )))
@@ -430,7 +431,7 @@ pub fn view<'a>(
.size(12) .size(12)
.color(Color::from_rgb(0.55, 0.58, 0.65)), .color(Color::from_rgb(0.55, 0.58, 0.65)),
Space::with_width(Length::Fill), Space::with_width(Length::Fill),
button(text(t(locale, "editor.linkToPost")).size(12)) keyboard::button(text(t(locale, "editor.linkToPost")).size(12))
.on_press(Message::MediaEditor(MediaEditorMsg::TogglePostPicker)) .on_press(Message::MediaEditor(MediaEditorMsg::TogglePostPicker))
.style(inputs::secondary_button) .style(inputs::secondary_button)
.padding([4, 10]), .padding([4, 10]),
@@ -457,7 +458,7 @@ pub fn view<'a>(
.post_picker_results .post_picker_results
.iter() .iter()
.map(|post| { .map(|post| {
button(text(post.title.clone()).size(12)) keyboard::button(text(post.title.clone()).size(12))
.on_press(Message::MediaEditor(MediaEditorMsg::LinkPost( .on_press(Message::MediaEditor(MediaEditorMsg::LinkPost(
post.post_id.clone(), post.post_id.clone(),
))) )))
@@ -497,13 +498,13 @@ pub fn view<'a>(
.iter() .iter()
.map(|post| { .map(|post| {
row![ row![
button(text(post.title.clone()).size(12)) keyboard::button(text(post.title.clone()).size(12))
.on_press(Message::MediaEditor(MediaEditorMsg::OpenLinkedPost( .on_press(Message::MediaEditor(MediaEditorMsg::OpenLinkedPost(
post.post_id.clone() post.post_id.clone()
))) )))
.padding([4, 0]), .padding([4, 0]),
Space::with_width(Length::Fill), Space::with_width(Length::Fill),
button(text(t(locale, "editor.unlinkMedia")).size(11)) keyboard::button(text(t(locale, "editor.unlinkMedia")).size(11))
.on_press(Message::MediaEditor(MediaEditorMsg::UnlinkPost( .on_press(Message::MediaEditor(MediaEditorMsg::UnlinkPost(
post.post_id.clone() post.post_id.clone()
))) )))
@@ -569,7 +570,7 @@ fn quick_action_item<'a>(
msg: MediaEditorMsg, msg: MediaEditorMsg,
enabled: bool, enabled: bool,
) -> Element<'a, Message> { ) -> Element<'a, Message> {
button(text(label).size(12).shaping(Shaping::Advanced)) keyboard::button(text(label).size(12).shaping(Shaping::Advanced))
.on_press_maybe(enabled.then_some(Message::MediaEditor(msg))) .on_press_maybe(enabled.then_some(Message::MediaEditor(msg)))
.padding([6, 12]) .padding([6, 12])
.style(status_bar::dropdown_item) .style(status_bar::dropdown_item)

View File

@@ -1,10 +1,12 @@
use crate::components::keyboard;
use iced::widget::button;
use std::collections::HashSet; use std::collections::HashSet;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use bds_core::engine::menu::{MenuItem, MenuItemKind}; use bds_core::engine::menu::{MenuItem, MenuItemKind};
use bds_core::i18n::UiLocale; use bds_core::i18n::UiLocale;
use iced::widget::{ use iced::widget::{
Space, button, column, container, mouse_area, row, scrollable, svg, text, text_input, tooltip, Space, column, container, mouse_area, row, scrollable, svg, text, text_input, tooltip,
}; };
use iced::{Alignment, Background, Border, Color, Element, Length, Padding, Point, Theme}; use iced::{Alignment, Background, Border, Color, Element, Length, Padding, Point, Theme};
use uuid::Uuid; use uuid::Uuid;
@@ -810,19 +812,22 @@ fn toolbar_icon(
} else { } else {
secondary_button secondary_button
}; };
let mut control = button(text(glyph).size(18)) let mut control = keyboard::button(text(glyph).size(18))
.width(Length::Fixed(38.0)) .width(Length::Fixed(38.0))
.height(Length::Fixed(34.0)) .height(Length::Fixed(34.0))
.style(style); .style(style);
if enabled { if enabled {
control = control.on_press(Message::MenuEditor(message)); control = control.on_press(Message::MenuEditor(message));
} }
keyboard::focusable(
tooltip( tooltip(
control, control,
text(t(locale, key)).size(12), text(t(locale, key)).size(12),
tooltip::Position::Bottom, tooltip::Position::Bottom,
) )
.gap(4) .gap(4),
enabled,
)
.into() .into()
} }
@@ -856,7 +861,7 @@ fn tree_item<'a>(
let collapsed = state.collapsed.contains(&item.id); let collapsed = state.collapsed.contains(&item.id);
let id = item.id.clone(); let id = item.id.clone();
let toggle: Element<'_, Message> = if is_submenu { let toggle: Element<'_, Message> = if is_submenu {
button(text(if collapsed { "" } else { "" }).size(14)) keyboard::button(text(if collapsed { "" } else { "" }).size(14))
.on_press(Message::MenuEditor(MenuEditorMsg::ToggleExpanded( .on_press(Message::MenuEditor(MenuEditorMsg::ToggleExpanded(
id.clone(), id.clone(),
))) )))
@@ -884,7 +889,7 @@ fn tree_item<'a>(
} else { } else {
item.label.clone() item.label.clone()
}; };
let label_button = button( let label_button = keyboard::button(
row![ row![
kind_icon(&item.kind), kind_icon(&item.kind),
text(label).size(14), text(label).size(14),
@@ -1016,7 +1021,7 @@ fn draft_editor<'a>(
.filter(|page| page_matches_query(page, &draft.query)) .filter(|page| page_matches_query(page, &draft.query))
{ {
choices = choices.push( choices = choices.push(
button(text(page.title.clone())) keyboard::button(text(page.title.clone()))
.on_press(Message::MenuEditor(MenuEditorMsg::ChoosePage( .on_press(Message::MenuEditor(MenuEditorMsg::ChoosePage(
page.id.clone(), page.id.clone(),
))) )))
@@ -1032,7 +1037,7 @@ fn draft_editor<'a>(
.filter(|name| category_matches_query(name, &draft.query)) .filter(|name| category_matches_query(name, &draft.query))
{ {
choices = choices.push( choices = choices.push(
button(text(category.clone())) keyboard::button(text(category.clone()))
.on_press(Message::MenuEditor(MenuEditorMsg::ChooseCategory( .on_press(Message::MenuEditor(MenuEditorMsg::ChooseCategory(
category.clone(), category.clone(),
))) )))
@@ -1047,10 +1052,10 @@ fn draft_editor<'a>(
DraftKind::Category => "menuEditor.useCategory", DraftKind::Category => "menuEditor.useCategory",
}; };
let actions = row![ let actions = row![
button(text(t(locale, submit_label))) keyboard::button(text(t(locale, submit_label)))
.on_press(Message::MenuEditor(MenuEditorMsg::SubmitDraft)) .on_press(Message::MenuEditor(MenuEditorMsg::SubmitDraft))
.style(primary_button), .style(primary_button),
button(text(t(locale, "common.cancel"))) keyboard::button(text(t(locale, "common.cancel")))
.on_press(Message::MenuEditor(MenuEditorMsg::CancelDraft)) .on_press(Message::MenuEditor(MenuEditorMsg::CancelDraft))
.style(secondary_button), .style(secondary_button),
] ]

View File

@@ -1,5 +1,6 @@
use crate::components::keyboard;
use iced::widget::text::Shaping; use iced::widget::text::Shaping;
use iced::widget::{Space, button, column, container, row, scrollable, text}; use iced::widget::{Space, column, container, row, scrollable, text};
use iced::{Color, Element, Length}; use iced::{Color, Element, Length};
use bds_core::engine::metadata_diff::{DiffReport, RepairDirection}; use bds_core::engine::metadata_diff::{DiffReport, RepairDirection};
@@ -18,10 +19,11 @@ pub struct MetadataDiffState {
} }
pub fn view<'a>(state: &'a MetadataDiffState, locale: UiLocale) -> Element<'a, Message> { pub fn view<'a>(state: &'a MetadataDiffState, locale: UiLocale) -> Element<'a, Message> {
let run = button(text(t(locale, "metadataDiff.run")).size(13)) let run = keyboard::button(text(t(locale, "metadataDiff.run")).size(13))
.on_press_maybe( .on_press_maybe(
(!state.is_running && !state.is_repairing).then_some(Message::RunMetadataDiff), (!state.is_running && !state.is_repairing).then_some(Message::RunMetadataDiff),
) )
.hotkey('r')
.style(inputs::primary_button) .style(inputs::primary_button)
.padding([6, 16]); .padding([6, 16]);
let mut content = column![ let mut content = column![
@@ -65,7 +67,7 @@ pub fn view<'a>(state: &'a MetadataDiffState, locale: UiLocale) -> Element<'a, M
) )
}); });
let actions = row![ let actions = row![
button(text(t(locale, "metadataDiff.fileToDb")).size(12)) keyboard::button(text(t(locale, "metadataDiff.fileToDb")).size(12))
.on_press_maybe((!state.is_repairing).then_some( .on_press_maybe((!state.is_repairing).then_some(
Message::RepairMetadataDiffItem { Message::RepairMetadataDiffItem {
index, index,
@@ -74,7 +76,7 @@ pub fn view<'a>(state: &'a MetadataDiffState, locale: UiLocale) -> Element<'a, M
)) ))
.style(inputs::secondary_button) .style(inputs::secondary_button)
.padding([5, 10]), .padding([5, 10]),
button(text(t(locale, "metadataDiff.dbToFile")).size(12)) keyboard::button(text(t(locale, "metadataDiff.dbToFile")).size(12))
.on_press_maybe((!state.is_repairing).then_some( .on_press_maybe((!state.is_repairing).then_some(
Message::RepairMetadataDiffItem { Message::RepairMetadataDiffItem {
index, index,
@@ -116,7 +118,7 @@ pub fn view<'a>(state: &'a MetadataDiffState, locale: UiLocale) -> Element<'a, M
.align_y(iced::Alignment::Center); .align_y(iced::Alignment::Center);
if orphan.reason == "file_without_db_entry" { if orphan.reason == "file_without_db_entry" {
item = item.push( item = item.push(
button(text(t(locale, "metadataDiff.importOrphan")).size(12)) keyboard::button(text(t(locale, "metadataDiff.importOrphan")).size(12))
.on_press_maybe( .on_press_maybe(
(!state.is_repairing) (!state.is_repairing)
.then_some(Message::ImportMetadataOrphan(index)), .then_some(Message::ImportMetadataOrphan(index)),

View File

@@ -1,9 +1,9 @@
use crate::components::keyboard;
use iced::widget::button;
use std::path::Path; use std::path::Path;
use iced::widget::text::Shaping; use iced::widget::text::Shaping;
use iced::widget::{ use iced::widget::{Space, checkbox, column, container, image, row, scrollable, text, text_input};
Space, button, checkbox, column, container, image, row, scrollable, text, text_input,
};
use iced::{Alignment, Background, Border, Color, Element, Length, Shadow, Theme, Vector}; use iced::{Alignment, Background, Border, Color, Element, Length, Shadow, Theme, Vector};
use bds_core::i18n::UiLocale; use bds_core::i18n::UiLocale;
@@ -282,7 +282,7 @@ pub fn view(
let selected = selected_project_id.as_deref() == Some(project.id.as_str()); let selected = selected_project_id.as_deref() == Some(project.id.as_str());
let marker = if selected { "" } else { "" }; let marker = if selected { "" } else { "" };
project_rows = project_rows.push( project_rows = project_rows.push(
button( keyboard::button(
row![ row![
text(marker).size(12), text(marker).size(12),
text(project.name.clone()).size(13), text(project.name.clone()).size(13),
@@ -307,12 +307,12 @@ pub fn view(
content = content.push(project_rows); content = content.push(project_rows);
} }
let cancel = button(text(t(locale, "remoteConnection.cancel")).size(13)) let cancel = keyboard::button(text(t(locale, "remoteConnection.cancel")).size(13))
.on_press(Message::DismissModal) .on_press(Message::DismissModal)
.padding([6, 16]) .padding([6, 16])
.style(cancel_button_style); .style(cancel_button_style);
let action = if !connected { let action = if !connected {
let button = button( let button = keyboard::button(
text(if connecting { text(if connecting {
t(locale, "remoteConnection.connecting") t(locale, "remoteConnection.connecting")
} else { } else {
@@ -328,7 +328,7 @@ pub fn view(
button.on_press(Message::RemoteConnectRequested) button.on_press(Message::RemoteConnectRequested)
} }
} else { } else {
let button = button(text(t(locale, "remoteConnection.open")).size(13)) let button = keyboard::button(text(t(locale, "remoteConnection.open")).size(13))
.padding([6, 16]) .padding([6, 16])
.style(confirm_button_style); .style(confirm_button_style);
if selected_project_id.is_some() { if selected_project_id.is_some() {
@@ -393,7 +393,7 @@ pub fn view(
let on_confirm_clone = on_confirm.clone(); let on_confirm_clone = on_confirm.clone();
let buttons = row![ let buttons = row![
button( keyboard::button(
text(t(locale, "modal.confirmDelete.cancel")) text(t(locale, "modal.confirmDelete.cancel"))
.size(13) .size(13)
.shaping(Shaping::Advanced) .shaping(Shaping::Advanced)
@@ -402,7 +402,7 @@ pub fn view(
.padding([6, 16]) .padding([6, 16])
.style(cancel_button_style), .style(cancel_button_style),
Space::with_width(Length::Fill), Space::with_width(Length::Fill),
button( keyboard::button(
text(t(locale, "modal.confirmDelete.delete")) text(t(locale, "modal.confirmDelete.delete"))
.size(13) .size(13)
.shaping(Shaping::Advanced) .shaping(Shaping::Advanced)
@@ -438,7 +438,7 @@ pub fn view(
let on_confirm_clone = on_confirm.clone(); let on_confirm_clone = on_confirm.clone();
let buttons = row![ let buttons = row![
button( keyboard::button(
text(t(locale, "modal.confirm.cancel")) text(t(locale, "modal.confirm.cancel"))
.size(13) .size(13)
.shaping(Shaping::Advanced) .shaping(Shaping::Advanced)
@@ -447,7 +447,7 @@ pub fn view(
.padding([6, 16]) .padding([6, 16])
.style(cancel_button_style), .style(cancel_button_style),
Space::with_width(Length::Fill), Space::with_width(Length::Fill),
button( keyboard::button(
text(t(locale, "modal.confirm.confirm")) text(t(locale, "modal.confirm.confirm"))
.size(13) .size(13)
.shaping(Shaping::Advanced) .shaping(Shaping::Advanced)
@@ -474,12 +474,12 @@ pub fn view(
ModalState::SearchIndexRepair => { ModalState::SearchIndexRepair => {
let buttons = row![ let buttons = row![
button(text(t(locale, "searchIndexRepair.later")).size(13)) keyboard::button(text(t(locale, "searchIndexRepair.later")).size(13))
.on_press(Message::DismissModal) .on_press(Message::DismissModal)
.padding([6, 16]) .padding([6, 16])
.style(cancel_button_style), .style(cancel_button_style),
Space::with_width(Length::Fill), Space::with_width(Length::Fill),
button(text(t(locale, "searchIndexRepair.rebuildNow")).size(13)) keyboard::button(text(t(locale, "searchIndexRepair.rebuildNow")).size(13))
.on_press(Message::ConfirmModal(ConfirmAction::RebuildSearchIndex)) .on_press(Message::ConfirmModal(ConfirmAction::RebuildSearchIndex))
.padding([6, 16]) .padding([6, 16])
.style(confirm_button_style), .style(confirm_button_style),
@@ -520,7 +520,7 @@ pub fn view(
); );
} }
let mut actions = row![ let mut actions = row![
button(text(t(locale, "find.next"))) keyboard::button(text(t(locale, "find.next")))
.on_press(Message::FindNext) .on_press(Message::FindNext)
.style(inputs::primary_button), .style(inputs::primary_button),
] ]
@@ -528,12 +528,12 @@ pub fn view(
if show_replace { if show_replace {
actions = actions actions = actions
.push( .push(
button(text(t(locale, "find.replace"))) keyboard::button(text(t(locale, "find.replace")))
.on_press(Message::ReplaceCurrent) .on_press(Message::ReplaceCurrent)
.style(inputs::secondary_button), .style(inputs::secondary_button),
) )
.push( .push(
button(text(t(locale, "find.replaceAll"))) keyboard::button(text(t(locale, "find.replaceAll")))
.on_press(Message::ReplaceAll) .on_press(Message::ReplaceAll)
.style(inputs::secondary_button), .style(inputs::secondary_button),
); );
@@ -582,7 +582,7 @@ pub fn view(
} else { } else {
content.push(Space::with_height(16.0)).push(row![ content.push(Space::with_height(16.0)).push(row![
Space::with_width(Length::Fill), Space::with_width(Length::Fill),
button(text(t(locale, "tasks.cancelTask")).size(13)) keyboard::button(text(t(locale, "tasks.cancelTask")).size(13))
.on_press(Message::CancelTask( .on_press(Message::CancelTask(
crate::state::navigation::TaskSource::Local, crate::state::navigation::TaskSource::Local,
task_id, task_id,
@@ -607,7 +607,7 @@ pub fn view(
external_url, external_url,
external_text, external_text,
} => { } => {
let internal_tab = button( let internal_tab = keyboard::button(
text(t(locale, "modal.postInsertLink.tabInternal")) text(t(locale, "modal.postInsertLink.tabInternal"))
.size(13) .size(13)
.shaping(Shaping::Advanced) .shaping(Shaping::Advanced)
@@ -623,7 +623,7 @@ pub fn view(
.padding([8, 16]) .padding([8, 16])
.style(cancel_button_style); .style(cancel_button_style);
let external_tab = button( let external_tab = keyboard::button(
text(t(locale, "modal.postInsertLink.tabExternal")) text(t(locale, "modal.postInsertLink.tabExternal"))
.size(13) .size(13)
.shaping(Shaping::Advanced) .shaping(Shaping::Advanced)
@@ -666,7 +666,7 @@ pub fn view(
let mut column = column![search_input, Space::with_height(12.0)]; let mut column = column![search_input, Space::with_height(12.0)];
for link in results { for link in results {
column = column.push( column = column.push(
button( keyboard::button(
row![ row![
column![ column![
text(link.title.clone()) text(link.title.clone())
@@ -749,7 +749,7 @@ pub fn view(
Space::with_height(12.0), Space::with_height(12.0),
row![ row![
Space::with_width(Length::Fill), Space::with_width(Length::Fill),
button(text(t(locale, "modal.postInsertLink.insert"))) keyboard::button(text(t(locale, "modal.postInsertLink.insert")))
.on_press(Message::PostEditor( .on_press(Message::PostEditor(
PostEditorMsg::PostInsertLinkExternalInsert PostEditorMsg::PostInsertLinkExternalInsert
)) ))
@@ -760,7 +760,7 @@ pub fn view(
.spacing(8) .spacing(8)
.into(); .into();
let create_post_btn: Element<'static, Message> = button( let create_post_btn: Element<'static, Message> = keyboard::button(
text(t(locale, "modal.postInsertLink.createPost")) text(t(locale, "modal.postInsertLink.createPost"))
.size(12) .size(12)
.shaping(Shaping::Advanced) .shaping(Shaping::Advanced)
@@ -792,7 +792,7 @@ pub fn view(
}; };
let buttons = row![ let buttons = row![
button(cancel_text) keyboard::button(cancel_text)
.on_press(Message::DismissModal) .on_press(Message::DismissModal)
.padding([6, 16]) .padding([6, 16])
.style(cancel_button_style), .style(cancel_button_style),
@@ -903,7 +903,7 @@ pub fn view(
.spacing(4) .spacing(4)
.align_x(Alignment::Center); .align_x(Alignment::Center);
let btn = button(media_col) let btn = keyboard::button(media_col)
.on_press(Message::PostEditor(PostEditorMsg::PostInsertMediaSelected( .on_press(Message::PostEditor(PostEditorMsg::PostInsertMediaSelected(
m.id.clone(), m.id.clone(),
))) )))
@@ -963,7 +963,7 @@ pub fn view(
.shaping(Shaping::Advanced); .shaping(Shaping::Advanced);
let buttons = row![ let buttons = row![
button(cancel_text) keyboard::button(cancel_text)
.on_press(Message::DismissModal) .on_press(Message::DismissModal)
.padding([6, 16]) .padding([6, 16])
.style(cancel_button_style), .style(cancel_button_style),
@@ -1045,7 +1045,7 @@ pub fn view(
.spacing(4) .spacing(4)
.align_x(Alignment::Center); .align_x(Alignment::Center);
let btn = button(media_col) let btn = keyboard::button(media_col)
.on_press(Message::PostEditor( .on_press(Message::PostEditor(
PostEditorMsg::PostGalleryImageSelected(index), PostEditorMsg::PostGalleryImageSelected(index),
)) ))
@@ -1082,7 +1082,7 @@ pub fn view(
.size(13) .size(13)
.shaping(Shaping::Advanced); .shaping(Shaping::Advanced);
let close_button = button(close_text) let close_button = keyboard::button(close_text)
.on_press(Message::DismissModal) .on_press(Message::DismissModal)
.padding([6, 16]) .padding([6, 16])
.style(cancel_button_style); .style(cancel_button_style);
@@ -1106,7 +1106,7 @@ pub fn view(
.width(Length::Fill) .width(Length::Fill)
.center_x(Length::Fill), .center_x(Length::Fill),
row![ row![
button(text("<")) keyboard::button(text("<"))
.on_press(Message::PostEditor(PostEditorMsg::PostGalleryPrevious)) .on_press(Message::PostEditor(PostEditorMsg::PostGalleryPrevious))
.padding([6, 12]) .padding([6, 12])
.style(cancel_button_style), .style(cancel_button_style),
@@ -1115,12 +1115,12 @@ pub fn view(
.size(12) .size(12)
.shaping(Shaping::Advanced), .shaping(Shaping::Advanced),
Space::with_width(Length::Fill), Space::with_width(Length::Fill),
button(text(">")) keyboard::button(text(">"))
.on_press(Message::PostEditor(PostEditorMsg::PostGalleryNext)) .on_press(Message::PostEditor(PostEditorMsg::PostGalleryNext))
.padding([6, 12]) .padding([6, 12])
.style(cancel_button_style), .style(cancel_button_style),
Space::with_width(12.0), Space::with_width(12.0),
button(text(t(locale, "modal.postGallery.backToGrid"))) keyboard::button(text(t(locale, "modal.postGallery.backToGrid")))
.on_press(Message::PostEditor( .on_press(Message::PostEditor(
PostEditorMsg::PostGalleryCloseLightbox PostEditorMsg::PostGalleryCloseLightbox
)) ))
@@ -1166,13 +1166,15 @@ pub fn view(
.iter() .iter()
.enumerate() .enumerate()
.map(|(index, field)| { .map(|(index, field)| {
let toggle = let toggle = keyboard::focusable(
checkbox(field.label.clone(), field.accepted) checkbox(field.label.clone(), field.accepted)
.on_toggle_maybe((!field.locked).then_some(move |value| { .on_toggle_maybe((!field.locked).then_some(move |value| {
Message::ToggleAiSuggestionField(index, value) Message::ToggleAiSuggestionField(index, value)
})) }))
.size(16) .size(16)
.text_size(13); .text_size(13),
!field.locked,
);
container( container(
column![ column![
toggle, toggle,
@@ -1211,12 +1213,12 @@ pub fn view(
.collect::<Vec<Element<'static, Message>>>(); .collect::<Vec<Element<'static, Message>>>();
let buttons = row![ let buttons = row![
button(text(t(locale, "common.cancel")).size(13)) keyboard::button(text(t(locale, "common.cancel")).size(13))
.on_press(Message::DismissModal) .on_press(Message::DismissModal)
.padding([6, 16]) .padding([6, 16])
.style(cancel_button_style), .style(cancel_button_style),
Space::with_width(Length::Fill), Space::with_width(Length::Fill),
button(text(t(locale, "modal.aiSuggestions.applySelected")).size(13)) keyboard::button(text(t(locale, "modal.aiSuggestions.applySelected")).size(13))
.on_press(Message::ApplyAiSuggestions(target, fields)) .on_press(Message::ApplyAiSuggestions(target, fields))
.padding([6, 16]) .padding([6, 16])
.style(confirm_button_style), .style(confirm_button_style),
@@ -1275,7 +1277,7 @@ pub fn view(
), ),
}; };
let status = language.existing_status.clone().unwrap_or_default(); let status = language.existing_status.clone().unwrap_or_default();
button( keyboard::button(
row![ row![
text(format!("{} {}", language.flag_emoji, language.name)) text(format!("{} {}", language.flag_emoji, language.name))
.size(13) .size(13)
@@ -1302,7 +1304,7 @@ pub fn view(
Space::with_height(12.0), Space::with_height(12.0),
column(rows).spacing(6), column(rows).spacing(6),
Space::with_height(16.0), Space::with_height(16.0),
button(text(t(locale, "common.cancel")).size(13)) keyboard::button(text(t(locale, "common.cancel")).size(13))
.on_press(Message::DismissModal) .on_press(Message::DismissModal)
.padding([6, 16]) .padding([6, 16])
.style(cancel_button_style), .style(cancel_button_style),

View File

@@ -1,5 +1,7 @@
use crate::components::keyboard;
use iced::widget::button;
use iced::widget::text::Shaping; use iced::widget::text::Shaping;
use iced::widget::{Space, button, column, container, progress_bar, row, scrollable, text}; use iced::widget::{Space, column, container, progress_bar, row, scrollable, text};
use iced::{Alignment, Background, Border, Color, Element, Font, Length, Theme}; use iced::{Alignment, Background, Border, Color, Element, Font, Length, Theme};
use bds_core::engine::git::GitCommit; use bds_core::engine::git::GitCommit;
@@ -80,7 +82,7 @@ fn task_row(
rows.push( rows.push(
row![ row![
Space::with_width(Length::Fill), Space::with_width(Length::Fill),
button(text(t(locale, "tasks.cancelTask")).size(10)) keyboard::button(text(t(locale, "tasks.cancelTask")).size(10))
.on_press(Message::CancelTask(snapshot.source, snapshot.id)) .on_press(Message::CancelTask(snapshot.source, snapshot.id))
.padding([3, 8]) .padding([3, 8])
.style(inputs::secondary_button), .style(inputs::secondary_button),
@@ -267,7 +269,7 @@ pub fn view(
// Tab header — per layout.allium: tasks, output, post_links (only when // Tab header — per layout.allium: tasks, output, post_links (only when
// active editor tab is a post), git_log (only when active tab is post or // active editor tab is a post), git_log (only when active tab is post or
// media). // media).
let tasks_btn = button( let tasks_btn = keyboard::button(
text(t(locale, "common.tasks")) text(t(locale, "common.tasks"))
.size(12) .size(12)
.shaping(Shaping::Advanced), .shaping(Shaping::Advanced),
@@ -280,7 +282,7 @@ pub fn view(
tab_inactive tab_inactive
}); });
let output_btn = button( let output_btn = keyboard::button(
text(t(locale, "panel.output")) text(t(locale, "panel.output"))
.size(12) .size(12)
.shaping(Shaping::Advanced), .shaping(Shaping::Advanced),
@@ -293,7 +295,7 @@ pub fn view(
tab_inactive tab_inactive
}); });
let close_btn = button(text("\u{2715}").size(12).shaping(Shaping::Advanced)) let close_btn = keyboard::button(text("\u{2715}").size(12).shaping(Shaping::Advanced))
.on_press(Message::TogglePanel) .on_press(Message::TogglePanel)
.padding([4, 6]) .padding([4, 6])
.style(close_btn_style); .style(close_btn_style);
@@ -301,7 +303,7 @@ pub fn view(
let mut tab_row: Vec<Element<'static, Message>> = vec![tasks_btn.into(), output_btn.into()]; let mut tab_row: Vec<Element<'static, Message>> = vec![tasks_btn.into(), output_btn.into()];
if active_tab_is_post { if active_tab_is_post {
let post_links_btn = button( let post_links_btn = keyboard::button(
text(t(locale, "panel.postLinks")) text(t(locale, "panel.postLinks"))
.size(12) .size(12)
.shaping(Shaping::Advanced), .shaping(Shaping::Advanced),
@@ -317,7 +319,7 @@ pub fn view(
} }
if active_tab_is_post_or_media { if active_tab_is_post_or_media {
let git_log_btn = button( let git_log_btn = keyboard::button(
text(t(locale, "panel.gitLog")) text(t(locale, "panel.gitLog"))
.size(12) .size(12)
.shaping(Shaping::Advanced), .shaping(Shaping::Advanced),
@@ -372,7 +374,7 @@ pub fn view(
let collapsed = collapsed_task_groups.contains(group_id); let collapsed = collapsed_task_groups.contains(group_id);
let group_name = snapshot.group_name.as_deref().unwrap_or(group_id); let group_name = snapshot.group_name.as_deref().unwrap_or(group_id);
items.push( items.push(
button( keyboard::button(
row![ row![
text(if collapsed { "\u{25b8}" } else { "\u{25be}" }).size(11), text(if collapsed { "\u{25b8}" } else { "\u{25be}" }).size(11),
text(format!("{} ({})", group_name, members.len())) text(format!("{} ({})", group_name, members.len()))
@@ -524,7 +526,7 @@ pub fn view(
let hash = commit.hash.clone(); let hash = commit.hash.clone();
let subject = commit.subject.clone().unwrap_or_else(|| hash.clone()); let subject = commit.subject.clone().unwrap_or_else(|| hash.clone());
let short = hash.chars().take(7).collect::<String>(); let short = hash.chars().take(7).collect::<String>();
button( keyboard::button(
row![ row![
text(short).size(11).font(iced::Font::MONOSPACE), text(short).size(11).font(iced::Font::MONOSPACE),
text(subject.clone()).size(11), text(subject.clone()).size(11),
@@ -560,7 +562,7 @@ pub fn view(
} }
fn post_link_button(locale: UiLocale, link: &ResolvedPostLink) -> Element<'static, Message> { fn post_link_button(locale: UiLocale, link: &ResolvedPostLink) -> Element<'static, Message> {
button(text(link.title.clone()).size(11).shaping(Shaping::Advanced)) keyboard::button(text(link.title.clone()).size(11).shaping(Shaping::Advanced))
.on_press(Message::OpenTab(Tab { .on_press(Message::OpenTab(Tab {
id: link.post_id.clone(), id: link.post_id.clone(),
title: if link.title.is_empty() { title: if link.title.is_empty() {

View File

@@ -1,10 +1,12 @@
use crate::components::keyboard;
use iced::widget::button;
use std::cell::RefCell; use std::cell::RefCell;
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::atomic::{AtomicU64, Ordering};
use iced::widget::text::{Shaping, Wrapping}; use iced::widget::text::{Shaping, Wrapping};
use iced::widget::{Column, Space, button, column, container, row, scrollable, text, text_input}; use iced::widget::{Column, Space, column, container, row, scrollable, text, text_input};
use iced::{Color, Element, Length, Theme}; use iced::{Color, Element, Length, Theme};
use bds_core::i18n::{self, UiLocale}; use bds_core::i18n::{self, UiLocale};
@@ -489,7 +491,7 @@ pub fn view<'a>(
format!("{}{}", truncate_header_title(&state.title), dirty_indicator) format!("{}{}", truncate_header_title(&state.title), dirty_indicator)
}; };
let quick_actions_button: Element<'a, Message> = button( let quick_actions_button: Element<'a, Message> = keyboard::button(
text(t(locale, "editor.quickActions")) text(t(locale, "editor.quickActions"))
.size(13) .size(13)
.shaping(Shaping::Advanced), .shaping(Shaping::Advanced),
@@ -569,7 +571,7 @@ pub fn view<'a>(
let mut header_action_items: Vec<Element<'a, Message>> = vec![ let mut header_action_items: Vec<Element<'a, Message>> = vec![
status_badge(locale, &state.status), status_badge(locale, &state.status),
quick_actions, quick_actions,
button( keyboard::button(
text(t(locale, "common.save")) text(t(locale, "common.save"))
.size(13) .size(13)
.shaping(Shaping::Advanced), .shaping(Shaping::Advanced),
@@ -581,7 +583,7 @@ pub fn view<'a>(
]; ];
if state.status == PostStatus::Draft { if state.status == PostStatus::Draft {
header_action_items.push( header_action_items.push(
button( keyboard::button(
text(t(locale, "editor.publish")) text(t(locale, "editor.publish"))
.size(13) .size(13)
.shaping(Shaping::Advanced), .shaping(Shaping::Advanced),
@@ -594,7 +596,7 @@ pub fn view<'a>(
} }
if !on_translation && state.status == PostStatus::Draft && state.published_at.is_some() { if !on_translation && state.status == PostStatus::Draft && state.published_at.is_some() {
header_action_items.push( header_action_items.push(
button( keyboard::button(
text(t(locale, "editor.discard")) text(t(locale, "editor.discard"))
.size(13) .size(13)
.shaping(Shaping::Advanced), .shaping(Shaping::Advanced),
@@ -606,7 +608,7 @@ pub fn view<'a>(
); );
} }
header_action_items.push( header_action_items.push(
button( keyboard::button(
text(t(locale, "modal.confirmDelete.delete")) text(t(locale, "modal.confirmDelete.delete"))
.size(13) .size(13)
.shaping(Shaping::Advanced), .shaping(Shaping::Advanced),
@@ -643,13 +645,14 @@ pub fn view<'a>(
} else { } else {
format!("\u{25B6} {}", t(locale, "editor.metadata")) format!("\u{25B6} {}", t(locale, "editor.metadata"))
}; };
let meta_toggle = button( let meta_toggle = keyboard::button(
text(meta_toggle_label) text(meta_toggle_label)
.size(12) .size(12)
.color(inputs::SECTION_COLOR) .color(inputs::SECTION_COLOR)
.shaping(Shaping::Advanced), .shaping(Shaping::Advanced),
) )
.on_press(Message::PostEditor(PostEditorMsg::ToggleMetadata)) .on_press(Message::PostEditor(PostEditorMsg::ToggleMetadata))
.hotkey('m')
.padding([8, 10]) .padding([8, 10])
.width(Length::Fill) .width(Length::Fill)
.style(inputs::disclosure_button); .style(inputs::disclosure_button);
@@ -663,7 +666,7 @@ pub fn view<'a>(
for flag in &flags { for flag in &flags {
let lang = flag.language.clone(); let lang = flag.language.clone();
let label = flag.flag_emoji.to_string(); let label = flag.flag_emoji.to_string();
let btn = button(text(label).size(14).shaping(Shaping::Advanced)) let btn = keyboard::button(text(label).size(14).shaping(Shaping::Advanced))
.on_press(Message::PostEditor(PostEditorMsg::SwitchLanguage(lang))) .on_press(Message::PostEditor(PostEditorMsg::SwitchLanguage(lang)))
.padding([2, 4]) .padding([2, 4])
.style(if flag.is_active { .style(if flag.is_active {
@@ -756,7 +759,7 @@ pub fn view<'a>(
.align_y(iced::Alignment::Center); .align_y(iced::Alignment::Center);
for tag in semantic_suggestions { for tag in semantic_suggestions {
chips = chips.push( chips = chips.push(
button(text(format!("+ {tag}")).size(11)) keyboard::button(text(format!("+ {tag}")).size(11))
.on_press(Message::PostEditor(PostEditorMsg::AddSuggestedTag( .on_press(Message::PostEditor(PostEditorMsg::AddSuggestedTag(
tag.to_string(), tag.to_string(),
))) )))
@@ -784,7 +787,7 @@ pub fn view<'a>(
.align_y(iced::Alignment::Center); .align_y(iced::Alignment::Center);
for tag in matching_suggestions { for tag in matching_suggestions {
chips = chips.push( chips = chips.push(
button(text(tag).size(11)) keyboard::button(text(tag).size(11))
.on_press(Message::PostEditor(PostEditorMsg::AddSuggestedTag( .on_press(Message::PostEditor(PostEditorMsg::AddSuggestedTag(
tag.to_string(), tag.to_string(),
))) )))
@@ -795,7 +798,9 @@ pub fn view<'a>(
if query_addable { if query_addable {
let query = state.tags_input.trim().to_string(); let query = state.tags_input.trim().to_string();
chips = chips.push( chips = chips.push(
button(text(tw(locale, "editor.createTag", &[("name", &query)])).size(11)) keyboard::button(
text(tw(locale, "editor.createTag", &[("name", &query)])).size(11),
)
.on_press(Message::PostEditor(PostEditorMsg::AddSuggestedTag(query))) .on_press(Message::PostEditor(PostEditorMsg::AddSuggestedTag(query)))
.padding([4, 8]) .padding([4, 8])
.style(inputs::secondary_button), .style(inputs::secondary_button),
@@ -860,7 +865,7 @@ pub fn view<'a>(
Column::with_children(items).spacing(2).into() Column::with_children(items).spacing(2).into()
}; };
let link_existing_button: Element<'a, Message> = button( let link_existing_button: Element<'a, Message> = keyboard::button(
text(t(locale, "editor.linkExistingMedia")) text(t(locale, "editor.linkExistingMedia"))
.size(11) .size(11)
.shaping(Shaping::Advanced), .shaping(Shaping::Advanced),
@@ -916,14 +921,14 @@ pub fn view<'a>(
] ]
.spacing(2) .spacing(2)
.width(Length::Fill), .width(Length::Fill),
button( keyboard::button(
text(t(locale, "common.open")) text(t(locale, "common.open"))
.size(11) .size(11)
.shaping(Shaping::Advanced) .shaping(Shaping::Advanced)
) )
.on_press(Message::PostEditor(PostEditorMsg::OpenLinkedMedia(open_id))) .on_press(Message::PostEditor(PostEditorMsg::OpenLinkedMedia(open_id)))
.padding([4, 10]), .padding([4, 10]),
button( keyboard::button(
text(t(locale, "editor.unlinkMedia")) text(t(locale, "editor.unlinkMedia"))
.size(11) .size(11)
.shaping(Shaping::Advanced) .shaping(Shaping::Advanced)
@@ -981,13 +986,14 @@ pub fn view<'a>(
} else { } else {
format!("\u{25B6} {}", t(locale, "editor.excerpt")) format!("\u{25B6} {}", t(locale, "editor.excerpt"))
}; };
let excerpt_toggle = button( let excerpt_toggle = keyboard::button(
text(excerpt_toggle_label) text(excerpt_toggle_label)
.size(12) .size(12)
.color(inputs::SECTION_COLOR) .color(inputs::SECTION_COLOR)
.shaping(Shaping::Advanced), .shaping(Shaping::Advanced),
) )
.on_press(Message::PostEditor(PostEditorMsg::ToggleExcerpt)) .on_press(Message::PostEditor(PostEditorMsg::ToggleExcerpt))
.hotkey('x')
.padding([8, 10]) .padding([8, 10])
.width(Length::Fill) .width(Length::Fill)
.style(inputs::disclosure_button); .style(inputs::disclosure_button);
@@ -1036,7 +1042,7 @@ pub fn view<'a>(
], ],
vec![ vec![
if show_content_actions { if show_content_actions {
button( keyboard::button(
text(t(locale, "editor.insertLink")) text(t(locale, "editor.insertLink"))
.size(13) .size(13)
.shaping(Shaping::Advanced), .shaping(Shaping::Advanced),
@@ -1049,7 +1055,7 @@ pub fn view<'a>(
Space::new(0, 0).into() Space::new(0, 0).into()
}, },
if show_content_actions { if show_content_actions {
button( keyboard::button(
text(t(locale, "editor.insertMedia")) text(t(locale, "editor.insertMedia"))
.size(13) .size(13)
.shaping(Shaping::Advanced), .shaping(Shaping::Advanced),
@@ -1062,7 +1068,7 @@ pub fn view<'a>(
Space::new(0, 0).into() Space::new(0, 0).into()
}, },
if show_content_actions { if show_content_actions {
button( keyboard::button(
text(t(locale, "editor.gallery")) text(t(locale, "editor.gallery"))
.size(13) .size(13)
.shaping(Shaping::Advanced), .shaping(Shaping::Advanced),
@@ -1209,7 +1215,7 @@ fn chip_input_field<'a>(
.map(|chip| { .map(|chip| {
let label = format!("{} \u{2715}", chip); let label = format!("{} \u{2715}", chip);
let chip_val = chip.clone(); let chip_val = chip.clone();
button(text(label).size(11).shaping(Shaping::Advanced)) keyboard::button(text(label).size(11).shaping(Shaping::Advanced))
.on_press(on_remove(chip_val)) .on_press(on_remove(chip_val))
.padding([2, 6]) .padding([2, 6])
.style(chip_button_style) .style(chip_button_style)
@@ -1266,7 +1272,7 @@ fn mode_button<'a>(
"preview" => t(locale, "editor.modePreview"), "preview" => t(locale, "editor.modePreview"),
_ => t(locale, "editor.modeMarkdown"), _ => t(locale, "editor.modeMarkdown"),
}; };
button(text(label).size(12).shaping(Shaping::Advanced)) keyboard::button(text(label).size(12).shaping(Shaping::Advanced))
.on_press(message) .on_press(message)
.padding([4, 10]) .padding([4, 10])
.style(if active_mode == mode { .style(if active_mode == mode {
@@ -1284,7 +1290,7 @@ fn quick_action_item<'a>(
enabled: bool, enabled: bool,
) -> Element<'a, Message> { ) -> Element<'a, Message> {
let _ = locale; let _ = locale;
button(text(label).size(12).shaping(Shaping::Advanced)) keyboard::button(text(label).size(12).shaping(Shaping::Advanced))
.on_press_maybe(enabled.then_some(Message::PostEditor(msg))) .on_press_maybe(enabled.then_some(Message::PostEditor(msg)))
.padding([6, 12]) .padding([6, 12])
.style(status_bar::dropdown_item) .style(status_bar::dropdown_item)

View File

@@ -1,5 +1,7 @@
use crate::components::keyboard;
use iced::widget::button;
use iced::widget::text::Shaping; use iced::widget::text::Shaping;
use iced::widget::{Column, Space, button, container, row, svg, text}; use iced::widget::{Column, Space, container, row, svg, text};
use iced::{Background, Border, Color, Element, Length, Theme}; use iced::{Background, Border, Color, Element, Length, Theme};
use bds_core::i18n::UiLocale; use bds_core::i18n::UiLocale;
@@ -148,7 +150,7 @@ pub fn view(
}; };
items.push( items.push(
button(label) keyboard::button(label)
.on_press(Message::SwitchProject(id)) .on_press(Message::SwitchProject(id))
.padding([4, 8]) .padding([4, 8])
.width(Length::Fill) .width(Length::Fill)
@@ -170,7 +172,7 @@ pub fn view(
// Open and create project actions // Open and create project actions
items.push( items.push(
button( keyboard::button(
row![ row![
text("") text("")
.size(14) .size(14)
@@ -190,7 +192,7 @@ pub fn view(
); );
items.push( items.push(
button( keyboard::button(
row![ row![
text("+") text("+")
.size(14) .size(14)
@@ -256,7 +258,7 @@ pub fn trigger_button(project_name: &str) -> Element<'static, Message> {
.shaping(Shaping::Advanced) .shaping(Shaping::Advanced)
.color(Color::from_rgb(0.55, 0.55, 0.60)); .color(Color::from_rgb(0.55, 0.55, 0.60));
button( keyboard::button(
row![folder_icon, name, chevron] row![folder_icon, name, chevron]
.spacing(4) .spacing(4)
.align_y(iced::Alignment::Center), .align_y(iced::Alignment::Center),

View File

@@ -1,6 +1,7 @@
use crate::components::keyboard;
use std::cell::RefCell; use std::cell::RefCell;
use iced::widget::{Space, button, column, container, row, scrollable, text}; use iced::widget::{Space, column, container, row, scrollable, text};
use iced::{Color, Element, Length, Theme}; use iced::{Color, Element, Length, Theme};
use bds_core::i18n::UiLocale; use bds_core::i18n::UiLocale;
@@ -125,22 +126,22 @@ pub fn view<'a>(state: &'a ScriptEditorState, locale: UiLocale) -> Element<'a, M
status_badge(&state.status), status_badge(&state.status),
], ],
vec![ vec![
button(text(t(locale, "common.save")).size(13)) keyboard::button(text(t(locale, "common.save")).size(13))
.on_press(Message::ScriptEditor(ScriptEditorMsg::Save)) .on_press(Message::ScriptEditor(ScriptEditorMsg::Save))
.style(inputs::primary_button) .style(inputs::primary_button)
.padding([6, 16]) .padding([6, 16])
.into(), .into(),
button(text(t(locale, "editor.run")).size(13)) keyboard::button(text(t(locale, "editor.run")).size(13))
.on_press(Message::ScriptEditor(ScriptEditorMsg::Run)) .on_press(Message::ScriptEditor(ScriptEditorMsg::Run))
.style(inputs::secondary_button) .style(inputs::secondary_button)
.padding([6, 16]) .padding([6, 16])
.into(), .into(),
button(text(t(locale, "editor.checkSyntax")).size(13)) keyboard::button(text(t(locale, "editor.checkSyntax")).size(13))
.on_press(Message::ScriptEditor(ScriptEditorMsg::CheckSyntax)) .on_press(Message::ScriptEditor(ScriptEditorMsg::CheckSyntax))
.style(inputs::secondary_button) .style(inputs::secondary_button)
.padding([6, 16]) .padding([6, 16])
.into(), .into(),
button(text(t(locale, "modal.confirmDelete.delete")).size(13)) keyboard::button(text(t(locale, "modal.confirmDelete.delete")).size(13))
.on_press(Message::ScriptEditor(ScriptEditorMsg::Delete)) .on_press(Message::ScriptEditor(ScriptEditorMsg::Delete))
.style(inputs::danger_button) .style(inputs::danger_button)
.padding([6, 16]) .padding([6, 16])

View File

@@ -1,5 +1,6 @@
use crate::components::keyboard;
use iced::widget::text::Shaping; use iced::widget::text::Shaping;
use iced::widget::{button, column, container, row, scrollable, text, text_editor, text_input}; use iced::widget::{column, container, row, scrollable, text, text_editor, text_input};
use iced::{Alignment, Color, Element, Length}; use iced::{Alignment, Color, Element, Length};
use std::collections::BTreeMap; use std::collections::BTreeMap;
@@ -49,6 +50,34 @@ pub enum SettingsSection {
MCP, MCP,
} }
impl SettingsSection {
pub fn access_key(&self) -> char {
match self {
Self::Project => 'p',
Self::Editor => 'r',
Self::AI => 'a',
Self::Technology => 't',
Self::Publishing => 'u',
Self::Data => 'd',
Self::MCP => 'c',
}
}
pub fn from_access_key(key: char) -> Option<Self> {
[
Self::Project,
Self::Editor,
Self::AI,
Self::Technology,
Self::Publishing,
Self::Data,
Self::MCP,
]
.into_iter()
.find(|section| section.access_key() == key)
}
}
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct SettingsCategoryRow { pub struct SettingsCategoryRow {
pub name: String, pub name: String,
@@ -261,6 +290,13 @@ impl Default for SettingsViewState {
} }
impl SettingsViewState { impl SettingsViewState {
pub fn section_is_visible(&self, section: &SettingsSection, locale: UiLocale) -> bool {
self.search_query.is_empty()
|| t(locale, section.i18n_key())
.to_lowercase()
.contains(&self.search_query.to_lowercase())
}
pub fn focus_section(&mut self, section: SettingsSection) { pub fn focus_section(&mut self, section: SettingsSection) {
self.collapsed = SettingsSection::all() self.collapsed = SettingsSection::all()
.iter() .iter()
@@ -369,12 +405,10 @@ pub fn view<'a>(state: &'a SettingsViewState, locale: UiLocale) -> Element<'a, M
.padding([8, 10]) .padding([8, 10])
.style(inputs::field_style); .style(inputs::field_style);
let query_lower = state.search_query.to_lowercase();
let mut section_items = Vec::new(); let mut section_items = Vec::new();
for section in state.ordered_sections() { for section in state.ordered_sections() {
let label = t(locale, section.i18n_key()); let label = t(locale, section.i18n_key());
if !query_lower.is_empty() && !label.to_lowercase().contains(&query_lower) { if !state.section_is_visible(&section, locale) {
continue; continue;
} }
let collapsed = state.collapsed.contains(&section); let collapsed = state.collapsed.contains(&section);
@@ -387,7 +421,7 @@ pub fn view<'a>(state: &'a SettingsViewState, locale: UiLocale) -> Element<'a, M
text(t(locale, "common.noResults")) text(t(locale, "common.noResults"))
.size(14) .size(14)
.color(Color::from_rgb(0.7, 0.72, 0.78)), .color(Color::from_rgb(0.7, 0.72, 0.78)),
button(text(t(locale, "common.clear")).size(13)) keyboard::button(text(t(locale, "common.clear")).size(13))
.on_press(Message::Settings(SettingsMsg::SearchChanged(String::new()))) .on_press(Message::Settings(SettingsMsg::SearchChanged(String::new())))
.style(inputs::secondary_button) .style(inputs::secondary_button)
.padding([6, 12]), .padding([6, 12]),
@@ -423,7 +457,7 @@ fn render_section<'a>(
locale: UiLocale, locale: UiLocale,
) -> Element<'a, Message> { ) -> Element<'a, Message> {
let toggle_char = if collapsed { "\u{25B6}" } else { "\u{25BC}" }; let toggle_char = if collapsed { "\u{25B6}" } else { "\u{25BC}" };
let header = button( let header = keyboard::button(
row![ row![
text(toggle_char).size(12), text(toggle_char).size(12),
text(label.to_string()).size(14).color(Color::WHITE), text(label.to_string()).size(14).color(Color::WHITE),
@@ -434,6 +468,7 @@ fn render_section<'a>(
.on_press(Message::Settings(SettingsMsg::ToggleSection( .on_press(Message::Settings(SettingsMsg::ToggleSection(
section.clone(), section.clone(),
))) )))
.hotkey(section.access_key())
.padding([6, 8]) .padding([6, 8])
.width(Length::Fill) .width(Length::Fill)
.style(inputs::disclosure_button); .style(inputs::disclosure_button);
@@ -478,11 +513,11 @@ fn section_project<'a>(state: &'a SettingsViewState, locale: UiLocale) -> Elemen
inputs::labeled_input(&t(locale, "settings.dataPath"), "", &state.data_path, |s| { inputs::labeled_input(&t(locale, "settings.dataPath"), "", &state.data_path, |s| {
Message::Settings(SettingsMsg::DataPathChanged(s)) Message::Settings(SettingsMsg::DataPathChanged(s))
},), },),
button(text(t(locale, "settings.browse")).size(12)) keyboard::button(text(t(locale, "settings.browse")).size(12))
.on_press(Message::Settings(SettingsMsg::BrowseDataPath)) .on_press(Message::Settings(SettingsMsg::BrowseDataPath))
.style(inputs::secondary_button) .style(inputs::secondary_button)
.padding([6, 12]), .padding([6, 12]),
button(text(t(locale, "settings.reset")).size(12)) keyboard::button(text(t(locale, "settings.reset")).size(12))
.on_press(Message::Settings(SettingsMsg::ResetDataPath)) .on_press(Message::Settings(SettingsMsg::ResetDataPath))
.style(inputs::secondary_button) .style(inputs::secondary_button)
.padding([6, 12]), .padding([6, 12]),
@@ -559,11 +594,11 @@ fn section_project<'a>(state: &'a SettingsViewState, locale: UiLocale) -> Elemen
|row| Message::Settings(SettingsMsg::BlogmarkCategoryChanged(row.name)), |row| Message::Settings(SettingsMsg::BlogmarkCategoryChanged(row.name)),
); );
let copy_blogmark_bookmarklet = let copy_blogmark_bookmarklet =
button(text(t(locale, "settings.copyBlogmarkBookmarklet")).size(13)) keyboard::button(text(t(locale, "settings.copyBlogmarkBookmarklet")).size(13))
.on_press(Message::Settings(SettingsMsg::CopyBlogmarkBookmarklet)) .on_press(Message::Settings(SettingsMsg::CopyBlogmarkBookmarklet))
.style(inputs::secondary_button) .style(inputs::secondary_button)
.padding([6, 16]); .padding([6, 16]);
let save = button(text(t(locale, "common.save")).size(13)) let save = keyboard::button(text(t(locale, "common.save")).size(13))
.on_press(Message::Settings(SettingsMsg::SaveProject)) .on_press(Message::Settings(SettingsMsg::SaveProject))
.style(inputs::primary_button) .style(inputs::primary_button)
.padding([6, 16]); .padding([6, 16]);
@@ -619,7 +654,7 @@ fn section_editor<'a>(state: &'a SettingsViewState, locale: UiLocale) -> Element
state.hide_unchanged_regions, state.hide_unchanged_regions,
|b| Message::Settings(SettingsMsg::HideUnchangedRegionsChanged(b)), |b| Message::Settings(SettingsMsg::HideUnchangedRegionsChanged(b)),
); );
let save = button(text(t(locale, "common.save")).size(13)) let save = keyboard::button(text(t(locale, "common.save")).size(13))
.on_press(Message::Settings(SettingsMsg::SaveEditor)) .on_press(Message::Settings(SettingsMsg::SaveEditor))
.style(inputs::primary_button) .style(inputs::primary_button)
.padding([6, 16]); .padding([6, 16]);
@@ -656,11 +691,11 @@ fn section_ai<'a>(state: &'a SettingsViewState, locale: UiLocale) -> Element<'a,
] ]
.spacing(4); .spacing(4);
let btns = row![ let btns = row![
button(text(t(locale, "common.save")).size(13)) keyboard::button(text(t(locale, "common.save")).size(13))
.on_press(Message::Settings(SettingsMsg::SaveAi)) .on_press(Message::Settings(SettingsMsg::SaveAi))
.style(inputs::primary_button) .style(inputs::primary_button)
.padding([6, 16]), .padding([6, 16]),
button(text(t(locale, "settings.resetToDefault")).size(13)) keyboard::button(text(t(locale, "settings.resetToDefault")).size(13))
.on_press(Message::Settings(SettingsMsg::ResetSystemPrompt)) .on_press(Message::Settings(SettingsMsg::ResetSystemPrompt))
.style(inputs::secondary_button) .style(inputs::secondary_button)
.padding([6, 16]), .padding([6, 16]),
@@ -727,7 +762,7 @@ fn ai_mode_block<'a>(
Message::Settings(SettingsMsg::AiEndpointUrlChanged(kind, value)) Message::Settings(SettingsMsg::AiEndpointUrlChanged(kind, value))
} }
), ),
button(text(t(locale, "settings.refreshModels")).size(13)) keyboard::button(text(t(locale, "settings.refreshModels")).size(13))
.on_press(Message::Settings(SettingsMsg::RefreshAiModels(kind))) .on_press(Message::Settings(SettingsMsg::RefreshAiModels(kind)))
.style(inputs::secondary_button) .style(inputs::secondary_button)
.padding([6, 16]), .padding([6, 16]),
@@ -772,7 +807,7 @@ fn ai_mode_block<'a>(
state.image_supports_vision, state.image_supports_vision,
move |value| Message::Settings(SettingsMsg::AiVisionChanged(kind, value)), move |value| Message::Settings(SettingsMsg::AiVisionChanged(kind, value)),
), ),
button(text(t(locale, "settings.testChat")).size(13)) keyboard::button(text(t(locale, "settings.testChat")).size(13))
.on_press(Message::Settings(SettingsMsg::TestAi(kind))) .on_press(Message::Settings(SettingsMsg::TestAi(kind)))
.style(inputs::secondary_button) .style(inputs::secondary_button)
.padding([6, 16]), .padding([6, 16]),
@@ -815,11 +850,11 @@ fn section_publishing<'a>(state: &'a SettingsViewState, locale: UiLocale) -> Ele
|s| Message::Settings(SettingsMsg::SshRemotePathChanged(s)), |s| Message::Settings(SettingsMsg::SshRemotePathChanged(s)),
); );
let btns = row![ let btns = row![
button(text(t(locale, "common.save")).size(13)) keyboard::button(text(t(locale, "common.save")).size(13))
.on_press(Message::Settings(SettingsMsg::SavePublishing)) .on_press(Message::Settings(SettingsMsg::SavePublishing))
.style(inputs::primary_button) .style(inputs::primary_button)
.padding([6, 16]), .padding([6, 16]),
button(text(t(locale, "settings.clear")).size(13)) keyboard::button(text(t(locale, "settings.clear")).size(13))
.on_press(Message::Settings(SettingsMsg::ClearPublishing)) .on_press(Message::Settings(SettingsMsg::ClearPublishing))
.style(inputs::danger_button) .style(inputs::danger_button)
.padding([6, 16]), .padding([6, 16]),
@@ -834,37 +869,37 @@ fn section_publishing<'a>(state: &'a SettingsViewState, locale: UiLocale) -> Ele
fn section_data<'a>(locale: UiLocale) -> Element<'a, Message> { fn section_data<'a>(locale: UiLocale) -> Element<'a, Message> {
let rebuild_btns = column![ let rebuild_btns = column![
button(text(t(locale, "settings.rebuildPosts")).size(13)) keyboard::button(text(t(locale, "settings.rebuildPosts")).size(13))
.on_press(Message::Settings(SettingsMsg::RebuildPosts)) .on_press(Message::Settings(SettingsMsg::RebuildPosts))
.style(inputs::secondary_button) .style(inputs::secondary_button)
.padding([6, 16]) .padding([6, 16])
.width(Length::Fill), .width(Length::Fill),
button(text(t(locale, "settings.rebuildMedia")).size(13)) keyboard::button(text(t(locale, "settings.rebuildMedia")).size(13))
.on_press(Message::Settings(SettingsMsg::RebuildMedia)) .on_press(Message::Settings(SettingsMsg::RebuildMedia))
.style(inputs::secondary_button) .style(inputs::secondary_button)
.padding([6, 16]) .padding([6, 16])
.width(Length::Fill), .width(Length::Fill),
button(text(t(locale, "settings.rebuildScripts")).size(13)) keyboard::button(text(t(locale, "settings.rebuildScripts")).size(13))
.on_press(Message::Settings(SettingsMsg::RebuildScripts)) .on_press(Message::Settings(SettingsMsg::RebuildScripts))
.style(inputs::secondary_button) .style(inputs::secondary_button)
.padding([6, 16]) .padding([6, 16])
.width(Length::Fill), .width(Length::Fill),
button(text(t(locale, "settings.rebuildTemplates")).size(13)) keyboard::button(text(t(locale, "settings.rebuildTemplates")).size(13))
.on_press(Message::Settings(SettingsMsg::RebuildTemplates)) .on_press(Message::Settings(SettingsMsg::RebuildTemplates))
.style(inputs::secondary_button) .style(inputs::secondary_button)
.padding([6, 16]) .padding([6, 16])
.width(Length::Fill), .width(Length::Fill),
button(text(t(locale, "settings.rebuildLinks")).size(13)) keyboard::button(text(t(locale, "settings.rebuildLinks")).size(13))
.on_press(Message::Settings(SettingsMsg::RebuildLinks)) .on_press(Message::Settings(SettingsMsg::RebuildLinks))
.style(inputs::secondary_button) .style(inputs::secondary_button)
.padding([6, 16]) .padding([6, 16])
.width(Length::Fill), .width(Length::Fill),
button(text(t(locale, "settings.rebuildSearchIndex")).size(13)) keyboard::button(text(t(locale, "settings.rebuildSearchIndex")).size(13))
.on_press(Message::Settings(SettingsMsg::RebuildSearchIndex)) .on_press(Message::Settings(SettingsMsg::RebuildSearchIndex))
.style(inputs::secondary_button) .style(inputs::secondary_button)
.padding([6, 16]) .padding([6, 16])
.width(Length::Fill), .width(Length::Fill),
button(text(t(locale, "settings.regenerateThumbnails")).size(13)) keyboard::button(text(t(locale, "settings.regenerateThumbnails")).size(13))
.on_press(Message::Settings(SettingsMsg::RegenerateThumbnails)) .on_press(Message::Settings(SettingsMsg::RegenerateThumbnails))
.style(inputs::secondary_button) .style(inputs::secondary_button)
.padding([6, 16]) .padding([6, 16])
@@ -872,12 +907,12 @@ fn section_data<'a>(locale: UiLocale) -> Element<'a, Message> {
] ]
.spacing(4); .spacing(4);
let open = button(text(t(locale, "settings.openDataFolder")).size(13)) let open = keyboard::button(text(t(locale, "settings.openDataFolder")).size(13))
.on_press(Message::Settings(SettingsMsg::OpenDataFolder)) .on_press(Message::Settings(SettingsMsg::OpenDataFolder))
.style(inputs::secondary_button) .style(inputs::secondary_button)
.padding([6, 16]); .padding([6, 16]);
let install_cli = button(text(t(locale, "settings.installCli")).size(13)) let install_cli = keyboard::button(text(t(locale, "settings.installCli")).size(13))
.on_press(Message::Settings(SettingsMsg::InstallCli)) .on_press(Message::Settings(SettingsMsg::InstallCli))
.style(inputs::secondary_button) .style(inputs::secondary_button)
.padding([6, 16]); .padding([6, 16]);
@@ -900,14 +935,17 @@ fn section_mcp<'a>(state: &'a SettingsViewState, locale: UiLocale) -> Element<'a
inputs::LABEL_COLOR inputs::LABEL_COLOR
}; };
let server = column![ let server = column![
keyboard::focusable(
iced::widget::checkbox(t(locale, "settings.mcpEnable"), state.mcp_enabled) iced::widget::checkbox(t(locale, "settings.mcpEnable"), state.mcp_enabled)
.on_toggle(|value| Message::Settings(SettingsMsg::McpEnabledChanged(value))), .on_toggle(|value| Message::Settings(SettingsMsg::McpEnabledChanged(value))),
true,
),
row![ row![
text(status).size(13).color(status_color), text(status).size(13).color(status_color),
text(state.mcp_endpoint.clone()) text(state.mcp_endpoint.clone())
.size(12) .size(12)
.color(inputs::LABEL_COLOR), .color(inputs::LABEL_COLOR),
button(text(t(locale, "settings.mcpRefresh")).size(12)) keyboard::button(text(t(locale, "settings.mcpRefresh")).size(12))
.on_press(Message::Settings(SettingsMsg::McpRefresh)) .on_press(Message::Settings(SettingsMsg::McpRefresh))
.style(inputs::secondary_button) .style(inputs::secondary_button)
.padding([5, 10]), .padding([5, 10]),
@@ -940,13 +978,13 @@ fn section_mcp<'a>(state: &'a SettingsViewState, locale: UiLocale) -> Element<'a
] ]
.spacing(2) .spacing(2)
.width(Length::Fill), .width(Length::Fill),
button(text(t(locale, "settings.mcpApprove")).size(12)) keyboard::button(text(t(locale, "settings.mcpApprove")).size(12))
.on_press(Message::Settings(SettingsMsg::McpProposalAccepted( .on_press(Message::Settings(SettingsMsg::McpProposalAccepted(
proposal.id.clone() proposal.id.clone()
))) )))
.style(inputs::primary_button) .style(inputs::primary_button)
.padding([5, 10]), .padding([5, 10]),
button(text(t(locale, "settings.mcpReject")).size(12)) keyboard::button(text(t(locale, "settings.mcpReject")).size(12))
.on_press(Message::Settings(SettingsMsg::McpProposalRejected( .on_press(Message::Settings(SettingsMsg::McpProposalRejected(
proposal.id.clone() proposal.id.clone()
))) )))
@@ -964,10 +1002,13 @@ fn section_mcp<'a>(state: &'a SettingsViewState, locale: UiLocale) -> Element<'a
let agents = column(state.mcp_agents.iter().map(|agent| { let agents = column(state.mcp_agents.iter().map(|agent| {
let configured = agent.configured; let configured = agent.configured;
column![ column![
keyboard::focusable(
iced::widget::checkbox(agent.label.clone(), configured).on_toggle({ iced::widget::checkbox(agent.label.clone(), configured).on_toggle({
let agent = agent.agent; let agent = agent.agent;
move |_| Message::Settings(SettingsMsg::McpAgentToggled(agent)) move |_| Message::Settings(SettingsMsg::McpAgentToggled(agent))
}), }),
true,
),
text(agent.config_path.clone()) text(agent.config_path.clone())
.size(11) .size(11)
.color(inputs::LABEL_COLOR), .color(inputs::LABEL_COLOR),

View File

@@ -1,7 +1,9 @@
use crate::components::keyboard;
use iced::widget::button;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use iced::widget::text::Shaping; use iced::widget::text::Shaping;
use iced::widget::{Space, button, column, container, image, row, scrollable, text, text_input}; use iced::widget::{Space, column, container, image, row, scrollable, text, text_input};
use iced::{Background, Border, Color, Element, Length, Theme}; use iced::{Background, Border, Color, Element, Length, Theme};
use bds_core::i18n::UiLocale; use bds_core::i18n::UiLocale;
@@ -301,10 +303,10 @@ fn row_delete_style(_theme: &Theme, status: button::Status) -> button::Style {
/// Per sidebar_views.allium *ListItemEntry RowLayout: right-aligned, visible /// Per sidebar_views.allium *ListItemEntry RowLayout: right-aligned, visible
/// only on row hover, routed to the row's delete message. /// only on row hover, routed to the row's delete message.
fn with_row_delete( fn with_row_delete(
open_button: iced::widget::Button<'static, Message>, open_button: impl Into<Element<'static, Message>>,
on_delete: Message, on_delete: Message,
) -> Element<'static, Message> { ) -> Element<'static, Message> {
let delete_button = button( let delete_button = keyboard::button(
text("\u{2715}") // ✕ text("\u{2715}") // ✕
.size(11) .size(11)
.shaping(Shaping::Advanced), .shaping(Shaping::Advanced),
@@ -313,7 +315,7 @@ fn with_row_delete(
.padding([2, 6]) .padding([2, 6])
.style(row_delete_style); .style(row_delete_style);
iced::widget::hover( iced::widget::hover(
open_button, open_button.into(),
container(delete_button) container(delete_button)
.width(Length::Fill) .width(Length::Fill)
.height(Length::Fill) .height(Length::Fill)
@@ -362,7 +364,7 @@ fn calendar_widget(
} else { } else {
calendar_style calendar_style
}; };
let year_btn = button(text(label).size(11).shaping(Shaping::Advanced)) let year_btn = keyboard::button(text(label).size(11).shaping(Shaping::Advanced))
.on_press(if year_selected { .on_press(if year_selected {
on_year_clone(None) on_year_clone(None)
} else { } else {
@@ -384,7 +386,7 @@ fn calendar_widget(
} else { } else {
calendar_style calendar_style
}; };
let month_btn = button(text(label).size(10).shaping(Shaping::Advanced)) let month_btn = keyboard::button(text(label).size(10).shaping(Shaping::Advanced))
.on_press(if month_selected { .on_press(if month_selected {
on_month_clone(None) on_month_clone(None)
} else { } else {
@@ -428,7 +430,7 @@ fn chip_selector(
} else { } else {
chip_style chip_style
}; };
let chip = button(text(tag.clone()).size(10).shaping(Shaping::Advanced)) let chip = keyboard::button(text(tag.clone()).size(10).shaping(Shaping::Advanced))
.on_press(on_toggle_clone(tag_clone)) .on_press(on_toggle_clone(tag_clone))
.padding([2, 6]) .padding([2, 6])
.style(style_fn); .style(style_fn);
@@ -476,7 +478,7 @@ fn single_select_chip_selector(
}; };
let value = value.clone(); let value = value.clone();
let on_toggle = on_toggle.clone(); let on_toggle = on_toggle.clone();
button(text(display.clone()).size(10).shaping(Shaping::Advanced)) keyboard::button(text(display.clone()).size(10).shaping(Shaping::Advanced))
.on_press(if is_selected { .on_press(if is_selected {
on_toggle(None) on_toggle(None)
} else { } else {
@@ -599,7 +601,7 @@ fn post_filter_panel(
// Clear all filters button // Clear all filters button
if filter.has_active_filters() { if filter.has_active_filters() {
sections.push( sections.push(
button( keyboard::button(
text(t(locale, "sidebar.filter.clearAll")) text(t(locale, "sidebar.filter.clearAll"))
.size(10) .size(10)
.shaping(Shaping::Advanced), .shaping(Shaping::Advanced),
@@ -645,7 +647,7 @@ fn media_filter_panel(filter: &MediaFilter, locale: UiLocale) -> Element<'static
if filter.has_active_filters() { if filter.has_active_filters() {
sections.push( sections.push(
button( keyboard::button(
text(t(locale, "sidebar.filter.clearAll")) text(t(locale, "sidebar.filter.clearAll"))
.size(10) .size(10)
.shaping(Shaping::Advanced), .shaping(Shaping::Advanced),
@@ -711,7 +713,7 @@ pub fn view(
row![ row![
header, header,
Space::with_width(Length::Fill), Space::with_width(Length::Fill),
button( keyboard::button(
text(t(locale, "common.add")) text(t(locale, "common.add"))
.size(11) .size(11)
.shaping(Shaping::Advanced) .shaping(Shaping::Advanced)
@@ -752,7 +754,8 @@ pub fn view(
} else { } else {
"\u{25BC}" // ▼ toggle icon "\u{25BC}" // ▼ toggle icon
}; };
let filter_toggle = button(text(toggle_label).size(11).shaping(Shaping::Advanced)) let filter_toggle =
keyboard::button(text(toggle_label).size(11).shaping(Shaping::Advanced))
.on_press(Message::TogglePostFilterPanel) .on_press(Message::TogglePostFilterPanel)
.padding([4, 6]) .padding([4, 6])
.style(toggle_style); .style(toggle_style);
@@ -817,7 +820,7 @@ pub fn view(
} else { } else {
item_style item_style
}; };
button( keyboard::button(
container(column![label_text, date_text].spacing(1)) container(column![label_text, date_text].spacing(1))
.width(Length::Fill) .width(Length::Fill)
.clip(true), .clip(true),
@@ -901,7 +904,8 @@ pub fn view(
} else { } else {
"\u{25BC}" // ▼ toggle icon "\u{25BC}" // ▼ toggle icon
}; };
let filter_toggle = button(text(toggle_label).size(11).shaping(Shaping::Advanced)) let filter_toggle =
keyboard::button(text(toggle_label).size(11).shaping(Shaping::Advanced))
.on_press(Message::ToggleMediaFilterPanel) .on_press(Message::ToggleMediaFilterPanel)
.padding([4, 6]) .padding([4, 6])
.style(toggle_style); .style(toggle_style);
@@ -995,7 +999,7 @@ pub fn view(
.spacing(8) .spacing(8)
.align_y(iced::Alignment::Center); .align_y(iced::Alignment::Center);
button(container(content).width(Length::Fill).clip(true)) keyboard::button(container(content).width(Length::Fill).clip(true))
.on_press(Message::OpenTab(Tab { .on_press(Message::OpenTab(Tab {
id: m.id.clone(), id: m.id.clone(),
tab_type: TabType::Media, tab_type: TabType::Media,
@@ -1046,7 +1050,7 @@ pub fn view(
} else { } else {
item_style item_style
}; };
let open_button = button( let open_button = keyboard::button(
container(column![label_text, date_text].spacing(1)) container(column![label_text, date_text].spacing(1))
.width(Length::Fill) .width(Length::Fill)
.clip(true), .clip(true),
@@ -1097,7 +1101,7 @@ pub fn view(
} else { } else {
item_style item_style
}; };
let open_button = button( let open_button = keyboard::button(
container(column![label_text, date_text].spacing(1)) container(column![label_text, date_text].spacing(1))
.width(Length::Fill) .width(Length::Fill)
.clip(true), .clip(true),
@@ -1145,7 +1149,7 @@ pub fn view(
.size(10) .size(10)
.shaping(Shaping::Advanced) .shaping(Shaping::Advanced)
.color(muted); .color(muted);
let open_button = button( let open_button = keyboard::button(
container( container(
column![ column![
text(definition_name).size(12).shaping(Shaping::Advanced), text(definition_name).size(12).shaping(Shaping::Advanced),
@@ -1197,7 +1201,7 @@ pub fn view(
.size(10) .size(10)
.shaping(Shaping::Advanced) .shaping(Shaping::Advanced)
.color(muted); .color(muted);
let open_button = button( let open_button = keyboard::button(
container( container(
column![ column![
text(conversation.title.clone()) text(conversation.title.clone())
@@ -1257,7 +1261,7 @@ pub fn view(
is_dirty: false, is_dirty: false,
}) })
}; };
button(container(label_text).width(Length::Fill)) keyboard::button(container(label_text).width(Length::Fill))
.on_press(msg) .on_press(msg)
.padding([5, 8]) .padding([5, 8])
.width(Length::Fill) .width(Length::Fill)
@@ -1291,7 +1295,7 @@ pub fn view(
.map(|(key, section)| { .map(|(key, section)| {
let label = t(locale, key); let label = t(locale, key);
let label_text = text(label).size(12).shaping(Shaping::Advanced); let label_text = text(label).size(12).shaping(Shaping::Advanced);
button(container(label_text).width(Length::Fill)) keyboard::button(container(label_text).width(Length::Fill))
.on_press(Message::OpenTagsSection(*section)) .on_press(Message::OpenTagsSection(*section))
.padding([5, 8]) .padding([5, 8])
.width(Length::Fill) .width(Length::Fill)

View File

@@ -1,5 +1,6 @@
use crate::components::keyboard;
use iced::widget::text::Shaping; use iced::widget::text::Shaping;
use iced::widget::{button, column, container, row, scrollable, text}; use iced::widget::{column, container, row, scrollable, text};
use iced::{Background, Color, Element, Length, Theme}; use iced::{Background, Color, Element, Length, Theme};
use bds_core::i18n::UiLocale; use bds_core::i18n::UiLocale;
@@ -21,7 +22,7 @@ pub struct SiteValidationState {
pub fn view<'a>(state: &'a SiteValidationState, locale: UiLocale) -> Element<'a, Message> { pub fn view<'a>(state: &'a SiteValidationState, locale: UiLocale) -> Element<'a, Message> {
let run_button = if state.is_running { let run_button = if state.is_running {
button( keyboard::button(
text(t(locale, "siteValidation.running")) text(t(locale, "siteValidation.running"))
.size(13) .size(13)
.shaping(Shaping::Advanced), .shaping(Shaping::Advanced),
@@ -29,12 +30,13 @@ pub fn view<'a>(state: &'a SiteValidationState, locale: UiLocale) -> Element<'a,
.style(inputs::primary_button) .style(inputs::primary_button)
.padding([6, 16]) .padding([6, 16])
} else { } else {
button( keyboard::button(
text(t(locale, "siteValidation.run")) text(t(locale, "siteValidation.run"))
.size(13) .size(13)
.shaping(Shaping::Advanced), .shaping(Shaping::Advanced),
) )
.on_press(Message::RunSiteValidation) .on_press(Message::RunSiteValidation)
.hotkey('r')
.style(inputs::primary_button) .style(inputs::primary_button)
.padding([6, 16]) .padding([6, 16])
}; };
@@ -42,7 +44,7 @@ pub fn view<'a>(state: &'a SiteValidationState, locale: UiLocale) -> Element<'a,
|| !state.extra_files.is_empty() || !state.extra_files.is_empty()
|| !state.stale_files.is_empty(); || !state.stale_files.is_empty();
let apply_button = if state.is_applying { let apply_button = if state.is_applying {
button( keyboard::button(
text(t(locale, "siteValidation.applying")) text(t(locale, "siteValidation.applying"))
.size(13) .size(13)
.shaping(Shaping::Advanced), .shaping(Shaping::Advanced),
@@ -50,16 +52,17 @@ pub fn view<'a>(state: &'a SiteValidationState, locale: UiLocale) -> Element<'a,
.style(inputs::secondary_button) .style(inputs::secondary_button)
.padding([6, 16]) .padding([6, 16])
} else if !state.is_running && state.error_message.is_none() && has_issues { } else if !state.is_running && state.error_message.is_none() && has_issues {
button( keyboard::button(
text(t(locale, "siteValidation.apply")) text(t(locale, "siteValidation.apply"))
.size(13) .size(13)
.shaping(Shaping::Advanced), .shaping(Shaping::Advanced),
) )
.on_press(Message::ApplySiteValidation) .on_press(Message::ApplySiteValidation)
.hotkey('a')
.style(inputs::primary_button) .style(inputs::primary_button)
.padding([6, 16]) .padding([6, 16])
} else { } else {
button( keyboard::button(
text(t(locale, "siteValidation.apply")) text(t(locale, "siteValidation.apply"))
.size(13) .size(13)
.shaping(Shaping::Advanced), .shaping(Shaping::Advanced),

View File

@@ -1,5 +1,7 @@
use crate::components::keyboard;
use iced::widget::button;
use iced::widget::text::Shaping; use iced::widget::text::Shaping;
use iced::widget::{Space, button, container, row, text}; use iced::widget::{Space, container, row, text};
use iced::{Alignment, Background, Border, Color, Element, Length, Theme}; use iced::{Alignment, Background, Border, Color, Element, Length, Theme};
use bds_core::engine::task::TaskStatus; use bds_core::engine::task::TaskStatus;
@@ -235,7 +237,7 @@ pub fn view(
); );
// Airplane mode toggle — ✈ icon // Airplane mode toggle — ✈ icon
let airplane_btn = button(text("\u{2708}").size(13).shaping(Shaping::Advanced)) let airplane_btn = keyboard::button(text("\u{2708}").size(13).shaping(Shaping::Advanced))
.on_press(Message::SetOfflineMode(!offline_mode)) .on_press(Message::SetOfflineMode(!offline_mode))
.padding([2, 4]) .padding([2, 4])
.style(if offline_mode { .style(if offline_mode {
@@ -249,7 +251,7 @@ pub fn view(
.size(14) .size(14)
.shaping(Shaping::Advanced); .shaping(Shaping::Advanced);
let locale_trigger = button(trigger_flag) let locale_trigger = keyboard::button(trigger_flag)
.on_press(Message::ToggleLocaleDropdown) .on_press(Message::ToggleLocaleDropdown)
.padding([1, 4]) .padding([1, 4])
.style(dropdown_trigger); .style(dropdown_trigger);

View File

@@ -1,5 +1,7 @@
use crate::components::keyboard;
use iced::widget::button;
use iced::widget::text::Shaping; use iced::widget::text::Shaping;
use iced::widget::{Column, Space, button, column, container, row, scrollable, text}; use iced::widget::{Column, Space, column, container, row, scrollable, text};
use iced::{Background, Border, Color, Element, Length, Radians, Theme, gradient}; use iced::{Background, Border, Color, Element, Length, Radians, Theme, gradient};
use bds_core::i18n::UiLocale; use bds_core::i18n::UiLocale;
@@ -220,7 +222,7 @@ fn swatch(background: Background, width_portion: u16) -> Element<'static, Messag
fn theme_button<'a>(theme: &StyleTheme, selected_theme: &str) -> Element<'a, Message> { fn theme_button<'a>(theme: &StyleTheme, selected_theme: &str) -> Element<'a, Message> {
let selected = theme.name == selected_theme; let selected = theme.name == selected_theme;
let theme_name = theme.name.to_string(); let theme_name = theme.name.to_string();
button( keyboard::button(
column![ column![
row![ row![
swatch(theme_accent_background(theme), 2), swatch(theme_accent_background(theme), 2),
@@ -318,13 +320,13 @@ pub fn view<'a>(
.size(13) .size(13)
.shaping(Shaping::Advanced); .shaping(Shaping::Advanced);
let apply_button: Element<'a, Message> = if state.can_apply() { let apply_button: Element<'a, Message> = if state.can_apply() {
button(apply_label) keyboard::button(apply_label)
.on_press(Message::Style(StyleMsg::Apply)) .on_press(Message::Style(StyleMsg::Apply))
.padding([8, 16]) .padding([8, 16])
.style(inputs::primary_button) .style(inputs::primary_button)
.into() .into()
} else { } else {
button(apply_label) keyboard::button(apply_label)
.padding([8, 16]) .padding([8, 16])
.style(inputs::primary_button) .style(inputs::primary_button)
.into() .into()

View File

@@ -1,7 +1,9 @@
use crate::components::keyboard;
use iced::widget::button;
use iced::widget::scrollable::Direction; use iced::widget::scrollable::Direction;
use iced::widget::text::Shaping; use iced::widget::text::Shaping;
use iced::widget::tooltip::Position; use iced::widget::tooltip::Position;
use iced::widget::{Space, button, container, row, scrollable, text, tooltip}; use iced::widget::{Space, container, row, scrollable, text, tooltip};
use iced::{Background, Border, Color, Element, Font, Length, Theme}; use iced::{Background, Border, Color, Element, Font, Length, Theme};
use bds_core::i18n::UiLocale; use bds_core::i18n::UiLocale;
@@ -177,9 +179,20 @@ pub fn view(tabs: &[Tab], active_tab: Option<&str>, locale: UiLocale) -> Element
.width(Length::Fill) .width(Length::Fill)
.clip(true); .clip(true);
// Keep the tooltip around non-interactive title content so it does
// not hide the tab and close controls from widget operations.
let tooltip_text = build_tooltip_text(tab, locale);
let title_area = tooltip(
title_area,
text(tooltip_text).size(11).shaping(Shaping::Advanced),
Position::Bottom,
)
.gap(4)
.style(inputs::tooltip_style);
let label = row![ let label = row![
title_area, title_area,
button(text("\u{2715}").size(10).shaping(Shaping::Advanced)) keyboard::button(text("\u{2715}").size(10).shaping(Shaping::Advanced))
.on_press(Message::CloseTab(close_id)) .on_press(Message::CloseTab(close_id))
.padding(2) .padding(2)
.style(close_style), .style(close_style),
@@ -188,24 +201,12 @@ pub fn view(tabs: &[Tab], active_tab: Option<&str>, locale: UiLocale) -> Element
.align_y(iced::Alignment::Center); .align_y(iced::Alignment::Center);
// tabs.allium: tab_min_width=100, tab_max_width=160 // tabs.allium: tab_min_width=100, tab_max_width=160
let tab_btn = button(label) keyboard::button(label)
.on_press(Message::SelectTab(tab_id)) .on_press(Message::SelectTab(tab_id))
.padding([6, 8]) .padding([6, 8])
.width(Length::Fixed(TAB_WIDTH)) .width(Length::Fixed(TAB_WIDTH))
.style(if is_active { tab_active } else { tab_inactive }); .style(if is_active { tab_active } else { tab_inactive })
.into()
// tabs.allium tooltip: title + "(Preview)" if transient + "* Modified" if dirty
let tooltip_text = build_tooltip_text(tab, locale);
let tip: Element<'static, Message> = tooltip(
tab_btn,
text(tooltip_text).size(11).shaping(Shaping::Advanced),
Position::Bottom,
)
.gap(4)
.style(inputs::tooltip_style)
.into();
tip
}) })
.collect(); .collect();

View File

@@ -1,7 +1,9 @@
use crate::components::keyboard;
use iced::widget::button;
use std::collections::HashMap; use std::collections::HashMap;
use iced::widget::{ use iced::widget::{
Space, button, checkbox, column, container, pick_list, row, scrollable, text, text_input, Space, checkbox, column, container, pick_list, row, scrollable, text, text_input,
}; };
use iced::{Alignment, Background, Color, Element, Length, Theme}; use iced::{Alignment, Background, Color, Element, Length, Theme};
@@ -247,27 +249,32 @@ fn view_categories<'a>(state: &'a SettingsViewState, locale: UiLocale) -> Elemen
})); }));
let name = category.name.clone(); let name = category.name.clone();
cells.push( cells.push(
container( container(keyboard::focusable(
checkbox("", category.render_in_lists).on_toggle(move |value| { checkbox("", category.render_in_lists).on_toggle(move |value| {
Message::Settings(SettingsMsg::CategoryRenderInListsChanged( Message::Settings(SettingsMsg::CategoryRenderInListsChanged(
name.clone(), name.clone(),
value, value,
)) ))
}), }),
) true,
))
.width(Length::Fixed(TOGGLE_WIDTH)) .width(Length::Fixed(TOGGLE_WIDTH))
.into(), .into(),
); );
let name = category.name.clone(); let name = category.name.clone();
cells.push( cells.push(
container(checkbox("", category.show_title).on_toggle(move |value| { container(keyboard::focusable(
checkbox("", category.show_title).on_toggle(move |value| {
Message::Settings(SettingsMsg::CategoryShowTitleChanged(name.clone(), value)) Message::Settings(SettingsMsg::CategoryShowTitleChanged(name.clone(), value))
})) }),
true,
))
.width(Length::Fixed(TOGGLE_WIDTH)) .width(Length::Fixed(TOGGLE_WIDTH))
.into(), .into(),
); );
let name = category.name.clone(); let name = category.name.clone();
cells.push( cells.push(
keyboard::focusable(
pick_list( pick_list(
template_options.clone(), template_options.clone(),
Some(category.post_template_slug.clone()), Some(category.post_template_slug.clone()),
@@ -280,11 +287,14 @@ fn view_categories<'a>(state: &'a SettingsViewState, locale: UiLocale) -> Elemen
) )
.padding([7, 9]) .padding([7, 9])
.style(inputs::select_style) .style(inputs::select_style)
.width(Length::Fixed(TEMPLATE_WIDTH)) .width(Length::Fixed(TEMPLATE_WIDTH)),
true,
)
.into(), .into(),
); );
let name = category.name.clone(); let name = category.name.clone();
cells.push( cells.push(
keyboard::focusable(
pick_list( pick_list(
template_options.clone(), template_options.clone(),
Some(category.list_template_slug.clone()), Some(category.list_template_slug.clone()),
@@ -297,19 +307,21 @@ fn view_categories<'a>(state: &'a SettingsViewState, locale: UiLocale) -> Elemen
) )
.padding([7, 9]) .padding([7, 9])
.style(inputs::select_style) .style(inputs::select_style)
.width(Length::Fixed(TEMPLATE_WIDTH)) .width(Length::Fixed(TEMPLATE_WIDTH)),
true,
)
.into(), .into(),
); );
cells.push( cells.push(
container( container(
row![ row![
button(text(t(locale, "common.save")).size(12)) keyboard::button(text(t(locale, "common.save")).size(12))
.on_press(Message::Settings(SettingsMsg::SaveCategory( .on_press(Message::Settings(SettingsMsg::SaveCategory(
category.name.clone(), category.name.clone(),
))) )))
.style(inputs::primary_button) .style(inputs::primary_button)
.padding([6, 10]), .padding([6, 10]),
button(text(t(locale, "common.remove")).size(12)) keyboard::button(text(t(locale, "common.remove")).size(12))
.on_press_maybe((!category.is_protected).then(|| Message::Settings( .on_press_maybe((!category.is_protected).then(|| Message::Settings(
SettingsMsg::RemoveCategory(category.name.clone()), SettingsMsg::RemoveCategory(category.name.clone()),
))) )))
@@ -336,11 +348,11 @@ fn view_categories<'a>(state: &'a SettingsViewState, locale: UiLocale) -> Elemen
&state.new_category_name, &state.new_category_name,
|value| Message::Settings(SettingsMsg::AddCategoryNameChanged(value)), |value| Message::Settings(SettingsMsg::AddCategoryNameChanged(value)),
), ),
button(text(t(locale, "common.add")).size(13)) keyboard::button(text(t(locale, "common.add")).size(13))
.on_press(Message::Settings(SettingsMsg::AddCategory)) .on_press(Message::Settings(SettingsMsg::AddCategory))
.style(inputs::primary_button) .style(inputs::primary_button)
.padding([6, 12]), .padding([6, 12]),
button(text(t(locale, "settings.resetCategories")).size(13)) keyboard::button(text(t(locale, "settings.resetCategories")).size(13))
.on_press(Message::Settings(SettingsMsg::ResetCategoriesToDefaults)) .on_press(Message::Settings(SettingsMsg::ResetCategoriesToDefaults))
.style(inputs::secondary_button) .style(inputs::secondary_button)
.padding([6, 12]), .padding([6, 12]),
@@ -382,7 +394,7 @@ fn view_categories<'a>(state: &'a SettingsViewState, locale: UiLocale) -> Elemen
} }
fn section_tab<'a>(label: &str, active: bool, section: TagsSection) -> Element<'a, Message> { fn section_tab<'a>(label: &str, active: bool, section: TagsSection) -> Element<'a, Message> {
button(text(label.to_string()).size(13)) keyboard::button(text(label.to_string()).size(13))
.on_press(Message::Tags(TagsMsg::SetSection(section))) .on_press(Message::Tags(TagsMsg::SetSection(section)))
.padding([6, 12]) .padding([6, 12])
.style(if active { .style(if active {
@@ -443,7 +455,7 @@ fn view_cloud<'a>(state: &'a TagsViewState, locale: UiLocale) -> Element<'a, Mes
.selected_tags .selected_tags
.iter() .iter()
.any(|selected_id| selected_id == &tag.id); .any(|selected_id| selected_id == &tag.id);
button( keyboard::button(
row![ row![
text(&tag.name).size(font_size).color(Color::WHITE), text(&tag.name).size(font_size).color(Color::WHITE),
text(post_count.to_string()) text(post_count.to_string())
@@ -488,7 +500,7 @@ fn view_cloud<'a>(state: &'a TagsViewState, locale: UiLocale) -> Element<'a, Mes
)) ))
.size(12) .size(12)
.color(Color::from_rgb(0.75, 0.77, 0.82)), .color(Color::from_rgb(0.75, 0.77, 0.82)),
button(text(t(locale, "tags.clearSelection")).size(12)) keyboard::button(text(t(locale, "tags.clearSelection")).size(12))
.on_press(Message::Tags(TagsMsg::ClearSelection)) .on_press(Message::Tags(TagsMsg::ClearSelection))
.style(inputs::secondary_button) .style(inputs::secondary_button)
.padding([4, 8]), .padding([4, 8]),
@@ -533,7 +545,7 @@ fn view_manage<'a>(state: &'a TagsViewState, locale: UiLocale) -> Element<'a, Me
|value| Message::Tags(TagsMsg::CreateColorChanged(value)), |value| Message::Tags(TagsMsg::CreateColorChanged(value)),
), ),
color_swatches(locale, true), color_swatches(locale, true),
button(text(t(locale, "tags.createButton")).size(13)) keyboard::button(text(t(locale, "tags.createButton")).size(13))
.on_press_maybe( .on_press_maybe(
(!state.create_name.trim().is_empty()).then_some(Message::Tags(TagsMsg::CreateTag)) (!state.create_name.trim().is_empty()).then_some(Message::Tags(TagsMsg::CreateTag))
) )
@@ -573,7 +585,7 @@ fn view_manage<'a>(state: &'a TagsViewState, locale: UiLocale) -> Element<'a, Me
.get(&tag.name.to_lowercase()) .get(&tag.name.to_lowercase())
.copied() .copied()
.unwrap_or(0); .unwrap_or(0);
button( keyboard::button(
row![ row![
container(Space::new(12, 12)).style(move |_: &Theme| container::Style { container(Space::new(12, 12)).style(move |_: &Theme| container::Style {
background: Some(Background::Color(color)), background: Some(Background::Color(color)),
@@ -620,7 +632,7 @@ fn view_manage<'a>(state: &'a TagsViewState, locale: UiLocale) -> Element<'a, Me
.iter() .iter()
.find(|option| option.slug == editing.template_slug); .find(|option| option.slug == editing.template_slug);
let delete_button: Element<'a, Message> = let delete_button: Element<'a, Message> =
button(text(t(locale, "modal.confirmDelete.delete")).size(13)) keyboard::button(text(t(locale, "modal.confirmDelete.delete")).size(13))
.on_press(Message::Tags(TagsMsg::DeleteTag(editing.id.clone()))) .on_press(Message::Tags(TagsMsg::DeleteTag(editing.id.clone())))
.style(inputs::danger_button) .style(inputs::danger_button)
.padding([6, 16]) .padding([6, 16])
@@ -645,7 +657,7 @@ fn view_manage<'a>(state: &'a TagsViewState, locale: UiLocale) -> Element<'a, Me
|choice| Message::Tags(TagsMsg::EditTagTemplate(choice)), |choice| Message::Tags(TagsMsg::EditTagTemplate(choice)),
), ),
row![ row![
button(text(t(locale, "common.save")).size(13)) keyboard::button(text(t(locale, "common.save")).size(13))
.on_press(Message::Tags(TagsMsg::SaveTag)) .on_press(Message::Tags(TagsMsg::SaveTag))
.style(inputs::primary_button) .style(inputs::primary_button)
.padding([6, 16]), .padding([6, 16]),
@@ -744,7 +756,7 @@ fn view_merge<'a>(state: &'a TagsViewState, locale: UiLocale) -> Element<'a, Mes
.padding([4, 0]) .padding([4, 0])
.into() .into()
}, },
button(text(t(locale, "tags.merge")).size(13)) keyboard::button(text(t(locale, "tags.merge")).size(13))
.on_press_maybe( .on_press_maybe(
state state
.merge_target .merge_target
@@ -768,7 +780,7 @@ fn view_discover<'a>(_state: &'a TagsViewState, locale: UiLocale) -> Element<'a,
text(t(locale, "tags.discoverDescription")) text(t(locale, "tags.discoverDescription"))
.size(12) .size(12)
.color(Color::from_rgb(0.60, 0.60, 0.65)), .color(Color::from_rgb(0.60, 0.60, 0.65)),
button(text(t(locale, "tags.discoverButton")).size(13)) keyboard::button(text(t(locale, "tags.discoverButton")).size(13))
.on_press(Message::Tags(TagsMsg::SyncTags)) .on_press(Message::Tags(TagsMsg::SyncTags))
.style(inputs::primary_button) .style(inputs::primary_button)
.padding([6, 16]), .padding([6, 16]),
@@ -819,7 +831,7 @@ fn color_swatches<'a>(locale: UiLocale, create_mode: bool) -> Element<'a, Messag
} else { } else {
TagsMsg::EditTagColor((*hex).to_string()) TagsMsg::EditTagColor((*hex).to_string())
}; };
button(Space::new(18, 18)) keyboard::button(Space::new(18, 18))
.on_press(Message::Tags(msg)) .on_press(Message::Tags(msg))
.padding(0) .padding(0)
.style(move |_theme: &Theme, _status| button::Style { .style(move |_theme: &Theme, _status| button::Style {

View File

@@ -1,6 +1,7 @@
use crate::components::keyboard;
use std::cell::RefCell; use std::cell::RefCell;
use iced::widget::{Space, button, column, container, row, scrollable, text}; use iced::widget::{Space, column, container, row, scrollable, text};
use iced::{Color, Element, Length, Theme}; use iced::{Color, Element, Length, Theme};
use bds_core::i18n::UiLocale; use bds_core::i18n::UiLocale;
@@ -114,17 +115,17 @@ pub fn view<'a>(state: &'a TemplateEditorState, locale: UiLocale) -> Element<'a,
status_badge(&state.status), status_badge(&state.status),
], ],
vec![ vec![
button(text(t(locale, "common.save")).size(13)) keyboard::button(text(t(locale, "common.save")).size(13))
.on_press(Message::TemplateEditor(TemplateEditorMsg::Save)) .on_press(Message::TemplateEditor(TemplateEditorMsg::Save))
.style(inputs::primary_button) .style(inputs::primary_button)
.padding([6, 16]) .padding([6, 16])
.into(), .into(),
button(text(t(locale, "editor.validate")).size(13)) keyboard::button(text(t(locale, "editor.validate")).size(13))
.on_press(Message::TemplateEditor(TemplateEditorMsg::Validate)) .on_press(Message::TemplateEditor(TemplateEditorMsg::Validate))
.style(inputs::secondary_button) .style(inputs::secondary_button)
.padding([6, 16]) .padding([6, 16])
.into(), .into(),
button(text(t(locale, "modal.confirmDelete.delete")).size(13)) keyboard::button(text(t(locale, "modal.confirmDelete.delete")).size(13))
.on_press(Message::TemplateEditor(TemplateEditorMsg::Delete)) .on_press(Message::TemplateEditor(TemplateEditorMsg::Delete))
.style(inputs::danger_button) .style(inputs::danger_button)
.padding([6, 16]) .padding([6, 16])

View File

@@ -1,5 +1,7 @@
use crate::components::keyboard;
use iced::widget::button;
use iced::widget::text::Shaping; use iced::widget::text::Shaping;
use iced::widget::{Space, button, container, row, text}; use iced::widget::{Space, container, row, text};
use iced::{Alignment, Background, Border, Color, Element, Length, Padding, Theme}; use iced::{Alignment, Background, Border, Color, Element, Length, Padding, Theme};
use crate::app::Message; use crate::app::Message;
@@ -62,7 +64,7 @@ pub fn view(toasts: &[Toast]) -> Option<Element<'static, Message>> {
.iter() .iter()
.map(|toast| { .map(|toast| {
let level = toast.level; let level = toast.level;
let dismiss = button(text("\u{2715}").size(11).shaping(Shaping::Advanced)) let dismiss = keyboard::button(text("\u{2715}").size(11).shaping(Shaping::Advanced))
.on_press(Message::DismissToast(toast.id)) .on_press(Message::DismissToast(toast.id))
.padding([2, 4]) .padding([2, 4])
.style(dismiss_btn); .style(dismiss_btn);

View File

@@ -1,5 +1,6 @@
use crate::components::keyboard;
use iced::widget::text::Shaping; use iced::widget::text::Shaping;
use iced::widget::{Space, button, column, container, row, scrollable, text}; use iced::widget::{Space, column, container, row, scrollable, text};
use iced::{Color, Element, Length}; use iced::{Color, Element, Length};
use bds_core::engine::validate_translations::{ use bds_core::engine::validate_translations::{
@@ -19,7 +20,7 @@ pub struct TranslationValidationState {
} }
pub fn view<'a>(state: &'a TranslationValidationState, locale: UiLocale) -> Element<'a, Message> { pub fn view<'a>(state: &'a TranslationValidationState, locale: UiLocale) -> Element<'a, Message> {
let run = button(text(t(locale, "translationValidation.run")).size(13)) let run = keyboard::button(text(t(locale, "translationValidation.run")).size(13))
.on_press_maybe((!state.is_running).then_some(Message::ValidateTranslations)) .on_press_maybe((!state.is_running).then_some(Message::ValidateTranslations))
.style(inputs::primary_button) .style(inputs::primary_button)
.padding([6, 16]); .padding([6, 16]);

View File

@@ -1,8 +1,9 @@
use crate::components::keyboard;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use iced::widget::text::Shaping; use iced::widget::text::Shaping;
use iced::widget::{Space, button, column, container, mouse_area, row, stack, text}; use iced::widget::{Space, column, container, mouse_area, row, stack, text};
use iced::{Alignment, Background, Color, Element, Length, Padding, Theme}; use iced::{Alignment, Background, Color, Element, Length, Padding, Theme};
use bds_core::engine::git::GitCommit; use bds_core::engine::git::GitCommit;
@@ -305,7 +306,7 @@ pub fn view<'a>(
.map(|&l| { .map(|&l| {
let flag_text = text(l.flag_emoji()).size(16).shaping(Shaping::Advanced); let flag_text = text(l.flag_emoji()).size(16).shaping(Shaping::Advanced);
button(flag_text) keyboard::button(flag_text)
.on_press(Message::SetUiLocale(l)) .on_press(Message::SetUiLocale(l))
.padding([4, 8]) .padding([4, 8])
.style(status_bar::dropdown_item) .style(status_bar::dropdown_item)
@@ -371,15 +372,32 @@ pub fn view<'a>(
None None
}; };
// Only the top blocking layer participates in keyboard traversal/access keys.
let modal_is_active = active_modal.is_some();
let blocking_overlay = modal_is_active || overlay.is_some();
let base_layout: Element<'a, Message> = if blocking_overlay {
keyboard::suspend(base_layout).into()
} else {
base_layout
};
// Collect overlays: dropdowns and toasts // Collect overlays: dropdowns and toasts
let mut overlays: Vec<Element<'a, Message>> = Vec::new(); let mut overlays: Vec<Element<'a, Message>> = Vec::new();
if let Some(toast_overlay) = toast::view(toasts) { if let Some(toast_overlay) = toast::view(toasts) {
overlays.push(toast_overlay); overlays.push(if blocking_overlay {
keyboard::suspend(toast_overlay).into()
} else {
toast_overlay
});
} }
if let Some(overlay) = overlay { if let Some(overlay) = overlay {
overlays.push(overlay); overlays.push(if modal_is_active {
keyboard::suspend(overlay).into()
} else {
overlay
});
} }
// Modal overlay (highest z-index) // Modal overlay (highest z-index)

View File

@@ -342,6 +342,8 @@ surface MetadataDiffSurface {
@guarantee ScanAction @guarantee ScanAction
-- Scan/Rescan button at top. -- Scan/Rescan button at top.
-- Progress bar + message during scan. -- Progress bar + message during scan.
-- Control+Option+R (macOS) or Control+Alt+R runs the scan; repair buttons use Tab
-- and are activated with Enter or Space.
@guarantee EntityTabs @guarantee EntityTabs
-- Tabs: Posts, Media, Scripts, Templates — each with badge count of diffs. -- Tabs: Posts, Media, Scripts, Templates — each with badge count of diffs.
@@ -443,6 +445,8 @@ surface SiteValidationSurface {
@guarantee ApplyAction @guarantee ApplyAction
-- Apply button disabled when nothing to fix. -- Apply button disabled when nothing to fix.
-- Control+Option (macOS) or Control+Alt, followed by R, scans and A applies;
-- both remain in Tab order.
-- On apply: renders missing, deletes extra, re-renders updated. -- On apply: renders missing, deletes extra, re-renders updated.
-- Toast: "Validation applied: N rendered, N deleted". -- Toast: "Validation applied: N rendered, N deleted".
} }

View File

@@ -131,6 +131,7 @@ surface PostEditorSurface {
@guarantee MetadataSection @guarantee MetadataSection
-- Collapsible section. Starts expanded when title is empty. -- Collapsible section. Starts expanded when title is empty.
-- Control+Option+M (macOS) or Control+Alt+M toggles it outside focused text input.
-- Two-column layout. -- Two-column layout.
-- Left column: Title, Tags, Author, Language + detect button, -- Left column: Title, Tags, Author, Language + detect button,
-- Do Not Translate checkbox, Slug (read-only), Categories, -- Do Not Translate checkbox, Slug (read-only), Categories,
@@ -156,6 +157,7 @@ surface PostEditorSurface {
@guarantee ExcerptSection @guarantee ExcerptSection
-- Collapsible section with textarea (4 rows). -- Collapsible section with textarea (4 rows).
-- Control+Option+X (macOS) or Control+Alt+X toggles it outside focused text input.
@guarantee EditorBodyToolbar @guarantee EditorBodyToolbar
-- Toolbar: "Content" label, mode toggle (Markdown/Preview), -- Toolbar: "Content" label, mode toggle (Markdown/Preview),

View File

@@ -219,6 +219,7 @@ surface SettingsViewSurface {
@guarantee CollapsibleSections @guarantee CollapsibleSections
-- All 8 sections are collapsible. -- All 8 sections are collapsible.
-- Section visibility respects search filter. -- Section visibility respects search filter.
-- Each visible section exposes a unique Control+Option or Control+Alt access key.
} }
-- ─── Settings view actions ────────────────────────────────── -- ─── Settings view actions ──────────────────────────────────

View File

@@ -136,6 +136,8 @@ surface WordPressImportSurface {
-- taxonomy, and discovers WordPress shortcodes. The saved report shows -- taxonomy, and discovers WordPress shortcodes. The saved report shows
-- item counts, conflicts, missing uploads, year distribution, macro -- item counts, conflicts, missing uploads, year distribution, macro
-- usage, and importable totals before any project content is changed. -- usage, and importable totals before any project content is changed.
-- Its Conflicts, Posts, Pages, Media, Taxonomy, and Macros sections
-- expose unique Control+Option or Control+Alt access keys for disclosure control.
@guarantee ConflictReview @guarantee ConflictReview
-- Conflicts default to ignore. The operator can keep ignoring, overwrite -- Conflicts default to ignore. The operator can keep ignoring, overwrite

View File

@@ -72,6 +72,26 @@ surface AppShellSurface {
shell.sidebar.width shell.sidebar.width
shell.content_area.panel.visible shell.content_area.panel.visible
shell.assistant_sidebar.visible shell.assistant_sidebar.visible
@guarantee KeyboardTraversal
-- Tab and Shift+Tab traverse every enabled button, checkbox, picker,
-- and text editor in visual order. Disabled controls are skipped.
-- Enter or Space activates the focused control.
@guarantee KeyboardHints
-- Holding Control+Option on macOS or Control+Alt on Linux/Windows reveals
-- an access-key marker on every enabled control intersecting the viewport.
-- Semantic controls use their direct one-character shortcut. Remaining
-- visible controls receive stable two- or three-letter A-Z codes in
-- widget-tree order, excluding direct letters as generic prefixes.
-- Every marker is a warm-yellow box with black text and border, placed
-- wholly inside its control without obscuring activity-bar icons.
-- Typing a prefix while holding the modifier hides non-matches and shows
-- only each candidate's remaining suffix; a complete code activates its
-- control. Escape or modifier release cancels the access-key mode.
-- Covered layers and off-screen scroll content receive no code. A focused
-- Option/Alt alone always remains available for character input, and a
-- focused text editor retains modified input instead of entering the mode.
} }
value ContentArea { value ContentArea {
@@ -162,6 +182,7 @@ value ActivityButton {
label_key: String -- i18n key for tooltip label_key: String -- i18n key for tooltip
badge: Badge? -- only git has a badge badge: Badge? -- only git has a badge
active: Boolean -- highlighted when this view is showing active: Boolean -- highlighted when this view is showing
access_key: String -- Control+Option (macOS) or Control+Alt + 1..9,0 in displayed order
} }
value Badge { value Badge {
@@ -317,3 +338,9 @@ value TokenUsage {
-- Ctrl/Cmd+B: toggle sidebar -- Ctrl/Cmd+B: toggle sidebar
-- Ctrl/Cmd+W: close active tab (see tabs.allium) -- Ctrl/Cmd+W: close active tab (see tabs.allium)
-- Tab / Shift+Tab: next / previous enabled control
-- Enter / Space: activate focused control
-- Control+Option (macOS) or Control+Alt + 1..9,0: activate the corresponding activity
-- Post editor Control+Option (macOS) or Control+Alt + M/X: toggle metadata/excerpt
-- Import and Settings sections expose unique Control+Option or Control+Alt access keys
-- Site Validation uses R/A and Metadata Diff uses R with the platform modifier pair