mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-11-06 08:15:35 +00:00
fix(settings): one control language down the right-hand column
The page had drifted from the design system: dropdowns and secondary buttons were raised white with a drop shadow, fields had a half-pixel inset ring that anti-aliased to nothing on a 1x display (the shell and proxy rows read as loose monospace text), and dropdowns sized to their value beside fixed-width fields, so the column's left edge zig-zagged. Fields, dropdowns, secondary buttons, steppers and search fields now all stand on the theme's muted well with no outline or shadow; a field shows a ring only for focus (accent) or error. Fields and dropdowns are 28px and one width (kit::CONTROL_W); only path and command fields are wider. Field text is sized on the Input itself, since its own text_sm outranked the size set around it. A host saved under its own address no longer repeats it as a subtitle.
This commit is contained in:
@@ -101,7 +101,9 @@ Git 的新增/修改标记种子为绿 `#2F8A52` / `#4CC27A`、琥珀 `#B7791F`
|
||||
页标题立在 48px 标题栏带内;分组标题 28px、11.5/16rem medium 辅助色;分组之间是 0.5px 分隔线,上下各 16px。
|
||||
设置行:标签正文色 regular,描述 12/16rem 辅助色,行最低 28px、内边距 8(hover/搜索命中的底色外扩 8px);
|
||||
搜索命中行用浅中性底,不用强调色。输入框与下拉框 28px,按钮、步进器 26px(圆角 6);
|
||||
输入框/下拉框/次要按钮都用同一浅底(surface 半阶)且无描边。控件一义一种:开关管开/关,下拉管单选
|
||||
输入框/下拉框/次要按钮都用同一浅底(主题 `muted`,与对话框输入井、侧栏搜索同色)且无描边、无阴影;
|
||||
输入框只在聚焦(强调色)或出错(红)时显示 1px 内环。右侧控件列统一 180px 宽(`kit::CONTROL_W`),
|
||||
只有路径/命令类输入加宽到 240px;过长的值省略号截断。控件一义一种:开关管开/关,下拉管单选
|
||||
(不再用分段控件),滑块管连续值,步进器管整数微调。
|
||||
每个视图唯一的主操作(保存主题草稿、连接、安装更新)用反色中性,同 Git 提交按钮;不可执行时回到浅底。
|
||||
开关与滑块仍用强调色。SSH 主机列表:标题行带 26px 图标按钮,28px 浅底搜索,分组标题 22px,
|
||||
@@ -126,7 +128,8 @@ Git 的新增/修改标记种子为绿 `#2F8A52` / `#4CC27A`、琥珀 `#B7791F`
|
||||
- **SFTP 浏览器**:行与本地文件树一致(26px、圆角 6、16px 图标辅助色、文件名侧栏标题色、大小等宽数字);
|
||||
编辑框为无边框淡底井(圆角 7);传输进度为正文色细条压在淡底轨道上,失败才转红。
|
||||
- **面板里的主按钮**(SFTP 确认、添加转发、SSH 断线条的重连):反色中性,同对话框主按钮。
|
||||
- **对话框**(SSH 认证/主机密钥、新建 worktree):`ui::dialog` 统一外壳,同切换器的 12px 圆角、
|
||||
- **对话框**(SSH 认证/主机密钥、新建 worktree,以及所有确认框——关闭标签、退出、丢弃更改、删除文件等 `window.prompt`,
|
||||
三个平台都不用系统原生弹窗):`ui::dialog` 统一外壳,同切换器的 12px 圆角、
|
||||
浮层阴影与遮罩、距顶 112px;标题行 48px(正文字号 medium,右侧 `esc` 键帽,下接细线),
|
||||
内容区左右 18px、字段间距 14px;字段名 11.5/16rem medium 辅助色,输入框为无边框 28px
|
||||
淡底井(`muted`、圆角 7)。页脚 40px 细线分隔,按钮 28px / 圆角 6、右对齐、主操作在最右:
|
||||
@@ -134,6 +137,9 @@ Git 的新增/修改标记种子为绿 `#2F8A52` / `#4CC27A`、琥珀 `#B7791F`
|
||||
退回淡底 + 辅助色且不响应点击。主机名与指纹放在淡底信息井里用等宽字。唯一的红色按钮是
|
||||
「主机密钥已变更」的 Override,该卡片也是唯一带红色描边的对话框。
|
||||
- **浮动提示条**:边缘保持中性细线,严重程度只由行首 6px 状态点表达(警告琥珀、断开红)。
|
||||
文档栏里的「文件已在磁盘上更改」横条同理:中性底 + 底部细线 + 琥珀点,按钮用对话框按钮,安全的「保留我的」为主。
|
||||
- **通知(toast)**:出错时带标题说明哪个操作失败(如「git push 失败」),原因作为次级文字,带状态图标;
|
||||
不直接把一行 stderr 当作全部内容。设置窗口是独立窗口,同样要渲染通知层。
|
||||
- **首页**:快捷操作列表 340px 宽,行高 28px、圆角 7,静止无底、hover 取窗口 hover 阶;
|
||||
快捷键用 18px 淡底键帽。
|
||||
- **终端**:ANSI 是用户内容,不拿来充当 UI 错误/成功配色;应用设计调整不得反转状态含义。
|
||||
|
||||
+7
-2
@@ -88,8 +88,13 @@ fn ui_scale(cx: &App) -> f32 {
|
||||
cx.global::<Config>().ui_font_size / UI_FONT_SIZE_DEFAULT
|
||||
}
|
||||
|
||||
/// How wide a text field in the right-hand column is.
|
||||
const FIELD_W: f32 = 180.;
|
||||
/// How wide a text field in the right-hand column is — the dropdowns' width,
|
||||
/// so the column is one straight edge whichever control a row holds.
|
||||
const FIELD_W: f32 = crate::ui::settings::kit::CONTROL_W;
|
||||
|
||||
/// A field for a path or a command line: the one kind of value unreadable cut
|
||||
/// short, so the one kind allowed past [`FIELD_W`].
|
||||
const PATH_FIELD_W: f32 = 240.;
|
||||
|
||||
/// The host editor's label column, and how wide a field beside one grows to.
|
||||
const SSH_LABEL_W: f32 = 96.;
|
||||
|
||||
@@ -419,6 +419,11 @@ impl Tty7App {
|
||||
(false, None) => (t(L10nKey::SettingsNewHost).to_string(), String::new()),
|
||||
};
|
||||
let blank_title = editing && p.is_none() && title == t(L10nKey::SettingsNewHost);
|
||||
// A host saved under its own address is titled with that address, and
|
||||
// a second line saying it again is noise — the rule the New Tab menu
|
||||
// already keeps. While editing, the line is the form's live preview
|
||||
// and stays.
|
||||
let echo = !editing && address == title;
|
||||
let (meta, meta_color) = if dirty {
|
||||
(t(L10nKey::SettingsUnsaved).to_string(), tk.warn_text)
|
||||
} else if live {
|
||||
@@ -463,15 +468,17 @@ impl Tty7App {
|
||||
.text_color(if blank_title { tk.k4 } else { tk.fg })
|
||||
.child(title),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.font_family(Tk::mono(cx))
|
||||
.text_size(fs(11.))
|
||||
.line_height(px(15.))
|
||||
.text_color(tk.k45)
|
||||
.truncate()
|
||||
.child(address),
|
||||
),
|
||||
.when(!echo, |col| {
|
||||
col.child(
|
||||
div()
|
||||
.font_family(Tk::mono(cx))
|
||||
.text_size(fs(11.))
|
||||
.line_height(px(15.))
|
||||
.text_color(tk.k45)
|
||||
.truncate()
|
||||
.child(address),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
|
||||
+72
-39
@@ -49,6 +49,15 @@ pub(crate) struct Tk {
|
||||
pub nav: Hsla,
|
||||
/// A raised button's fill.
|
||||
pub btn: Hsla,
|
||||
/// The one fill every control on the page stands on — field, dropdown,
|
||||
/// secondary button, stepper — the theme's `muted`, the same well the
|
||||
/// dialogs' fields and the sidebar's search sit in. No outline, no drop.
|
||||
pub well: Hsla,
|
||||
/// [`Self::well`] a step firmer, under the pointer or while open.
|
||||
pub well_hover: Hsla,
|
||||
/// The focus ring: the accent, the one colour kept for "the keyboard
|
||||
/// is here".
|
||||
pub focus: Hsla,
|
||||
/// A popover's fill.
|
||||
pub menu: Hsla,
|
||||
/// A switch or slider knob at rest.
|
||||
@@ -76,6 +85,7 @@ impl Tk {
|
||||
let page = theme.background;
|
||||
let a = |light: f32, dark_a: f32| fg.opacity(if dark { dark_a } else { light });
|
||||
let white = gpui::white();
|
||||
let well = theme.muted;
|
||||
Self {
|
||||
dark,
|
||||
fg,
|
||||
@@ -95,6 +105,9 @@ impl Tk {
|
||||
heading: a(0.9, 0.9),
|
||||
nav: a(0.82, 0.8),
|
||||
btn: if dark { fg.opacity(0.1) } else { white },
|
||||
well,
|
||||
well_hover: well.blend(fg.opacity(if dark { 0.06 } else { 0.05 })),
|
||||
focus: theme.ring,
|
||||
menu: if dark { lift(page, 0.06) } else { white },
|
||||
knob: if dark { fg } else { white },
|
||||
chip: if dark { fg.opacity(0.16) } else { white },
|
||||
@@ -330,13 +343,12 @@ impl RenderOnce for Btn {
|
||||
.px(px(12.))
|
||||
.when(self.icon.is_some(), |b| b.pl(px(9.)).pr(px(11.)))
|
||||
.rounded(px(6.))
|
||||
.bg(tk.btn)
|
||||
.shadow(tk.raised())
|
||||
.bg(tk.well)
|
||||
.text_size(self.size.unwrap_or(fs(12.5)))
|
||||
.text_color(tk.fg)
|
||||
.when(!self.disabled, |b| {
|
||||
b.hover(move |s| s.shadow(tk.raised_hover()))
|
||||
.active(move |s| s.bg(tk.k05))
|
||||
b.hover(move |s| s.bg(tk.well_hover))
|
||||
.active(move |s| s.bg(tk.well_hover.blend(tk.k05)))
|
||||
}),
|
||||
BtnKind::Primary => base
|
||||
.h(px(26.))
|
||||
@@ -409,22 +421,31 @@ pub(crate) fn search_glass(size: f32, tk: &Tk) -> Icon {
|
||||
.text_color(tk.k35)
|
||||
}
|
||||
|
||||
/// The height of a field or a dropdown. Buttons and steppers are a step
|
||||
/// lower, at 26.
|
||||
pub(crate) const CONTROL_H: f32 = 28.;
|
||||
|
||||
/// A search field with a hairline ring, for filtering a list on the page.
|
||||
pub(crate) fn search_field(input: &gpui::Entity<InputState>, tk: &Tk) -> Div {
|
||||
h_flex()
|
||||
.h(px(26.))
|
||||
.h(px(CONTROL_H))
|
||||
.px(px(8.))
|
||||
.gap(px(6.))
|
||||
.items_center()
|
||||
.rounded(px(6.))
|
||||
.shadow(vec![ring(tk.k15, 0.5, true)])
|
||||
.rounded(px(7.))
|
||||
.bg(tk.well)
|
||||
.child(search_glass(10., tk))
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.text_size(fs(12.5))
|
||||
.child(Input::new(input).appearance(false).px_0().py_0().h(px(24.))),
|
||||
// On the input, not around it: `Input` sets its own `text_sm`
|
||||
// and only a size given to it directly outranks that.
|
||||
div().flex_1().min_w_0().child(
|
||||
Input::new(input)
|
||||
.appearance(false)
|
||||
.px_0()
|
||||
.py_0()
|
||||
.h(px(24.))
|
||||
.text_size(fs(12.5)),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -437,32 +458,36 @@ pub(crate) fn text_field(
|
||||
tk: &Tk,
|
||||
cx: &App,
|
||||
) -> Div {
|
||||
let ring_color = if invalid {
|
||||
tk.danger
|
||||
} else if focused {
|
||||
tk.k45
|
||||
} else {
|
||||
tk.k15
|
||||
// At rest a field is only its well. The ring is kept for the two states
|
||||
// that have to be seen at a glance: where the keyboard is, and what is
|
||||
// wrong.
|
||||
let ring_color = match (invalid, focused) {
|
||||
(true, _) => Some(tk.danger),
|
||||
(false, true) => Some(tk.focus),
|
||||
(false, false) => None,
|
||||
};
|
||||
div()
|
||||
.h(px(26.))
|
||||
.h(px(CONTROL_H))
|
||||
.px(px(10.))
|
||||
.flex()
|
||||
.items_center()
|
||||
.rounded(px(6.))
|
||||
.shadow(vec![ring(
|
||||
ring_color,
|
||||
if focused || invalid { 1. } else { 0.5 },
|
||||
true,
|
||||
)])
|
||||
.bg(tk.well)
|
||||
.when_some(ring_color, |f, c| f.shadow(vec![ring(c, 1., true)]))
|
||||
.font_family(Tk::mono(cx))
|
||||
.text_size(fs(12.))
|
||||
.text_color(tk.fg)
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.child(Input::new(input).appearance(false).px_0().py_0().h(px(24.))),
|
||||
div().flex_1().min_w_0().child(
|
||||
Input::new(input)
|
||||
.appearance(false)
|
||||
.px_0()
|
||||
.py_0()
|
||||
.h(px(24.))
|
||||
// The input's own `text_sm` beat the 12px set on this
|
||||
// box, so the value stood a size above every dropdown
|
||||
// label in the same column.
|
||||
.text_size(fs(12.)),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -489,8 +514,16 @@ pub(crate) fn section_head(title: &str, desc: Option<String>, tk: &Tk) -> Div {
|
||||
})
|
||||
}
|
||||
|
||||
/// The width of a control in the page's right-hand column — dropdown or text
|
||||
/// field alike. The column used to size each dropdown to its value (anything
|
||||
/// from 148 to 260) beside fields fixed at 180, so its left edge zig-zagged
|
||||
/// down the page. One width, and a value too long for it elides. Only fields
|
||||
/// that hold a path or a command line are wider, because those are unreadable
|
||||
/// cut short.
|
||||
pub(crate) const CONTROL_W: f32 = 180.;
|
||||
|
||||
/// The trigger of a dropdown: a raised button with the current value, an
|
||||
/// optional mark before it, and a chevron.
|
||||
/// optional mark before it, and a chevron, [`CONTROL_W`] wide.
|
||||
pub(crate) fn select_trigger(
|
||||
id: impl Into<ElementId>,
|
||||
label: impl Into<SharedString>,
|
||||
@@ -500,17 +533,16 @@ pub(crate) fn select_trigger(
|
||||
) -> Stateful<Div> {
|
||||
h_flex()
|
||||
.id(id)
|
||||
.h(px(26.))
|
||||
.min_w(px(148.))
|
||||
.max_w(px(260.))
|
||||
.h(px(CONTROL_H))
|
||||
.w(px(CONTROL_W))
|
||||
.flex_shrink_0()
|
||||
.pl(px(if leading.is_some() { 8. } else { 10. }))
|
||||
.pr(px(7.))
|
||||
.gap(px(8.))
|
||||
.items_center()
|
||||
.rounded(px(6.))
|
||||
.bg(if open { tk.k05 } else { tk.btn })
|
||||
.shadow(tk.raised())
|
||||
.hover(move |s| s.shadow(tk.raised_hover()))
|
||||
.bg(if open { tk.well_hover } else { tk.well })
|
||||
.hover(move |s| s.bg(tk.well_hover))
|
||||
.cursor_pointer()
|
||||
.text_size(fs(12.5))
|
||||
.text_color(tk.fg)
|
||||
@@ -642,7 +674,9 @@ pub(crate) fn stepper(
|
||||
.text_color(tk.k6)
|
||||
.when(!enabled, |d| d.opacity(0.35))
|
||||
.when(enabled, |d| {
|
||||
d.cursor_pointer().active(move |s| s.bg(tk.k05))
|
||||
d.cursor_pointer()
|
||||
.hover(move |s| s.bg(tk.well_hover))
|
||||
.active(move |s| s.bg(tk.well_hover.blend(tk.k05)))
|
||||
})
|
||||
.child(glyph)
|
||||
};
|
||||
@@ -652,8 +686,7 @@ pub(crate) fn stepper(
|
||||
.h(px(26.))
|
||||
.items_center()
|
||||
.rounded(px(6.))
|
||||
.bg(tk.btn)
|
||||
.shadow(tk.raised())
|
||||
.bg(tk.well)
|
||||
.child(
|
||||
end("dec", "−", s.can_dec)
|
||||
.rounded_l(px(6.))
|
||||
|
||||
@@ -756,7 +756,10 @@ impl Tty7App {
|
||||
.id("shell-program-detected-trigger")
|
||||
.flex_shrink_0()
|
||||
.ml(px(4.))
|
||||
.mr(px(-4.))
|
||||
// Pulled into the field's 10px padding far enough that the
|
||||
// chevron's centre sits 12px from the edge — where a
|
||||
// dropdown's does, one row down.
|
||||
.mr(px(-7.))
|
||||
.size(px(18.))
|
||||
.rounded(px(4.))
|
||||
.flex()
|
||||
@@ -849,7 +852,7 @@ impl Tty7App {
|
||||
let value = wd_path_input.read(cx).value();
|
||||
let error = (!crate::ui::app::wd_path_saveable(&value))
|
||||
.then(|| t(L10nKey::SettingsWdPathInvalid).to_string());
|
||||
let control = self.settings_checked_input(&wd_path_input, 240., error, cx);
|
||||
let control = self.settings_checked_input(&wd_path_input, PATH_FIELD_W, error, cx);
|
||||
rows.push(
|
||||
self.settings_row(
|
||||
t(L10nKey::SettingsCustomPath),
|
||||
@@ -936,7 +939,7 @@ impl Tty7App {
|
||||
// reads as "this is what file links do".
|
||||
let link_file_command_row = (link_file_open == LinkFileOpen::Command).then(|| {
|
||||
let control = self
|
||||
.settings_text_input(&link_file_command_input, 240., false, cx)
|
||||
.settings_text_input(&link_file_command_input, PATH_FIELD_W, false, cx)
|
||||
.into_any_element();
|
||||
self.settings_row(
|
||||
t(L10nKey::SettingsOpenFilesCommand),
|
||||
|
||||
@@ -143,7 +143,8 @@ impl Tty7App {
|
||||
d.child(kit::popover_below(
|
||||
true,
|
||||
4.,
|
||||
self.settings_menu_panel(&id, entries, cx).min_w(px(148.)),
|
||||
self.settings_menu_panel(&id, entries, cx)
|
||||
.min_w(px(kit::CONTROL_W)),
|
||||
))
|
||||
})
|
||||
.into_any_element()
|
||||
|
||||
Reference in New Issue
Block a user