mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 06:05:24 +00:00
* feat: shell controller, TabStage, layout and theme plugins with repair view * feat: move the classic layout into its own bundled plugin * feat: move color themes into a bundled themes plugin * fix: plugin tests and type checks work with a linked sdk * fix: home tab, layout switch and missing layout fallbacks for looks * chore: add classic layout and themes to the readme * chore: drop example plugin links from the plugin guide * feat: plugin filters by type, paged browse, hide plugins needing newer termix * feat: move desktop, status, import, audit and branding features into plugins * feat: host lock api so a plugin can load hosts from a config file (#1581) * chore: add hosts config to the plugin table * feat: login policy hook for plugins and optional https redirect (#1584) * chore: add login security to the plugin table * feat: api key socket tickets so a master can open remote sessions (#1654) * feat: sync moves to a plugin, desktop gets multiple accounts (#1580, #1654) * chore: add sync to the plugin table * feat: move the sync screens and wizard out of core into the plugin (#1580) * fix: add a neutral linked desktop hook for plugins (#1580) * fix: show the linked account's name and admin flag in the app (#1580) * feat: ssh transport providers, core ssh relay and plugin folders api * feat: support FIDO2 security keys through ssh agent * feat: add companion panels beside host tabs * feat: mTLS client certificates for desktop server links * feat: native web view for plugins in the desktop app * fix: detect the desktop link window by Electron's default user agent Fixes #1675 * fix: changelog for desktop link fix * fix: keep text primary keys in the plugin data export * fix: ship sharp binaries for Windows arm64 builds * feat: client cert on the sync link types (sdk 1.0.18) * chore: update translations * feat: themed title bar for the desktop app (#1690) * chore: changelog for the themed title bar * fix: draw the title bar in the sidebar color * feat: per host layout for the terminal panel bar * chore: sdk 1.0.19 for the panel bar layout * chore: bundle all 43 plugins for 26.10.3 * chore: update translations --------- Co-authored-by: SirHephaistos <60616553+SirHephaistos@users.noreply.github.com> Co-authored-by: Giovanny Aranda <garanda21@gmail.com>
583 lines
13 KiB
CSS
583 lines
13 KiB
CSS
/* Plugin utilities (termix-plugin build) rank above base, below core's own. */
|
|
@layer properties, termix-plugin-properties, theme, base, components,
|
|
termix-plugin-utilities, utilities;
|
|
@import "tailwindcss";
|
|
@import "tw-animate-css";
|
|
@import "./theme.css";
|
|
@import "@fontsource-variable/jetbrains-mono";
|
|
@import "@fontsource/jetbrains-mono/400.css";
|
|
@import "@fontsource/jetbrains-mono/400-italic.css";
|
|
@import "@fontsource/jetbrains-mono/700.css";
|
|
@import "@fontsource/jetbrains-mono/700-italic.css";
|
|
@import "@fontsource/fira-code/400.css";
|
|
@import "@fontsource/fira-code/700.css";
|
|
@import "@fontsource/source-code-pro/400.css";
|
|
@import "@fontsource/source-code-pro/400-italic.css";
|
|
@import "@fontsource/source-code-pro/700.css";
|
|
@import "@fontsource/source-code-pro/700-italic.css";
|
|
|
|
@font-face {
|
|
font-family: "Caskaydia Cove Nerd Font Mono";
|
|
src: url("/fonts/CaskaydiaCoveNerdFontMono-Regular.ttf") format("truetype");
|
|
font-weight: 400;
|
|
font-style: normal;
|
|
font-display: swap;
|
|
}
|
|
|
|
@font-face {
|
|
font-family: "Caskaydia Cove Nerd Font Mono";
|
|
src: url("/fonts/CaskaydiaCoveNerdFontMono-Bold.ttf") format("truetype");
|
|
font-weight: 700;
|
|
font-style: normal;
|
|
font-display: swap;
|
|
}
|
|
|
|
@font-face {
|
|
font-family: "Caskaydia Cove Nerd Font Mono";
|
|
src: url("/fonts/CaskaydiaCoveNerdFontMono-Italic.ttf") format("truetype");
|
|
font-weight: 400;
|
|
font-style: italic;
|
|
font-display: swap;
|
|
}
|
|
|
|
@font-face {
|
|
font-family: "Caskaydia Cove Nerd Font Mono";
|
|
src: url("/fonts/CaskaydiaCoveNerdFontMono-BoldItalic.ttf") format("truetype");
|
|
font-weight: 700;
|
|
font-style: italic;
|
|
font-display: swap;
|
|
}
|
|
|
|
:root {
|
|
--accent-brand: #f59145;
|
|
--motion-duration-micro: 110ms;
|
|
--motion-duration-panel: 220ms;
|
|
--motion-duration-context: 340ms;
|
|
--motion-ease-enter: cubic-bezier(0.16, 1, 0.3, 1);
|
|
--motion-ease-exit: cubic-bezier(0.4, 0, 1, 1);
|
|
--background: oklch(1 0 0);
|
|
--foreground: oklch(0.1802 0.0043 128.73);
|
|
--card: oklch(1 0 0);
|
|
--card-foreground: oklch(0.1802 0.0043 128.73);
|
|
--popover: oklch(1 0 0);
|
|
--popover-foreground: oklch(0.1802 0.0043 128.73);
|
|
--primary: oklch(0.2115 0.0042 128.68);
|
|
--primary-foreground: oklch(0.985 0 0);
|
|
--secondary: oklch(0.97 0 0);
|
|
--secondary-foreground: oklch(0.2115 0.0042 128.68);
|
|
--muted: oklch(0.97 0 0);
|
|
--muted-foreground: oklch(0.44 0 0);
|
|
--accent: oklch(0.97 0 0);
|
|
--accent-foreground: oklch(0.2115 0.0042 128.68);
|
|
--destructive: oklch(0.577 0.245 27.325);
|
|
--border: oklch(0.922 0 0);
|
|
--input: oklch(0.922 0 0);
|
|
--ring: oklch(0.708 0 0);
|
|
--chart-1: oklch(0.87 0 0);
|
|
--chart-2: oklch(0.556 0 0);
|
|
--chart-3: oklch(0.439 0 0);
|
|
--chart-4: oklch(0.371 0 0);
|
|
--chart-5: oklch(0.269 0 0);
|
|
--radius: 0.625rem;
|
|
--surface: oklch(0.97 0 0);
|
|
--sidebar: oklch(0.985 0 0);
|
|
--sidebar-foreground: oklch(0.1802 0.0043 128.73);
|
|
--sidebar-primary: oklch(0.2115 0.0042 128.68);
|
|
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
--sidebar-accent: oklch(0.97 0 0);
|
|
--sidebar-accent-foreground: oklch(0.2115 0.0042 128.68);
|
|
--sidebar-border: oklch(0.922 0 0);
|
|
--sidebar-ring: oklch(0.708 0 0);
|
|
--warning: oklch(0.795 0.162 86.05);
|
|
--terminal-bg: #ffffff;
|
|
--surface-dim: oklch(0.945 0 0);
|
|
--bg-base: var(--background);
|
|
--border-base: var(--border);
|
|
--foreground-secondary: var(--muted-foreground);
|
|
--canvas: var(--background);
|
|
--overlay: oklch(0 0 0 / 50%);
|
|
}
|
|
|
|
.dark {
|
|
--background: oklch(0.155 0.004 128.73);
|
|
--foreground: oklch(0.985 0 0);
|
|
--card: oklch(0.21 0.004 128.68);
|
|
--card-foreground: oklch(0.985 0 0);
|
|
--popover: oklch(0.21 0.004 128.68);
|
|
--popover-foreground: oklch(0.985 0 0);
|
|
--primary: oklch(0.922 0 0);
|
|
--primary-foreground: oklch(0.2115 0.0042 128.68);
|
|
--secondary: oklch(0.269 0 0);
|
|
--secondary-foreground: oklch(0.985 0 0);
|
|
--surface: oklch(0.185 0.004 128.73);
|
|
--surface-dim: oklch(0.165 0.003 128.73);
|
|
--muted: oklch(0.255 0 0);
|
|
--muted-foreground: oklch(0.72 0 0);
|
|
--accent: oklch(0.255 0 0);
|
|
--accent-foreground: oklch(0.985 0 0);
|
|
--destructive: oklch(0.704 0.191 22.216);
|
|
--border: oklch(1 0 0 / 12%);
|
|
--input: oklch(1 0 0 / 15%);
|
|
--ring: oklch(0.556 0 0);
|
|
--chart-1: oklch(0.87 0 0);
|
|
--chart-2: oklch(0.556 0 0);
|
|
--chart-3: oklch(0.439 0 0);
|
|
--chart-4: oklch(0.371 0 0);
|
|
--chart-5: oklch(0.269 0 0);
|
|
--sidebar: oklch(0.195 0.004 128.68);
|
|
--sidebar-foreground: oklch(0.985 0 0);
|
|
--sidebar-primary: oklch(0.488 0.243 264.376);
|
|
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
--sidebar-accent: oklch(0.255 0 0);
|
|
--sidebar-accent-foreground: oklch(0.985 0 0);
|
|
--sidebar-border: oklch(1 0 0 / 12%);
|
|
--sidebar-ring: oklch(0.556 0 0);
|
|
--warning: oklch(0.852 0.156 91.94);
|
|
--terminal-bg: #0c0d0b;
|
|
}
|
|
|
|
@layer base {
|
|
* {
|
|
/* Tailwind v4 defaults border-color to currentColor, so any bare "border"
|
|
utility picked up the text color and rendered near-white. */
|
|
@apply border-border outline-none;
|
|
scrollbar-width: thin;
|
|
scrollbar-color: oklch(1 0 0 / 20%) transparent;
|
|
}
|
|
*::-webkit-scrollbar {
|
|
width: 6px;
|
|
height: 6px;
|
|
}
|
|
*::-webkit-scrollbar-track {
|
|
background: transparent;
|
|
}
|
|
*::-webkit-scrollbar-thumb {
|
|
background: oklch(1 0 0 / 20%);
|
|
border-radius: 3px;
|
|
}
|
|
*::-webkit-scrollbar-thumb:hover {
|
|
background: oklch(1 0 0 / 35%);
|
|
}
|
|
*::-webkit-scrollbar-corner {
|
|
background: transparent;
|
|
}
|
|
html,
|
|
body,
|
|
#root {
|
|
height: 100dvh;
|
|
@apply overflow-hidden;
|
|
}
|
|
body {
|
|
@apply bg-background text-foreground font-mono;
|
|
}
|
|
html {
|
|
@apply font-mono;
|
|
}
|
|
}
|
|
|
|
/* Icons sized with the size prop carry width/height attributes in px, which
|
|
the interface size setting cannot reach. These restate them in CSS, where
|
|
they become rem like every other length. A size class on the icon wins. */
|
|
@layer base {
|
|
svg.lucide[width="8"] {
|
|
width: 8px;
|
|
height: 8px;
|
|
}
|
|
svg.lucide[width="9"] {
|
|
width: 9px;
|
|
height: 9px;
|
|
}
|
|
svg.lucide[width="10"] {
|
|
width: 10px;
|
|
height: 10px;
|
|
}
|
|
svg.lucide[width="11"] {
|
|
width: 11px;
|
|
height: 11px;
|
|
}
|
|
svg.lucide[width="12"] {
|
|
width: 12px;
|
|
height: 12px;
|
|
}
|
|
svg.lucide[width="13"] {
|
|
width: 13px;
|
|
height: 13px;
|
|
}
|
|
svg.lucide[width="14"] {
|
|
width: 14px;
|
|
height: 14px;
|
|
}
|
|
svg.lucide[width="15"] {
|
|
width: 15px;
|
|
height: 15px;
|
|
}
|
|
svg.lucide[width="16"] {
|
|
width: 16px;
|
|
height: 16px;
|
|
}
|
|
svg.lucide[width="17"] {
|
|
width: 17px;
|
|
height: 17px;
|
|
}
|
|
svg.lucide[width="18"] {
|
|
width: 18px;
|
|
height: 18px;
|
|
}
|
|
svg.lucide[width="19"] {
|
|
width: 19px;
|
|
height: 19px;
|
|
}
|
|
svg.lucide[width="20"] {
|
|
width: 20px;
|
|
height: 20px;
|
|
}
|
|
svg.lucide[width="21"] {
|
|
width: 21px;
|
|
height: 21px;
|
|
}
|
|
svg.lucide[width="22"] {
|
|
width: 22px;
|
|
height: 22px;
|
|
}
|
|
svg.lucide[width="23"] {
|
|
width: 23px;
|
|
height: 23px;
|
|
}
|
|
svg.lucide[width="24"] {
|
|
width: 24px;
|
|
height: 24px;
|
|
}
|
|
svg.lucide[width="25"] {
|
|
width: 25px;
|
|
height: 25px;
|
|
}
|
|
svg.lucide[width="26"] {
|
|
width: 26px;
|
|
height: 26px;
|
|
}
|
|
svg.lucide[width="27"] {
|
|
width: 27px;
|
|
height: 27px;
|
|
}
|
|
svg.lucide[width="28"] {
|
|
width: 28px;
|
|
height: 28px;
|
|
}
|
|
svg.lucide[width="29"] {
|
|
width: 29px;
|
|
height: 29px;
|
|
}
|
|
svg.lucide[width="30"] {
|
|
width: 30px;
|
|
height: 30px;
|
|
}
|
|
svg.lucide[width="31"] {
|
|
width: 31px;
|
|
height: 31px;
|
|
}
|
|
svg.lucide[width="32"] {
|
|
width: 32px;
|
|
height: 32px;
|
|
}
|
|
svg.lucide[width="36"] {
|
|
width: 36px;
|
|
height: 36px;
|
|
}
|
|
svg.lucide[width="40"] {
|
|
width: 40px;
|
|
height: 40px;
|
|
}
|
|
svg.lucide[width="48"] {
|
|
width: 48px;
|
|
height: 48px;
|
|
}
|
|
svg.lucide[width="64"] {
|
|
width: 64px;
|
|
height: 64px;
|
|
}
|
|
svg.lucide[width="96"] {
|
|
width: 96px;
|
|
height: 96px;
|
|
}
|
|
}
|
|
|
|
/* Font size scale applied to html element */
|
|
html.fs-xs {
|
|
font-size: 12px;
|
|
}
|
|
html.fs-sm {
|
|
font-size: 13px;
|
|
}
|
|
html.fs-md {
|
|
font-size: 14px;
|
|
}
|
|
html.fs-lg {
|
|
font-size: 17px;
|
|
}
|
|
html.fs-xl {
|
|
font-size: 20px;
|
|
}
|
|
|
|
@supports (padding-bottom: env(safe-area-inset-bottom)) {
|
|
.safe-bottom {
|
|
padding-bottom: env(safe-area-inset-bottom);
|
|
}
|
|
}
|
|
|
|
@keyframes blink {
|
|
0%,
|
|
100% {
|
|
opacity: 1;
|
|
}
|
|
50% {
|
|
opacity: 0;
|
|
}
|
|
}
|
|
|
|
/* Motion communicates state and hierarchy. Keep it off the terminal glyph layer. */
|
|
.motion-interactive {
|
|
transition-duration: var(--motion-duration-micro);
|
|
transition-timing-function: var(--motion-ease-enter);
|
|
}
|
|
|
|
.motion-meter {
|
|
transition: width var(--motion-duration-panel) var(--motion-ease-enter);
|
|
}
|
|
|
|
.motion-panel-enter {
|
|
animation: motion-panel-enter var(--motion-duration-panel)
|
|
var(--motion-ease-enter) both;
|
|
}
|
|
|
|
.motion-context-enter {
|
|
animation: motion-context-enter var(--motion-duration-context)
|
|
var(--motion-ease-enter) both;
|
|
}
|
|
|
|
.motion-fade-enter {
|
|
animation: motion-fade-enter var(--motion-duration-context)
|
|
var(--motion-ease-enter) both;
|
|
}
|
|
|
|
.motion-workspace-enter {
|
|
animation: motion-workspace-enter var(--motion-duration-panel)
|
|
var(--motion-ease-enter) both;
|
|
}
|
|
|
|
.motion-workspace-layout {
|
|
animation: motion-workspace-layout var(--motion-duration-context)
|
|
var(--motion-ease-enter) both;
|
|
}
|
|
|
|
.motion-transfer-phase {
|
|
animation: motion-transfer-phase var(--motion-duration-micro)
|
|
var(--motion-ease-enter) both;
|
|
}
|
|
|
|
.transfer-progress-track {
|
|
position: relative;
|
|
height: 5px;
|
|
overflow: hidden;
|
|
background: color-mix(in oklab, var(--primary) 16%, transparent);
|
|
}
|
|
|
|
.transfer-progress-fill {
|
|
width: 100%;
|
|
height: 100%;
|
|
background: var(--accent-brand);
|
|
transform: scaleX(var(--transfer-progress, 0));
|
|
transform-origin: left center;
|
|
transition: transform var(--motion-duration-panel) var(--motion-ease-enter);
|
|
}
|
|
|
|
.transfer-progress-indeterminate {
|
|
width: 32%;
|
|
transform: translateX(-110%);
|
|
animation: transfer-progress-flow 1.1s var(--motion-ease-enter) infinite;
|
|
}
|
|
|
|
.transfer-progress-stalled {
|
|
background: var(--destructive);
|
|
animation: none;
|
|
}
|
|
|
|
.transfer-progress-indeterminate.transfer-progress-stalled {
|
|
transform: translateX(105%);
|
|
}
|
|
|
|
.host-status-stripe {
|
|
position: relative;
|
|
isolation: isolate;
|
|
}
|
|
|
|
.host-status-stripe[data-locking="true"]::after {
|
|
content: "";
|
|
position: absolute;
|
|
inset: 0 -3px;
|
|
z-index: -1;
|
|
background: inherit;
|
|
opacity: 0;
|
|
transform: scaleX(1);
|
|
animation: host-signal-lock var(--motion-duration-context)
|
|
var(--motion-ease-enter) both;
|
|
}
|
|
|
|
@keyframes motion-panel-enter {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(6px) scale(0.99);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0) scale(1);
|
|
}
|
|
}
|
|
|
|
@keyframes motion-context-enter {
|
|
from {
|
|
opacity: 0;
|
|
transform: scale(0.995);
|
|
filter: saturate(0.65);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: scale(1);
|
|
filter: saturate(1);
|
|
}
|
|
}
|
|
|
|
@keyframes motion-fade-enter {
|
|
from {
|
|
opacity: 0;
|
|
}
|
|
}
|
|
|
|
@keyframes motion-workspace-enter {
|
|
from {
|
|
opacity: 0;
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
}
|
|
}
|
|
|
|
@keyframes motion-workspace-layout {
|
|
from {
|
|
opacity: 0.72;
|
|
transform: scale(0.996);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: scale(1);
|
|
}
|
|
}
|
|
|
|
@keyframes motion-transfer-phase {
|
|
from {
|
|
opacity: 0.45;
|
|
transform: translateX(-3px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateX(0);
|
|
}
|
|
}
|
|
|
|
@keyframes transfer-progress-flow {
|
|
to {
|
|
transform: translateX(420%);
|
|
}
|
|
}
|
|
|
|
@keyframes host-signal-lock {
|
|
0% {
|
|
opacity: 0;
|
|
transform: scaleX(3.2);
|
|
}
|
|
38% {
|
|
opacity: 0.48;
|
|
}
|
|
100% {
|
|
opacity: 0;
|
|
transform: scaleX(1);
|
|
}
|
|
}
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
:root {
|
|
--motion-duration-micro: 1ms;
|
|
--motion-duration-panel: 1ms;
|
|
--motion-duration-context: 1ms;
|
|
}
|
|
|
|
.motion-panel-enter,
|
|
.motion-context-enter,
|
|
.motion-fade-enter,
|
|
.motion-workspace-enter,
|
|
.motion-workspace-layout,
|
|
.motion-transfer-phase,
|
|
.transfer-progress-indeterminate,
|
|
.host-status-stripe[data-locking="true"]::after {
|
|
animation: none;
|
|
}
|
|
|
|
.motion-meter {
|
|
transition: none;
|
|
}
|
|
|
|
.transfer-progress-indeterminate {
|
|
transform: translateX(105%);
|
|
}
|
|
|
|
*,
|
|
*::before,
|
|
*::after {
|
|
scroll-behavior: auto !important;
|
|
animation-duration: 1ms !important;
|
|
animation-iteration-count: 1 !important;
|
|
transition-duration: 1ms !important;
|
|
}
|
|
}
|
|
|
|
/* A settings search result, marked briefly after the page opens. */
|
|
.settings-search-hit {
|
|
outline: 1px solid var(--accent-brand);
|
|
outline-offset: 2px;
|
|
transition: outline-color 1.2s ease;
|
|
}
|
|
|
|
/* Cards and rows the settings page search filtered out. */
|
|
[data-search-hidden] {
|
|
display: none !important;
|
|
}
|
|
|
|
::highlight(settings-search) {
|
|
background-color: color-mix(in oklab, var(--accent-brand) 35%, transparent);
|
|
color: inherit;
|
|
}
|
|
|
|
/*
|
|
* The title bar the desktop app draws on top of its window. The window has
|
|
* no native frame, so this strip is what moves the window around: the whole
|
|
* bar drags, everything inside it that takes clicks does not.
|
|
*/
|
|
.termix-titlebar {
|
|
-webkit-app-region: drag;
|
|
user-select: none;
|
|
}
|
|
|
|
.termix-titlebar button,
|
|
.termix-titlebar a,
|
|
.termix-titlebar input,
|
|
.termix-titlebar [role="button"] {
|
|
-webkit-app-region: no-drag;
|
|
}
|
|
|
|
/*
|
|
* Layouts size themselves to the viewport with h-dvh. Under the app-drawn
|
|
* title bar the usable height is shorter, so trim it while the bar shows.
|
|
*/
|
|
.h-dvh {
|
|
height: calc(100dvh - var(--termix-titlebar-height, 0px));
|
|
}
|