update copy button
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M9.00003 10.4286L14 7.57141M9.00003 13.5714L14 16.4286M20.25 6C20.25 7.79493 18.7949 9.25 17 9.25C15.2051 9.25 13.75 7.79493 13.75 6C13.75 4.20507 15.2051 2.75 17 2.75C18.7949 2.75 20.25 4.20507 20.25 6ZM20.25 18C20.25 19.7949 18.7949 21.25 17 21.25C15.2051 21.25 13.75 19.7949 13.75 18C13.75 16.2051 15.2051 14.75 17 14.75C18.7949 14.75 20.25 16.2051 20.25 18ZM9.25 12C9.25 13.7949 7.79493 15.25 6 15.25C4.20507 15.25 2.75 13.7949 2.75 12C2.75 10.2051 4.20507 8.75 6 8.75C7.79493 8.75 9.25 10.2051 9.25 12Z" stroke="currentColor" stroke-width="1.5"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 641 B |
@@ -90,6 +90,7 @@ pub enum CustomIconName {
|
|||||||
GitBranch,
|
GitBranch,
|
||||||
Tag,
|
Tag,
|
||||||
Markdown,
|
Markdown,
|
||||||
|
Share,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IconNamed for CustomIconName {
|
impl IconNamed for CustomIconName {
|
||||||
@@ -114,6 +115,7 @@ impl IconNamed for CustomIconName {
|
|||||||
CustomIconName::GitBranch => "icons/git-branch.svg",
|
CustomIconName::GitBranch => "icons/git-branch.svg",
|
||||||
CustomIconName::Tag => "icons/tag.svg",
|
CustomIconName::Tag => "icons/tag.svg",
|
||||||
CustomIconName::Markdown => "icons/markdown.svg",
|
CustomIconName::Markdown => "icons/markdown.svg",
|
||||||
|
CustomIconName::Share => "icons/share.svg",
|
||||||
}
|
}
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ use nostr::prelude::*;
|
|||||||
/// Parsed NIP-34 repository announcement (plain data, ready for the UI).
|
/// Parsed NIP-34 repository announcement (plain data, ready for the UI).
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct Announcement {
|
pub struct Announcement {
|
||||||
|
/// ID of the announcement event itself.
|
||||||
|
pub event_id: EventId,
|
||||||
/// Repository ID (`d` tag).
|
/// Repository ID (`d` tag).
|
||||||
pub id: String,
|
pub id: String,
|
||||||
/// Author of the announcement event.
|
/// Author of the announcement event.
|
||||||
@@ -211,6 +213,7 @@ impl Announcement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Some(Self {
|
Some(Self {
|
||||||
|
event_id: event.id,
|
||||||
owner: event.pubkey,
|
owner: event.pubkey,
|
||||||
created_at: event.created_at,
|
created_at: event.created_at,
|
||||||
id,
|
id,
|
||||||
|
|||||||
@@ -3,17 +3,21 @@ use std::path::{Path, PathBuf};
|
|||||||
|
|
||||||
use assets::CustomIconName;
|
use assets::CustomIconName;
|
||||||
use gpui::prelude::*;
|
use gpui::prelude::*;
|
||||||
use gpui::{AnyElement, App, SharedString, Window, div, px};
|
use gpui::{AnyElement, App, ClipboardItem, SharedString, Window, div, px};
|
||||||
|
use gpui_base::StyledExt;
|
||||||
|
use gpui_component::clipboard::Clipboard;
|
||||||
use gpui_component::list::ListItem;
|
use gpui_component::list::ListItem;
|
||||||
|
use gpui_component::menu::{PopupMenu, PopupMenuItem};
|
||||||
use gpui_component::tooltip::Tooltip;
|
use gpui_component::tooltip::Tooltip;
|
||||||
use gpui_component::tree::{TreeEntry, TreeItem};
|
use gpui_component::tree::{TreeEntry, TreeItem};
|
||||||
use gpui_component::{ActiveTheme, Icon, IconName, Sizable, h_flex, v_flex};
|
use gpui_component::{ActiveTheme, Icon, IconName, Sizable, h_flex, v_flex};
|
||||||
use signed_core::RepoStatus;
|
use nostr::nips::nip19::{Nip19Coordinate, ToBech32};
|
||||||
|
use signed_core::{Announcement, RepoStatus};
|
||||||
use signed_git::{DiffHunk, DiffLine, DiffLineKind, FileDiff};
|
use signed_git::{DiffHunk, DiffLine, DiffLineKind, FileDiff};
|
||||||
|
|
||||||
/// A `Send` file-tree node: the tree is built on a background thread and
|
/// A `Send` file-tree node: the tree is built on a background thread and
|
||||||
/// converted into [`TreeItem`]s (which hold `Rc` state, so they cannot
|
/// converted into [`TreeItem`]s (which hold `Rc` state,
|
||||||
/// cross threads) on the main thread.
|
/// so they cannot cross threads) on the main thread.
|
||||||
pub(super) struct TreeItemSeed {
|
pub(super) struct TreeItemSeed {
|
||||||
/// Path of the node, relative to the worktree root.
|
/// Path of the node, relative to the worktree root.
|
||||||
id: String,
|
id: String,
|
||||||
@@ -22,12 +26,12 @@ pub(super) struct TreeItemSeed {
|
|||||||
children: Vec<TreeItemSeed>,
|
children: Vec<TreeItemSeed>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert tree seeds into [`TreeItem`]s, expanding every folder when
|
/// Convert tree seeds into [`TreeItem`]s, expanding every folder
|
||||||
/// `expand_folders` is set.
|
/// when `expand_folders` is set.
|
||||||
///
|
///
|
||||||
/// The commit diff explorer shows only changed files, which is typically a
|
/// The commit diff explorer shows only changed files,
|
||||||
/// handful of paths, so its folders start expanded; the worktree explorer
|
/// which is typically a handful of paths, so its folders start expanded;
|
||||||
/// starts collapsed instead.
|
/// the worktree explorer starts collapsed instead.
|
||||||
pub(super) fn tree_items(seeds: Vec<TreeItemSeed>, expand_folders: bool) -> Vec<TreeItem> {
|
pub(super) fn tree_items(seeds: Vec<TreeItemSeed>, expand_folders: bool) -> Vec<TreeItem> {
|
||||||
fn convert(seed: TreeItemSeed, expand_folders: bool) -> TreeItem {
|
fn convert(seed: TreeItemSeed, expand_folders: bool) -> TreeItem {
|
||||||
let mut item = TreeItem::new(seed.id, seed.label);
|
let mut item = TreeItem::new(seed.id, seed.label);
|
||||||
@@ -48,8 +52,8 @@ pub(super) fn tree_items(seeds: Vec<TreeItemSeed>, expand_folders: bool) -> Vec<
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One row of a file tree: icon + name, indented by depth. Clicking a file
|
/// One row of a file tree: icon + name, indented by depth.
|
||||||
/// runs `on_click`; folders expand/collapse via the tree itself.
|
/// Clicking a file runs `on_click`; folders expand/collapse via the tree itself.
|
||||||
pub(super) fn tree_row<F>(ix: usize, entry: &TreeEntry, selected: bool, on_click: F) -> ListItem
|
pub(super) fn tree_row<F>(ix: usize, entry: &TreeEntry, selected: bool, on_click: F) -> ListItem
|
||||||
where
|
where
|
||||||
F: Fn(&mut Window, &mut App) + 'static,
|
F: Fn(&mut Window, &mut App) + 'static,
|
||||||
@@ -139,8 +143,7 @@ pub(super) fn build_tree_items(entries: &[PathBuf]) -> Vec<TreeItemSeed> {
|
|||||||
/// The markdown fence language for a file path, or `None` for plain text.
|
/// The markdown fence language for a file path, or `None` for plain text.
|
||||||
///
|
///
|
||||||
/// Names are chosen so `gpui_component`'s highlighter can resolve them
|
/// Names are chosen so `gpui_component`'s highlighter can resolve them
|
||||||
/// (`highlighter::Language::from_name` accepts short aliases such as `rs`
|
/// (`highlighter::Language::from_name` accepts short aliases such as `rs` and `js`).
|
||||||
/// and `js`).
|
|
||||||
pub(super) fn code_language(path: &str) -> Option<&'static str> {
|
pub(super) fn code_language(path: &str) -> Option<&'static str> {
|
||||||
let name = Path::new(path)
|
let name = Path::new(path)
|
||||||
.file_name()
|
.file_name()
|
||||||
@@ -220,8 +223,8 @@ pub(super) fn placeholder(message: &str, cx: &App) -> AnyElement {
|
|||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The status badge shown next to an issue or pull request: icon + colored
|
/// The status badge shown next to an issue or pull request: icon + colored square,
|
||||||
/// square, with a tooltip describing the status.
|
/// with a tooltip describing the status.
|
||||||
pub(super) fn status_badge(status: RepoStatus, cx: &App) -> AnyElement {
|
pub(super) fn status_badge(status: RepoStatus, cx: &App) -> AnyElement {
|
||||||
let (icon, label, tooltip, bg, fg) = match status {
|
let (icon, label, tooltip, bg, fg) = match status {
|
||||||
RepoStatus::Open => (
|
RepoStatus::Open => (
|
||||||
@@ -267,6 +270,125 @@ pub(super) fn status_badge(status: RepoStatus, cx: &App) -> AnyElement {
|
|||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) struct ShareTargets {
|
||||||
|
/// NIP-19 `naddr1...` of the announcement (with its announced relays).
|
||||||
|
pub(super) naddr: String,
|
||||||
|
/// Hex ID of the announcement event itself.
|
||||||
|
pub(super) event_id: String,
|
||||||
|
/// NIP-34 coordinate `30617:<pubkey>:<repo-id>`.
|
||||||
|
pub(super) coordinate: String,
|
||||||
|
/// `https://gitworkshop.dev/<naddr>`
|
||||||
|
pub(super) gitworkshop: String,
|
||||||
|
/// `https://ditto.pub/<naddr>`
|
||||||
|
pub(super) ditto: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ShareTargets {
|
||||||
|
pub(super) fn from_announcement(announcement: &Announcement) -> Self {
|
||||||
|
let addr = announcement.addr();
|
||||||
|
let coordinate = addr.to_string();
|
||||||
|
let naddr = Nip19Coordinate::new(addr, announcement.relays.iter().cloned())
|
||||||
|
.to_bech32()
|
||||||
|
.expect("a complete coordinate always encodes to naddr");
|
||||||
|
|
||||||
|
Self {
|
||||||
|
naddr: naddr.clone(),
|
||||||
|
event_id: announcement.event_id.to_bech32().unwrap(),
|
||||||
|
coordinate,
|
||||||
|
gitworkshop: format!("https://gitworkshop.dev/{naddr}"),
|
||||||
|
ditto: format!("https://ditto.pub/{naddr}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The share dropdown menu: one row per target, each showing a compact
|
||||||
|
/// label while the copy button (and row click) copy the full value.
|
||||||
|
pub(super) fn menu(&self, menu: PopupMenu) -> PopupMenu {
|
||||||
|
menu.min_w(px(340.))
|
||||||
|
.item(share_menu_row(
|
||||||
|
"copy-gitworkshop",
|
||||||
|
"GitWorkshop",
|
||||||
|
truncate_naddr_link(&self.gitworkshop, 4),
|
||||||
|
self.gitworkshop.clone(),
|
||||||
|
))
|
||||||
|
.item(share_menu_row(
|
||||||
|
"copy-ditto",
|
||||||
|
"Ditto",
|
||||||
|
truncate_naddr_link(&self.ditto, 4),
|
||||||
|
self.ditto.clone(),
|
||||||
|
))
|
||||||
|
.item(share_menu_row(
|
||||||
|
"copy-event-id",
|
||||||
|
"Event ID",
|
||||||
|
middle_truncate(&self.event_id, 10, 10),
|
||||||
|
self.event_id.clone(),
|
||||||
|
))
|
||||||
|
.item(share_menu_row(
|
||||||
|
"copy-coordinate",
|
||||||
|
"Coordinate",
|
||||||
|
middle_truncate(&self.coordinate, 10, 10),
|
||||||
|
self.coordinate.clone(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One row of the share menu: a small title on top of the compact label,
|
||||||
|
/// with a copy button that flips to a check while the value is on the
|
||||||
|
/// clipboard. Clicking the row text copies and dismisses the menu; the copy
|
||||||
|
/// button stops propagation, so the menu stays open for further copies.
|
||||||
|
/// Both copy `copy`, never the truncated label.
|
||||||
|
pub(super) fn share_menu_row(
|
||||||
|
id: &'static str,
|
||||||
|
title: &'static str,
|
||||||
|
label: String,
|
||||||
|
copy: String,
|
||||||
|
) -> PopupMenuItem {
|
||||||
|
let row_copy = copy.clone();
|
||||||
|
PopupMenuItem::element(move |_window, _cx| {
|
||||||
|
let button_copy = copy.clone();
|
||||||
|
h_flex()
|
||||||
|
.flex_1()
|
||||||
|
.gap_2()
|
||||||
|
.items_end()
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.flex_1()
|
||||||
|
.gap_1()
|
||||||
|
.text_xs()
|
||||||
|
.child(div().flex_shrink_0().w_20().font_semibold().child(title))
|
||||||
|
.child(div().flex_1().text_ellipsis().child(label.clone())),
|
||||||
|
)
|
||||||
|
.child(Clipboard::new(id).tooltip("Copy").value(button_copy))
|
||||||
|
})
|
||||||
|
.on_click(move |_, _, cx| {
|
||||||
|
cx.write_to_clipboard(ClipboardItem::new_string(row_copy.clone()));
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `[head chars]...[tail chars]` middle truncation; the value is left alone
|
||||||
|
/// when it is too short for the ellipsis to save space.
|
||||||
|
fn middle_truncate(value: &str, head: usize, tail: usize) -> String {
|
||||||
|
let len = value.chars().count();
|
||||||
|
if len <= head + tail + 3 {
|
||||||
|
return value.to_string();
|
||||||
|
}
|
||||||
|
let head: String = value.chars().take(head).collect();
|
||||||
|
let tail: String = value.chars().skip(len - tail).collect();
|
||||||
|
format!("{head}...{tail}")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shorten an naddr link to `<url>/naddr1...[last tail chars]`, e.g.
|
||||||
|
/// `https://gitworkshop.dev/naddr1...abcd`. Only the label is shortened;
|
||||||
|
/// the value to be copied stays the full URL.
|
||||||
|
fn truncate_naddr_link(url: &str, tail: usize) -> String {
|
||||||
|
let Some(end) = url.find("naddr1").map(|i| i + "naddr1".len()) else {
|
||||||
|
return url.to_string();
|
||||||
|
};
|
||||||
|
if url.len() - end <= tail + 3 {
|
||||||
|
return url.to_string();
|
||||||
|
}
|
||||||
|
format!("{}...{}", &url[..end], &url[url.len() - tail..])
|
||||||
|
}
|
||||||
|
|
||||||
/// Width of one line-number gutter in a diff row.
|
/// Width of one line-number gutter in a diff row.
|
||||||
pub(super) const GUTTER_WIDTH: f32 = 44.;
|
pub(super) const GUTTER_WIDTH: f32 = 44.;
|
||||||
/// Height of one row in a virtual diff list.
|
/// Height of one row in a virtual diff list.
|
||||||
@@ -482,4 +604,39 @@ mod tests {
|
|||||||
assert_eq!(code_language("LICENSE"), None);
|
assert_eq!(code_language("LICENSE"), None);
|
||||||
assert_eq!(code_language("README.md"), None);
|
assert_eq!(code_language("README.md"), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn middle_truncates_long_values_only() {
|
||||||
|
assert_eq!(
|
||||||
|
middle_truncate(
|
||||||
|
"a008def15796fba9a0d6fab04e8fd57089285d9fd505da5a83fe8aad57a3564d",
|
||||||
|
10,
|
||||||
|
10,
|
||||||
|
),
|
||||||
|
"a008def157...ad57a3564d"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
middle_truncate(
|
||||||
|
"30617:a008def15796fba9a0d6fab04e8fd57089285d9fd505da5a83fe8aad57a3564d:ngit",
|
||||||
|
10,
|
||||||
|
10
|
||||||
|
),
|
||||||
|
"30617:a008...3564d:ngit"
|
||||||
|
);
|
||||||
|
// Too short to save space with the ellipsis: left alone.
|
||||||
|
assert_eq!(middle_truncate("short", 10, 10), "short");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn naddr_link_keeps_url_and_tail() {
|
||||||
|
assert_eq!(
|
||||||
|
truncate_naddr_link("https://gitworkshop.dev/naddr1qqqxyzabc1234", 4),
|
||||||
|
"https://gitworkshop.dev/naddr1...1234"
|
||||||
|
);
|
||||||
|
// No naddr1 prefix: unchanged.
|
||||||
|
assert_eq!(
|
||||||
|
truncate_naddr_link("https://example.com/x", 4),
|
||||||
|
"https://example.com/x"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use dock::{BasePanel, DockArea, DockPlacement, Panel, PanelEvent, panel_handle};
|
|||||||
use gix::Repository;
|
use gix::Repository;
|
||||||
use gpui::prelude::*;
|
use gpui::prelude::*;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
Action, AnyElement, App, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
Action, AnyElement, App, ClipboardItem, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
||||||
PathPromptOptions, Pixels, Render, SharedString, Size, Subscription, Task, WeakEntity, Window,
|
PathPromptOptions, Pixels, Render, SharedString, Size, Subscription, Task, WeakEntity, Window,
|
||||||
div, px, relative, size,
|
div, px, relative, size,
|
||||||
};
|
};
|
||||||
@@ -45,7 +45,7 @@ use browser::{
|
|||||||
};
|
};
|
||||||
use commits::COMMIT_ROW_HEIGHT;
|
use commits::COMMIT_ROW_HEIGHT;
|
||||||
use diff::CommitDiffView;
|
use diff::CommitDiffView;
|
||||||
use helpers::{TreeItemSeed, build_tree_items, is_markdown_path, tree_items};
|
use helpers::{ShareTargets, TreeItemSeed, build_tree_items, is_markdown_path, tree_items};
|
||||||
use issues::{IssuesView, open_new_issue_dialog};
|
use issues::{IssuesView, open_new_issue_dialog};
|
||||||
use pull_requests::{PullRequestsView, open_new_pull_request_dialog};
|
use pull_requests::{PullRequestsView, open_new_pull_request_dialog};
|
||||||
|
|
||||||
@@ -1044,6 +1044,7 @@ impl RepoDetailView {
|
|||||||
|
|
||||||
let name = self.display_name(cx);
|
let name = self.display_name(cx);
|
||||||
let description = announcement.description();
|
let description = announcement.description();
|
||||||
|
let share = ShareTargets::from_announcement(announcement);
|
||||||
|
|
||||||
let commits_count = self.all_commits.as_ref().map(|list| list.total);
|
let commits_count = self.all_commits.as_ref().map(|list| list.total);
|
||||||
let worktree_empty = self.switching_ref || self.worktree.is_none();
|
let worktree_empty = self.switching_ref || self.worktree.is_none();
|
||||||
@@ -1091,7 +1092,7 @@ impl RepoDetailView {
|
|||||||
h_flex()
|
h_flex()
|
||||||
.mt_2()
|
.mt_2()
|
||||||
.w_full()
|
.w_full()
|
||||||
.gap_2()
|
.gap_0p5()
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.text_xs()
|
.text_xs()
|
||||||
@@ -1112,6 +1113,7 @@ impl RepoDetailView {
|
|||||||
.outline()
|
.outline()
|
||||||
.button(
|
.button(
|
||||||
Button::new("issues-open")
|
Button::new("issues-open")
|
||||||
|
.icon(CustomIconName::GitIssueDone)
|
||||||
.child(
|
.child(
|
||||||
h_flex().gap_2().text_sm().child("Issues").child(
|
h_flex().gap_2().text_sm().child("Issues").child(
|
||||||
Tag::secondary()
|
Tag::secondary()
|
||||||
@@ -1138,6 +1140,7 @@ impl RepoDetailView {
|
|||||||
.outline()
|
.outline()
|
||||||
.button(
|
.button(
|
||||||
Button::new("prs-open")
|
Button::new("prs-open")
|
||||||
|
.icon(CustomIconName::GitPullRequest)
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
@@ -1163,10 +1166,23 @@ impl RepoDetailView {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
Button::new("link")
|
DropdownButton::new("share")
|
||||||
.icon(IconName::ExternalLink)
|
.secondary()
|
||||||
.tooltip("Open in gitworkshop.dev")
|
.button(
|
||||||
.secondary(),
|
Button::new("link")
|
||||||
|
.icon(IconName::Copy)
|
||||||
|
.tooltip("Copy ID")
|
||||||
|
.secondary()
|
||||||
|
.on_click({
|
||||||
|
let naddr = share.naddr.clone();
|
||||||
|
move |_, _, cx| {
|
||||||
|
cx.write_to_clipboard(
|
||||||
|
ClipboardItem::new_string(naddr.clone()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.dropdown_menu(move |menu, _, _| share.menu(menu)),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
Button::new("clone")
|
Button::new("clone")
|
||||||
@@ -1330,8 +1346,6 @@ impl RepoDetailView {
|
|||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Horizontal list of everyone who maintains the repository: the owner
|
|
||||||
/// shown in full, and any additional maintainers as a compact overlapping avatar group.
|
|
||||||
fn render_maintainers(&self, cx: &mut Context<Self>) -> AnyElement {
|
fn render_maintainers(&self, cx: &mut Context<Self>) -> AnyElement {
|
||||||
let announcement = self.announcement(cx);
|
let announcement = self.announcement(cx);
|
||||||
let profile_store = ProfileStore::global(cx);
|
let profile_store = ProfileStore::global(cx);
|
||||||
@@ -1352,7 +1366,7 @@ impl RepoDetailView {
|
|||||||
.w_full()
|
.w_full()
|
||||||
.gap_3()
|
.gap_3()
|
||||||
.child(
|
.child(
|
||||||
Button::new("maintainers").ghost().child(
|
Button::new("maintainers").compact().ghost().child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.child(
|
.child(
|
||||||
|
|||||||
Reference in New Issue
Block a user