mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 14:45:24 +00:00
v26.10.3 (#1692)
* 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>
This commit is contained in:
577 files changed
+76479
-101831
No files matched your search
@@ -6,6 +6,7 @@ import { SurfaceScope } from "@/components/surface/surface-scope";
|
||||
import { useUiPreferencesContext } from "@/contexts/UiPreferencesContext";
|
||||
import { usePermissions } from "@/hooks/use-permissions";
|
||||
import { isElectron } from "@/lib/electron";
|
||||
import { TITLE_BAR_OFFSET } from "@/shell/TitleBar";
|
||||
import {
|
||||
getOnboardingPlugins,
|
||||
type OnboardingPluginList,
|
||||
@@ -236,6 +237,7 @@ export function OnboardingStage({
|
||||
<OnboardingStageContext.Provider value={stageValue}>
|
||||
<div
|
||||
className="fixed inset-0 z-[60] overflow-y-auto bg-background text-foreground"
|
||||
style={{ paddingTop: TITLE_BAR_OFFSET }}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label={t("onboarding.stageLabel")}
|
||||
|
||||
@@ -20,12 +20,22 @@ export function isStepRelevant(
|
||||
return true;
|
||||
}
|
||||
|
||||
/** A step that took over older keys counts those as seen at version 1. */
|
||||
function effectiveSeen(
|
||||
step: OnboardingStepDef,
|
||||
seen: Record<string, number>,
|
||||
): number {
|
||||
const own = seen[step.id] ?? 0;
|
||||
if (own > 0 || !step.replaces) return own;
|
||||
return step.replaces.some((key) => (seen[key] ?? 0) > 0) ? 1 : 0;
|
||||
}
|
||||
|
||||
export function isStepPending(
|
||||
step: OnboardingStepDef,
|
||||
seen: Record<string, number>,
|
||||
ctx: OnboardingCtx,
|
||||
): boolean {
|
||||
if ((seen[step.id] ?? 0) < step.version) return true;
|
||||
if (effectiveSeen(step, seen) < step.version) return true;
|
||||
try {
|
||||
return step.forcePending?.(ctx) === true;
|
||||
} catch {
|
||||
|
||||
@@ -42,22 +42,33 @@ export interface OnboardingStepDef {
|
||||
forcePending?: (ctx: OnboardingCtx) => boolean;
|
||||
/** Skip stays hidden until this step has been confirmed. */
|
||||
required?: boolean;
|
||||
/** Older step keys this one took over; seeing one counts as seeing this. */
|
||||
replaces?: string[];
|
||||
}
|
||||
|
||||
/** Where each section of plugin steps sits among the core steps. */
|
||||
export const SECTION_POSITION: Record<OnboardingSection, number> = {
|
||||
start: 25,
|
||||
setup: 50,
|
||||
explore: 80,
|
||||
security: 95,
|
||||
};
|
||||
|
||||
/** An explicit position stays inside the flow, before the last core step. */
|
||||
const MIN_POSITION = 1;
|
||||
const MAX_POSITION = 99;
|
||||
|
||||
export const onboardingSteps = createRegistry<OnboardingStepDef>();
|
||||
|
||||
/** Plugin steps sort inside their section by order, then key. */
|
||||
export function sectionPosition(
|
||||
section: OnboardingSection | undefined,
|
||||
order = 0,
|
||||
position?: number,
|
||||
): number {
|
||||
if (typeof position === "number" && Number.isFinite(position)) {
|
||||
return Math.max(MIN_POSITION, Math.min(MAX_POSITION, position));
|
||||
}
|
||||
const base = SECTION_POSITION[section ?? "explore"];
|
||||
return base + Math.max(-1, Math.min(1, order / 1000)) * 0.4;
|
||||
}
|
||||
|
||||
@@ -1,17 +1,7 @@
|
||||
import {
|
||||
LayoutTemplate,
|
||||
Monitor,
|
||||
MonitorDown,
|
||||
Palette,
|
||||
Puzzle,
|
||||
Shield,
|
||||
} from "lucide-react";
|
||||
import { MonitorDown, Puzzle, Shield } from "lucide-react";
|
||||
import { CORE_ONBOARDING_STEPS } from "@/types/onboarding";
|
||||
import { PluginsStep } from "./steps/PluginsStep";
|
||||
import { PresetStep } from "./steps/PresetStep";
|
||||
import { AppearanceStep } from "./steps/AppearanceStep";
|
||||
import { SecurityStep } from "./steps/SecurityStep";
|
||||
import { DesktopSyncStep } from "./steps/DesktopSyncStep";
|
||||
import { DesktopUpdatesStep } from "./steps/DesktopUpdatesStep";
|
||||
import { desktopUpdates } from "@/lib/desktop-updates";
|
||||
import type { OnboardingStepDef } from "./onboarding-registry";
|
||||
@@ -37,16 +27,6 @@ export const CORE_STEPS: OnboardingStepDef[] = [
|
||||
isRelevant: (ctx) => !ctx.pluginsManagedElsewhere && ctx.pluginSetupPending,
|
||||
forcePending: (ctx) => ctx.pluginSetupPending,
|
||||
},
|
||||
{
|
||||
id: "desktop-sync",
|
||||
version: CORE_ONBOARDING_STEPS["desktop-sync"],
|
||||
titleKey: "onboarding.desktopTitle",
|
||||
icon: Monitor,
|
||||
Component: DesktopSyncStep,
|
||||
audience: "all",
|
||||
position: 20,
|
||||
isRelevant: (ctx) => ctx.isDesktop,
|
||||
},
|
||||
{
|
||||
id: "desktop-updates",
|
||||
version: CORE_ONBOARDING_STEPS["desktop-updates"],
|
||||
@@ -58,24 +38,6 @@ export const CORE_STEPS: OnboardingStepDef[] = [
|
||||
// App Store builds have no updater.
|
||||
isRelevant: (ctx) => ctx.isDesktop && desktopUpdates() !== null,
|
||||
},
|
||||
{
|
||||
id: "preset",
|
||||
version: CORE_ONBOARDING_STEPS.preset,
|
||||
titleKey: "onboarding.presetTitle",
|
||||
icon: LayoutTemplate,
|
||||
Component: PresetStep,
|
||||
audience: "all",
|
||||
position: 30,
|
||||
},
|
||||
{
|
||||
id: "appearance",
|
||||
version: CORE_ONBOARDING_STEPS.appearance,
|
||||
titleKey: "onboarding.appearanceTitle",
|
||||
icon: Palette,
|
||||
Component: AppearanceStep,
|
||||
audience: "all",
|
||||
position: 40,
|
||||
},
|
||||
{
|
||||
id: "security",
|
||||
version: CORE_ONBOARDING_STEPS.security,
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
AccentPicker,
|
||||
FontSizePicker,
|
||||
LanguageSelect,
|
||||
ThemeGrid,
|
||||
UiFontSelect,
|
||||
useAppearanceSettings,
|
||||
} from "@/settings/appearance/appearance-controls";
|
||||
|
||||
function Field({
|
||||
label,
|
||||
hint,
|
||||
children,
|
||||
}: {
|
||||
label: string;
|
||||
hint?: string;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<span className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
||||
{label}
|
||||
</span>
|
||||
{children}
|
||||
{hint && (
|
||||
<span className="text-[10px] text-muted-foreground">{hint}</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** Every change applies right away, so the page itself is the preview. */
|
||||
export function AppearanceStep() {
|
||||
const { t } = useTranslation();
|
||||
const appearance = useAppearanceSettings();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-5">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("onboarding.appearanceIntro")}
|
||||
</p>
|
||||
|
||||
<Field label={t("newUi.sidebar.userProfile.themeLabel")}>
|
||||
<ThemeGrid value={appearance.theme} onChange={appearance.setTheme} />
|
||||
</Field>
|
||||
|
||||
<Field label={t("newUi.sidebar.userProfile.accentColorLabel")}>
|
||||
<AccentPicker
|
||||
value={appearance.accent}
|
||||
onChange={appearance.setAccent}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2">
|
||||
<Field
|
||||
label={t("newUi.sidebar.userProfile.interfaceFontLabel")}
|
||||
hint={t("newUi.sidebar.userProfile.interfaceFontDescription")}
|
||||
>
|
||||
<UiFontSelect
|
||||
value={appearance.uiFont}
|
||||
onChange={appearance.setUiFont}
|
||||
/>
|
||||
</Field>
|
||||
<Field label={t("newUi.sidebar.userProfile.fontSizeLabel")}>
|
||||
<FontSizePicker
|
||||
value={appearance.fontSize}
|
||||
onChange={appearance.setFontSize}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<Field label={t("newUi.sidebar.userProfile.languageLabel")}>
|
||||
<LanguageSelect
|
||||
value={appearance.language}
|
||||
onChange={appearance.setLanguage}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Cloud, Laptop } from "lucide-react";
|
||||
import { LinkServerDialog } from "@/settings/sync/LinkServerDialog";
|
||||
import { useSyncStatus } from "@/hooks/use-sync-status";
|
||||
import { setSyncStatus } from "@/lib/sync-status";
|
||||
import { notifySyncChanged } from "@/lib/linked-server";
|
||||
|
||||
/**
|
||||
* Desktop only: use Termix on this device alone, or link it to a server.
|
||||
* Either choice can be changed later from Sync.
|
||||
*/
|
||||
export function DesktopSyncStep() {
|
||||
const { t } = useTranslation();
|
||||
const status = useSyncStatus();
|
||||
const [linkOpen, setLinkOpen] = useState(false);
|
||||
const linked = !!status?.linked;
|
||||
|
||||
const options = [
|
||||
{
|
||||
id: "local",
|
||||
icon: Laptop,
|
||||
title: t("onboarding.desktopLocalTitle"),
|
||||
body: t("onboarding.desktopLocalBody"),
|
||||
selected: !linked,
|
||||
onClick: () => {},
|
||||
},
|
||||
{
|
||||
id: "link",
|
||||
icon: Cloud,
|
||||
title: t("onboarding.desktopLinkTitle"),
|
||||
body: linked
|
||||
? t("onboarding.desktopLinked", {
|
||||
server: status?.serverName || status?.serverUrl,
|
||||
})
|
||||
: t("onboarding.desktopLinkBody"),
|
||||
selected: linked,
|
||||
onClick: () => !linked && setLinkOpen(true),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("onboarding.desktopBody")}
|
||||
</p>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
{options.map(({ id, icon: Icon, title, body, selected, onClick }) => (
|
||||
<button
|
||||
key={id}
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className={`flex items-start gap-2.5 border p-2.5 text-left transition-colors ${
|
||||
selected
|
||||
? "border-accent-brand bg-accent-brand/10"
|
||||
: "border-border bg-card hover:bg-muted/40"
|
||||
}`}
|
||||
>
|
||||
<Icon size={14} className="mt-0.5 shrink-0 text-accent-brand" />
|
||||
<span className="flex flex-col gap-1">
|
||||
<span className="text-xs font-medium">{title}</span>
|
||||
<span className="text-[10px] leading-snug text-muted-foreground">
|
||||
{body}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<LinkServerDialog
|
||||
open={linkOpen}
|
||||
onOpenChange={setLinkOpen}
|
||||
onLinked={(next) => {
|
||||
setSyncStatus(next);
|
||||
notifySyncChanged();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Gauge, Sparkles, Wrench } from "lucide-react";
|
||||
import { useUiPreferencesContext } from "@/contexts/UiPreferencesContext";
|
||||
import { applyPresetSideEffects } from "@/lib/apply-ui-preset";
|
||||
import { PRESETS, type UiPreset } from "@/types/ui-preferences";
|
||||
|
||||
const CHOICES: Exclude<UiPreset, "custom">[] = [
|
||||
"simple",
|
||||
"balanced",
|
||||
"advanced",
|
||||
];
|
||||
|
||||
const ICONS = { simple: Sparkles, balanced: Gauge, advanced: Wrench } as const;
|
||||
|
||||
/**
|
||||
* A miniature of the host list at the given preset. Deliberately drawn rather
|
||||
* than rendering the real component: the point is to show the shape difference
|
||||
* (how many rows fit, how much detail each carries) at a glance.
|
||||
*/
|
||||
function HostRowPreview({ preset }: { preset: Exclude<UiPreset, "custom"> }) {
|
||||
const { hostList } = PRESETS[preset];
|
||||
const rows = hostList.density === "compact" ? 5 : 3;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-[3px] border border-border/60 bg-background/60 p-1.5">
|
||||
{Array.from({ length: rows }).map((_, i) => (
|
||||
<div key={i} className="flex items-stretch gap-1">
|
||||
{hostList.showStatusStripes && (
|
||||
<div className="w-[2px] shrink-0 bg-accent-brand/70" />
|
||||
)}
|
||||
<div className="flex flex-1 flex-col gap-[2px] py-[2px]">
|
||||
<div className="h-[3px] w-2/3 bg-muted-foreground/40" />
|
||||
{hostList.density === "comfortable" && (
|
||||
<div className="h-[2px] w-1/2 bg-muted-foreground/20" />
|
||||
)}
|
||||
{hostList.showTags && (
|
||||
<div className="flex gap-[2px]">
|
||||
<div className="h-[3px] w-3 bg-muted-foreground/20" />
|
||||
<div className="h-[3px] w-2 bg-muted-foreground/20" />
|
||||
</div>
|
||||
)}
|
||||
{hostList.showResourceBars && (
|
||||
<div className="flex gap-[2px]">
|
||||
<div className="h-[2px] w-4 bg-accent-brand/40" />
|
||||
<div className="h-[2px] w-3 bg-accent-brand/25" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function PresetStep() {
|
||||
const { t } = useTranslation();
|
||||
const ctx = useUiPreferencesContext();
|
||||
const selected = ctx?.preferences.preset ?? "balanced";
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("onboarding.presetIntro")}
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 gap-2 sm:grid-cols-3">
|
||||
{CHOICES.map((preset) => {
|
||||
const Icon = ICONS[preset];
|
||||
const active = selected === preset;
|
||||
return (
|
||||
<button
|
||||
key={preset}
|
||||
type="button"
|
||||
aria-pressed={active}
|
||||
onClick={() => {
|
||||
ctx?.setPreset(preset);
|
||||
ctx?.clearAllOverrides();
|
||||
void applyPresetSideEffects(preset);
|
||||
}}
|
||||
className={`flex flex-col gap-2 border p-2.5 text-left transition-colors ${
|
||||
active
|
||||
? "border-accent-brand bg-accent-brand/10"
|
||||
: "border-border bg-card hover:bg-muted/40"
|
||||
}`}
|
||||
>
|
||||
<span className="flex items-center gap-1.5 text-xs font-semibold">
|
||||
<Icon
|
||||
size={13}
|
||||
className={
|
||||
active ? "text-accent-brand" : "text-muted-foreground"
|
||||
}
|
||||
/>
|
||||
{t(`newUi.sidebar.userProfile.preset_${preset}`)}
|
||||
</span>
|
||||
<HostRowPreview preset={preset} />
|
||||
<span className="text-[10px] leading-snug text-muted-foreground">
|
||||
{t(`newUi.sidebar.userProfile.preset_${preset}_desc`)}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<p className="text-[10px] text-muted-foreground/70">
|
||||
{t("onboarding.presetChangeLater")}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user