diff --git a/README.md b/README.md index 35164306..311b3201 100644 --- a/README.md +++ b/README.md @@ -48,8 +48,13 @@ chmod +x nui-sftp-*.AppImage #### Flatpak -For other distributions a flatpak is provided. Currently not in the official flathub, but that will come eventually. -Download the flatpak from a release and install it like so: +A flatpak is published on [Flathub](https://flathub.org/apps/org.nuicpp.nui_sftp) as `org.nuicpp.nui_sftp`: +```bash +flatpak install flathub org.nuicpp.nui_sftp +flatpak run org.nuicpp.nui_sftp +``` + +Flatpak bundles are also attached to each release if you prefer to install one directly: ```bash # Make sure to pass the correct file name! flatpak install nui-sftp-flatpak-x86_64_0.1.1.flatpak diff --git a/frontend/include/frontend/brand_mark.hpp b/frontend/include/frontend/brand_mark.hpp new file mode 100644 index 00000000..ae2d1d12 --- /dev/null +++ b/frontend/include/frontend/brand_mark.hpp @@ -0,0 +1,48 @@ +#pragma once + +#include +#include +#include +#include + +namespace Frontend +{ + /** + * @brief Inline NuiSftp brand mark — a path + polygon filled with + * `currentColor` so the parent element's `color:` controls the tint and + * any `filter: drop-shadow(...)` on the parent applies cleanly. + */ + inline Nui::ElementRenderer brandMarkSvg() + { + namespace SvgEl = Nui::Elements::Svg; + namespace SvgAttr = Nui::Attributes::Svg; + using namespace Nui::Attributes::Literals; + + return SvgEl::svg{ + SvgAttr::viewBox = "0 0 500 500", + "aria-hidden"_attr = "true", + }( + SvgEl::g{ + SvgAttr::transform = "matrix(1.4755152,0,0,1.4755152,-118.95322,-118.94235)", + SvgAttr::fill = "currentColor", + }( + SvgEl::path{ + SvgAttr::d = "m 364.3,293.5 -0.3,0.4 -26.6,31.8 -22.6,26.8 -22.6,-26.8 " + "-26.6,-31.8 -0.3,-0.4 h 29 v -28.2 c 0.1,0 0.1,0 0.1,0 " + "l 0.2,-31.8 c -0.1,-3.2 -0.2,-6.4 -0.5,-9.7 -1.8,-26 " + "-24.3,-39.5 -47.2,-37.1 -25,2.5 -39.7,20.2 -41,46.3 " + "0,0.6 0,1.2 0,1.9 0,0.9 0,1.8 0,2.7 L 202,233 192.9,222.4 " + "c -0.2,-0.2 -0.3,-0.4 -0.6,-0.6 l -7.3,-8.7 -11.4,13.5 " + "-9.1,10.8 v -89.9 h 41.3 v 24.1 c 13.5,-17.2 33.5,-24.3 " + "55.6,-24.1 46.7,0.4 73,36.3 73.3,80.9 v 4.6 c 0,0 0.1,7.5 " + "0.1,7.5 v 24.8 c 0.1,0 0.3,28.2 0.3,28.2 h 29 z", + }(), + SvgEl::polygon{ + SvgAttr::points = "164.7,292 135.7,292 136,291.6 162.6,259.8 185.1,233 " + "207.7,259.8 234.2,291.6 234.5,292 205.5,292 205.4,320.1 " + "205.2,352.5 185.1,352.3 165,352.5 164.9,320.1", + }() + ) + ); + } +} diff --git a/frontend/source/frontend/toolbar.cpp b/frontend/source/frontend/toolbar.cpp index 480b136f..c23908e4 100644 --- a/frontend/source/frontend/toolbar.cpp +++ b/frontend/source/frontend/toolbar.cpp @@ -2,7 +2,6 @@ #include #include #include -#include #include #include #include @@ -15,6 +14,7 @@ #include +#include #include #include #include @@ -192,17 +192,9 @@ Nui::ElementRenderer Toolbar::operator()() // clang-format off return div{class_ = "toolbar"}( - iconPanel({ - .icon = [](){ - return img{ - src = "nui://app.example/icons/nui-sftp-logo.svg", - style = "width: 32px; height: 32px;", - }(); - }(), - .color = "var(--theme-color)", - .padding = 0, - .withBorder = false - }), + div{class_ = "toolbar-brand-mark"}( + Frontend::brandMarkSvg() + ), Snc::select(Snc::SelectOptionsactiveTerminalEngine), decltype(impl_->terminalEngines)>{ .activeOption = impl_->activeTerminalEngine, .options = impl_->terminalEngines, diff --git a/static/styles/toolbar.css b/static/styles/toolbar.css index 99dfa413..7a509254 100644 --- a/static/styles/toolbar.css +++ b/static/styles/toolbar.css @@ -1,6 +1,7 @@ .toolbar { grid-area: Toolbar; display: flex; + align-items: center; border-bottom: 1px solid var(--border-color-fields); background-color: var(--background-color); gap: 5px; @@ -9,4 +10,25 @@ &>.script-nui-select { min-width: 200px; } +} + +/* Inline brand-mark shown at the left of the toolbar. Defaults to a flat + theme-tinted plate; themes (e.g. Nebula) override the chrome for richer + looks. */ +.toolbar-brand-mark { + width: 36px; + height: 36px; + border-radius: 6px; + display: grid; + place-items: center; + color: var(--theme-color); + background: color-mix(in srgb, var(--theme-color) 18%, var(--background-color)); + overflow: hidden; + flex-shrink: 0; +} + +.toolbar-brand-mark svg { + width: 78%; + height: 78%; + display: block; } \ No newline at end of file diff --git a/themes/Nebula.css b/themes/Nebula.css new file mode 100644 index 00000000..2cf44c74 --- /dev/null +++ b/themes/Nebula.css @@ -0,0 +1,278 @@ +:root[data-custom-theme="Nebula"] { + --bg-0: #08080c; + --bg-1: #0e0e14; + --bg-2: #14141c; + --line: rgba(255, 255, 255, 0.07); + --line-strong: rgba(255, 255, 255, 0.13); + --ink: #ecebf2; + --ink-dim: #a6a3b8; + --ink-muted: #6e6b80; + + --accent-a: oklch(0.70 0.18 295); + --accent-b: oklch(0.78 0.13 195); + --accent-grad: linear-gradient(135deg, var(--accent-a), var(--accent-b)); + + --glass-bg: rgba(22, 22, 30, 0.55); + --glass-blur: 22px; + --glass-border: rgba(255, 255, 255, 0.08); + --glass-highlight: rgba(255, 255, 255, 0.06); + + --theme-color: oklch(0.78 0.13 195); + + --background-color: #0e0e14; + --background-color-fields: #14141c; + --brighter-background: #1a1a24; + --darker-background: #08080c; + + --color: #ecebf2; + --color-secondary: #a6a3b8; + --color-fields: #ecebf2; + --subdued-text: #6e6b80; + + --border-color-fields: rgba(255, 255, 255, 0.13); + --foreground-border-color: rgba(255, 255, 255, 0.07); + --resize-handle-color: rgba(255, 255, 255, 0.13); + + --brightness-mixin: black; + --brightness-mixin-percent: 50%; + --anti-brightness-mixin: white; + --anti-brightness-mixin-percent: 80%; + + --opq-card: #0b0b12; + --opq-muted: var(--ink-muted); + --opq-border-faint: rgba(255, 255, 255, 0.06); + --opq-border: rgba(255, 255, 255, 0.18); + --opq-zebra-alternation: rgba(255, 255, 255, 0.025); + --opq-accent: oklch(0.70 0.18 295); + + --app-scrollbar-thumb: rgba(255, 255, 255, 0.10); + --app-scrollbar-thumb-hover: rgba(255, 255, 255, 0.22); + --app-scrollbar-thumb-active: color-mix(in oklch, var(--accent-a) 55%, transparent); + + --sync-progress-color: color-mix(in oklch, var(--accent-b) 35%, transparent); + --sync-done-color: color-mix(in oklch, var(--accent-b) 32%, transparent); + --sync-error-color: rgba(231, 76, 60, 0.40); + + --script-nui-components-tabs-primary-color: var(--accent-b); + --script-nui-components-pagination-primary-color: var(--accent-b); + --script-nui-popup-menu-primary-color: var(--accent-b); + --settings-group-header-title-color: var(--accent-b); + + --nui-file-grid-background-color: transparent; + --nui-file-grid-table-header-background: rgba(20, 20, 28, 0.55); + --nui-file-grid-table-cell-background: transparent; + --nui-file-grid-border-color: rgba(255, 255, 255, 0.07); + --nui-file-grid-item-highlight: color-mix(in oklch, var(--accent-a) 30%, transparent); + --nui-file-grid-symbolic-link-color: color-mix(in oklch, var(--accent-b) 70%, white 20%); +} + +:root[data-custom-theme="Nebula"] body { + background-color: #05060a !important; + background-image: + radial-gradient(1100px 700px at 50% 30%, rgba(30, 45, 80, 0.25), transparent 65%), + radial-gradient(900px 700px at 80% 100%, rgba(15, 22, 45, 0.40), transparent 70%), + radial-gradient(700px 600px at 10% 0%, rgba(15, 22, 45, 0.30), transparent 70%) !important; + background-attachment: fixed !important; +} + +:root[data-custom-theme="Nebula"] .toolbar { + background: + linear-gradient(180deg, rgba(20, 20, 28, 0.85), rgba(14, 14, 20, 0.85)) !important; + border-bottom: 1px solid rgba(255, 255, 255, 0.07) !important; + backdrop-filter: blur(14px) saturate(140%) !important; + -webkit-backdrop-filter: blur(14px) saturate(140%) !important; + box-shadow: 0 1px 0 rgba(255, 255, 255, 0.04) inset !important; +} + +:root[data-custom-theme="Nebula"] .toolbar-brand-mark { + width: 36px !important; + height: 36px !important; + border-radius: 8px !important; + background: #0a0a14 !important; + border: 1px solid rgba(255, 255, 255, 0.13) !important; + color: #cfe0ff !important; + box-shadow: + 0 0 0 1px rgba(255, 255, 255, 0.06) inset, + 0 8px 18px -6px rgba(140, 180, 255, 0.40) !important; + overflow: hidden !important; +} + +:root[data-custom-theme="Nebula"] .toolbar-brand-mark svg { + width: 78% !important; + height: 78% !important; + filter: drop-shadow(0 0 4px rgba(180, 210, 255, 0.50)) !important; +} + +:root[data-custom-theme="Nebula"] .sidebar, +:root[data-custom-theme="Nebula"] .icon-panel-bar { + background: + linear-gradient(180deg, rgba(14, 14, 20, 0.92), rgba(8, 8, 12, 0.92)) !important; + border-right: 1px solid rgba(255, 255, 255, 0.07) !important; +} + +:root[data-custom-theme="Nebula"] dialog, +:root[data-custom-theme="Nebula"] .script-nui-dialog { + background: rgba(22, 22, 30, 0.78) !important; + border: 1px solid rgba(255, 255, 255, 0.08) !important; + border-radius: 14px !important; + box-shadow: + 0 1px 0 rgba(255, 255, 255, 0.06) inset, + 0 30px 60px -30px rgba(0, 0, 0, 0.6), + 0 8px 24px -10px rgba(0, 0, 0, 0.5) !important; + backdrop-filter: blur(22px) saturate(140%) !important; + -webkit-backdrop-filter: blur(22px) saturate(140%) !important; +} + +:root[data-custom-theme="Nebula"] dialog::backdrop { + background: rgba(5, 6, 10, 0.55) !important; + backdrop-filter: blur(12px) saturate(120%) !important; + -webkit-backdrop-filter: blur(12px) saturate(120%) !important; +} + +:root[data-custom-theme="Nebula"] .script-nui-button.style-variant-primary, +:root[data-custom-theme="Nebula"] .script-nui-button[data-style-variant="primary"] { + background: linear-gradient(135deg, var(--accent-a), var(--accent-b)) !important; + color: #0a0a10 !important; + border: 1px solid transparent !important; + box-shadow: + 0 10px 30px -10px color-mix(in oklch, var(--accent-a) 70%, transparent), + 0 0 0 1px rgba(255, 255, 255, 0.12) inset !important; +} + +:root[data-custom-theme="Nebula"] .script-nui-button:not(.style-variant-primary):not([data-style-variant="primary"]):not(.style-variant-danger):not([data-style-variant="danger"]) { + background: rgba(255, 255, 255, 0.04) !important; + border: 1px solid rgba(255, 255, 255, 0.13) !important; + color: var(--color) !important; +} + +:root[data-custom-theme="Nebula"] .script-nui-button:not(.style-variant-primary):not([data-style-variant="primary"]):not(.style-variant-danger):not([data-style-variant="danger"]):hover { + background: rgba(255, 255, 255, 0.08) !important; +} + +:root[data-custom-theme="Nebula"] .nui-file-grid-background { + background-color: transparent !important; + background-image: none !important; +} + +:root[data-custom-theme="Nebula"][data-theme="light"] { + --accent-a: oklch(0.55 0.18 295); + --accent-b: oklch(0.58 0.14 195); + --accent-grad: linear-gradient(135deg, var(--accent-a), var(--accent-b)); + + --glass-bg: rgba(255, 255, 255, 0.55); + --glass-border: rgba(0, 0, 0, 0.08); + --glass-highlight: rgba(255, 255, 255, 0.60); + + --theme-color: oklch(0.58 0.14 195); + + --background-color: #eceef2; + --background-color-fields: #e3e6ec; + --brighter-background: #f4f5f8; + --darker-background: #d6d9e0; + + --color: #1a1b22; + --color-secondary: #4a4b58; + --color-fields: #1a1b22; + --subdued-text: #7a7b88; + + --border-color-fields: rgba(0, 0, 0, 0.13); + --foreground-border-color: rgba(0, 0, 0, 0.07); + --resize-handle-color: rgba(0, 0, 0, 0.13); + + --brightness-mixin: white; + --brightness-mixin-percent: 80%; + --anti-brightness-mixin: black; + --anti-brightness-mixin-percent: 60%; + + --opq-card: #ffffff; + --opq-muted: var(--ink-muted); + --opq-border-faint: rgba(0, 0, 0, 0.06); + --opq-border: rgba(0, 0, 0, 0.13); + --opq-zebra-alternation: rgba(0, 0, 0, 0.025); + --opq-accent: oklch(0.55 0.18 295); + + --app-scrollbar-thumb: rgba(0, 0, 0, 0.18); + --app-scrollbar-thumb-hover: rgba(0, 0, 0, 0.32); + --app-scrollbar-thumb-active: color-mix(in oklch, var(--accent-a) 55%, transparent); + + --sync-progress-color: color-mix(in oklch, var(--accent-b) 32%, transparent); + --sync-done-color: color-mix(in oklch, var(--accent-b) 28%, transparent); + --sync-error-color: rgba(198, 40, 40, 0.32); + + --nui-file-grid-table-header-background: rgba(255, 255, 255, 0.65); + --nui-file-grid-border-color: rgba(0, 0, 0, 0.07); + --nui-file-grid-item-highlight: color-mix(in oklch, var(--accent-a) 22%, transparent); + --nui-file-grid-symbolic-link-color: color-mix(in oklch, var(--accent-a) 70%, black 10%); +} + +:root[data-custom-theme="Nebula"][data-theme="light"] body { + background-color: #eef0f4 !important; + background-image: + radial-gradient(1100px 700px at 50% 30%, rgba(150, 130, 200, 0.10), transparent 65%), + radial-gradient(900px 700px at 80% 100%, rgba(120, 160, 200, 0.12), transparent 70%), + radial-gradient(700px 600px at 10% 0%, rgba(180, 160, 220, 0.08), transparent 70%) !important; + background-attachment: fixed !important; +} + +:root[data-custom-theme="Nebula"][data-theme="light"] .toolbar { + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.72), rgba(244, 246, 250, 0.72)) !important; + border-bottom: 1px solid rgba(0, 0, 0, 0.07) !important; + box-shadow: 0 1px 0 rgba(255, 255, 255, 0.40) inset !important; +} + +:root[data-custom-theme="Nebula"][data-theme="light"] .toolbar-brand-mark { + background: #f7f8fc !important; + border: 1px solid rgba(0, 0, 0, 0.13) !important; + color: #3a2f7a !important; + box-shadow: + 0 0 0 1px rgba(255, 255, 255, 0.60) inset, + 0 8px 18px -6px rgba(120, 100, 200, 0.30) !important; +} + +:root[data-custom-theme="Nebula"][data-theme="light"] .toolbar-brand-mark svg { + filter: drop-shadow(0 0 3px rgba(140, 110, 220, 0.40)) !important; +} + +:root[data-custom-theme="Nebula"][data-theme="light"] .sidebar, +:root[data-custom-theme="Nebula"][data-theme="light"] .icon-panel-bar { + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.90), rgba(244, 246, 250, 0.90)) !important; + border-right: 1px solid rgba(0, 0, 0, 0.07) !important; +} + +:root[data-custom-theme="Nebula"][data-theme="light"] dialog, +:root[data-custom-theme="Nebula"][data-theme="light"] .script-nui-dialog { + background: rgba(255, 255, 255, 0.78) !important; + border: 1px solid rgba(0, 0, 0, 0.08) !important; + box-shadow: + 0 1px 0 rgba(255, 255, 255, 0.60) inset, + 0 30px 60px -30px rgba(60, 70, 110, 0.32), + 0 8px 24px -10px rgba(60, 70, 110, 0.22) !important; +} + +:root[data-custom-theme="Nebula"][data-theme="light"] dialog::backdrop { + background: rgba(220, 222, 230, 0.55) !important; + backdrop-filter: blur(12px) saturate(120%) !important; + -webkit-backdrop-filter: blur(12px) saturate(120%) !important; +} + +:root[data-custom-theme="Nebula"][data-theme="light"] .script-nui-button.style-variant-primary, +:root[data-custom-theme="Nebula"][data-theme="light"] .script-nui-button[data-style-variant="primary"] { + background: linear-gradient(135deg, var(--accent-a), var(--accent-b)) !important; + color: #ffffff !important; + border: 1px solid transparent !important; + box-shadow: + 0 10px 30px -10px color-mix(in oklch, var(--accent-a) 50%, transparent), + 0 0 0 1px rgba(255, 255, 255, 0.20) inset !important; +} + +:root[data-custom-theme="Nebula"][data-theme="light"] .script-nui-button:not(.style-variant-primary):not([data-style-variant="primary"]):not(.style-variant-danger):not([data-style-variant="danger"]) { + background: rgba(255, 255, 255, 0.55) !important; + border: 1px solid rgba(0, 0, 0, 0.13) !important; + color: var(--color) !important; +} + +:root[data-custom-theme="Nebula"][data-theme="light"] .script-nui-button:not(.style-variant-primary):not([data-style-variant="primary"]):not(.style-variant-danger):not([data-style-variant="danger"]):hover { + background: rgba(255, 255, 255, 0.85) !important; +} \ No newline at end of file diff --git a/webpage/frontend/source/frontend/sections/features.cpp b/webpage/frontend/source/frontend/sections/features.cpp index a70aea31..7b6e660b 100644 --- a/webpage/frontend/source/frontend/sections/features.cpp +++ b/webpage/frontend/source/frontend/sections/features.cpp @@ -117,11 +117,11 @@ namespace NuiSftpPage::Sections }, { "cross_platform", - "Native on Windows. Flatpak on Linux. Native on Arch. NixOS & AppImage & Ubuntu 26 coming.", - "First-class Windows builds, distro-agnostic Flatpak today, and native " - "nixpkgs packages on the way. Same UI, same configuration, same " - "keybinds.", - {"Windows 10/11", "AUR", "Flatpak", "AppImage Soon"}, + "Native on Windows. Flathub on Linux. Native on Arch. AppImage everywhere else. NixOS coming.", + "First-class Windows builds, an official Flatpak on Flathub, an AppImage for " + "everything else, and native nixpkgs packages on the way. Same UI, same " + "configuration, same keybinds.", + {"Windows 10/11", "AUR", "Flathub", "AppImage"}, }, }; }(); diff --git a/webpage/frontend/source/frontend/sections/platforms.cpp b/webpage/frontend/source/frontend/sections/platforms.cpp index abfc5f54..4ca18bad 100644 --- a/webpage/frontend/source/frontend/sections/platforms.cpp +++ b/webpage/frontend/source/frontend/sections/platforms.cpp @@ -13,16 +13,37 @@ namespace NuiSftpPage::Sections { // A single distribution row: · · · // `comingSoon` dims the row to mark not-yet-available distros. - Nui::ElementRenderer pkgRow(std::string const& label, std::string const& kind, bool comingSoon = false) + // `copyText` makes the row click-to-copy via globalThis.nuiSftpCopyToClipboard. + Nui::ElementRenderer + pkgRow(std::string const& label, std::string const& kind, bool comingSoon = false, std::string copyText = {}) { using namespace Nui::Elements; using namespace Nui::Attributes; using Nui::Elements::div; using Nui::Elements::span; + std::string classes = "pkg-row"; + if (!copyText.empty()) + classes += " copyable"; + + auto const styleStr = comingSoon ? std::string{"opacity: 0.6"} : std::string{}; + + if (copyText.empty()) + { + return div{ + class_ = classes, + style = styleStr, + }(span{}(label), span{class_ = "sep"}(), span{class_ = "copy"}(kind)); + } + return div{ - class_ = "pkg-row", - style = comingSoon ? "opacity: 0.6" : "", + class_ = classes, + style = styleStr, + Nui::Attributes::title = "Click to copy", + onClick = + [copyText](Nui::val event) { + Nui::val::global("nuiSftpCopyToClipboard")(event["currentTarget"], copyText); + }, }(span{}(label), span{class_ = "sep"}(), span{class_ = "copy"}(kind)); } @@ -71,12 +92,13 @@ namespace NuiSftpPage::Sections using Nui::Elements::div; using Nui::Elements::span; - // "AUR today · Flathub & NixOS soon" — middle dot U+00B7 - std::string const versionLine = "AUR today " + Utf8::cp(0x00B7) + " Flathub & NixOS soon"; + // "AUR · Flathub · AppImage today · NixOS soon" — middle dot U+00B7 + std::string const dot = " " + Utf8::cp(0x00B7) + " "; + std::string const versionLine = "AUR" + dot + "Flathub" + dot + "AppImage today" + dot + "NixOS soon"; std::string const description = - "Native AUR package, an AppImage, and a Flatpak bundle from Releases today. " - "Flathub and nixpkgs are on the way."; + "Native AUR package, an AppImage from Releases, and an official Flatpak on Flathub today. " + "nixpkgs is on the way."; return div{class_ = "platform-card glass shine"}( div{ @@ -92,10 +114,13 @@ namespace NuiSftpPage::Sections }(versionLine) )), p{style = "margin: 0; color: var(--ink-dim); font-size: 14px"}(description), - pkgRow("yay -S nui-sftp", "archlinux user repository"), + pkgRow("yay -S nui-sftp", "archlinux user repository", false, "yay -S nui-sftp"), pkgRow("./nui-sftp-*.AppImage", "appimage"), - pkgRow("flatpak install ./nui-sftp-*.flatpak", "from releases"), - pkgRow("flathub: org.nuicpp.nui_sftp", "soon", true), + pkgRow( + "flatpak install flathub org.nuicpp.nui_sftp", + "flathub", + false, + "flatpak install flathub org.nuicpp.nui_sftp"), pkgRow("nixpkgs.nui-sftp", "soon", true), a{ class_ = "btn primary shine", @@ -119,7 +144,7 @@ namespace NuiSftpPage::Sections div{ class_ = "section-head" }(div{class_ = "section-eyebrow"}("// download"), - h2{class_ = "section-title"}("Native on Windows. AUR on Arch. Flathub & NixOS coming."), + h2{class_ = "section-title"}("Native on Windows. AUR on Arch. Flathub & AppImage everywhere else."), p{class_ = "section-sub"}()), div{class_ = "platforms"}(windowsCard(), linuxCard()) )); diff --git a/webpage/static/index.html b/webpage/static/index.html index a86aafdd..893fafa1 100644 --- a/webpage/static/index.html +++ b/webpage/static/index.html @@ -13,6 +13,7 @@ + diff --git a/webpage/static/source/clipboard.js b/webpage/static/source/clipboard.js new file mode 100644 index 00000000..f7c57637 --- /dev/null +++ b/webpage/static/source/clipboard.js @@ -0,0 +1,63 @@ +/** + * Copy-to-clipboard for install command rows. + * + * On click, copies the given text and briefly swaps the row's `.copy` + * label (e.g. "flathub") with a confirmation ("copied!"), restoring + * after a short delay. + * + * Exposed globally as `globalThis.nuiSftpCopyToClipboard(rowEl, text)` + * so Nui-side click handlers can fire it via emscripten::val. + */ +(function () { + 'use strict'; + + var FEEDBACK_MS = 1200; + + function fallbackCopy(text) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.style.position = 'fixed'; + ta.style.opacity = '0'; + ta.style.pointerEvents = 'none'; + document.body.appendChild(ta); + ta.focus(); + ta.select(); + try { document.execCommand('copy'); } catch (_) { /* best-effort */ } + document.body.removeChild(ta); + } + + function showFeedback(rowEl) { + if (!rowEl) return; + var label = rowEl.querySelector('.copy'); + if (!label) return; + // Don't stack feedbacks if the user mashes the row. + if (label.dataset.original === undefined) { + label.dataset.original = label.textContent; + } + label.textContent = 'copied!'; + rowEl.classList.add('copied'); + + if (rowEl._copyTimer) clearTimeout(rowEl._copyTimer); + rowEl._copyTimer = setTimeout(function () { + label.textContent = label.dataset.original; + delete label.dataset.original; + rowEl.classList.remove('copied'); + rowEl._copyTimer = null; + }, FEEDBACK_MS); + } + + function copy(rowEl, text) { + if (!text) return; + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(text).then( + function () { showFeedback(rowEl); }, + function () { fallbackCopy(text); showFeedback(rowEl); } + ); + } else { + fallbackCopy(text); + showFeedback(rowEl); + } + } + + globalThis.nuiSftpCopyToClipboard = copy; +})(); diff --git a/webpage/static/styles/styles.css b/webpage/static/styles/styles.css index 448c9da7..cfcafc1f 100644 --- a/webpage/static/styles/styles.css +++ b/webpage/static/styles/styles.css @@ -964,6 +964,17 @@ h2.section-title { font-size: 10px; color: var(--ink-muted); letter-spacing: 0.06em; text-transform: uppercase; } +.platform-card .pkg-row.copyable { + cursor: pointer; + transition: background 120ms, border-color 120ms; +} +.platform-card .pkg-row.copyable:hover { + background: rgba(0,0,0,0.45); + border-color: var(--line-strong); +} +.platform-card .pkg-row.copied .copy { + color: var(--accent-a); +} /* ---------- oss block ---------- */ .oss {