Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@ This document defines a unification plan: protocol handling converges into `koko
- Nuxt UI based screens should consume Nuxt UI components first and avoid custom theme branching unless a component is not expressive enough
- custom connector/workspace UIs must not infer colors from `primary` or a single background color; they should only consume semantic tokens such as surface, text, border, hover, selected, and focus

### Mobile layout

- `useMobile()` shares the reactive layout mode; `app.vue` publishes it as `body.mobile`.
- Mobile layout requires a coarse primary pointer with no hover and a viewport width of at most 767px or height of at most 600px. Desktop resizing alone does not activate it.
- Use `body.mobile` for mobile styles, including scoped component styles and teleported content; use `useMobile()` for UI behavior. Width-only responsive rules may still adapt constrained desktop layouts.
- Session and nested tab strips use `--workspace-session-tab-width` and `--workspace-sub-tab-width` for both their ideal strip width and each tab's preferred width, keeping the create control next to the last tab.

## Theme System

The shared workspace should use one theme pipeline for desktop shell UI and connector workspaces.
Expand Down
4 changes: 3 additions & 1 deletion ui/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ const webWorkspaceBrand = useState<string>(WORKSPACE_BRAND_STATE_KEY, () => COMM
const webWorkspaceFavicon = useState<string>(WORKSPACE_FAVICON_STATE_KEY, () => "");

const { isMacOS, isWindows } = usePlatform();
const isMobile = useMobile();
const { locale, setLocale, t } = useI18n();
const uiLocale = computed(() => getUiLocale(locale.value));
watch(locale, (value) => agentClient.setResponseLanguage(normalizeLanguageCode(value)), {
Expand Down Expand Up @@ -128,7 +129,8 @@ useHead({
}),
bodyAttrs: {
class: computed(
() => `${platformClass.value} ${micaClass.value} ${vibrancyClass.value} font-sans antialiased h-screen w-screen`
() =>
`${platformClass.value} ${micaClass.value} ${vibrancyClass.value} ${isMobile.value ? "mobile" : ""} font-sans antialiased h-screen w-screen`
),
style: computed(
() => `
Expand Down
7 changes: 7 additions & 0 deletions ui/assets/css/tokens.css
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@
--data-grid-text: var(--app-text-primary);
--data-grid-text-muted: var(--app-text-muted);
--workspace-toolbar-height: 38px;
--workspace-session-tab-width: 176px;
--workspace-sub-tab-width: 160px;
--workspace-tab-group-accent: var(--color-bg-accent);
--workspace-tab-group-foreground: var(--app-accent-foreground);
--workspace-surface-background: var(--app-surface-canvas);
Expand Down Expand Up @@ -210,6 +212,11 @@
--replay-shadow: var(--theme-shadow-soft);
}

body.mobile {
--workspace-session-tab-width: 128px;
--workspace-sub-tab-width: 120px;
}

.dark {
--app-border-card: color-mix(in srgb, var(--theme-fg) 34%, transparent);
--app-ai-icon-gradient: linear-gradient(
Expand Down
56 changes: 29 additions & 27 deletions ui/components/ConnectForm/connectForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -309,38 +309,40 @@ watch(
background: var(--theme-accent);
}

@media (max-width: 767px), (max-height: 600px) {
.connect-form {
flex: 1;
gap: 0.5rem;
overflow: hidden;
}
body.mobile .connect-form {
flex: 1;
gap: 0.5rem;
overflow: hidden;
}

.protocol-tabs {
flex: 1;
overscroll-behavior-x: contain;
}
body.mobile .protocol-tabs {
flex: 1;
overscroll-behavior-x: contain;
}

.protocol-tab-button,
.connect-form--island .protocol-tab-button {
min-height: 44px;
padding-inline: 12px;
}
body.mobile .protocol-tab-button,
body.mobile .connect-form--island .protocol-tab-button {
min-height: 44px;
padding-inline: 12px;
}

.connect-form-body {
min-height: 0;
overflow-y: auto;
overscroll-behavior-y: contain;
padding: 2px 2px 8px;
scroll-padding-block: 8px;
}
body.mobile .connect-form-body {
min-height: 0;
overflow-y: auto;
overscroll-behavior-y: contain;
padding: 2px 2px 8px;
scroll-padding-block: 8px;
}

.connect-form-controls {
gap: 0.75rem;
}
body.mobile .connect-form-controls {
gap: 0.75rem;
}

.connect-form-controls :deep(input:not([type="checkbox"]):not([type="radio"])),
.connect-form-controls :deep(button[role="combobox"]) {
@media (max-width: 767px) {
body.mobile .connect-form-controls :deep(input:not([type="checkbox"]):not([type="radio"])),
body.mobile .connect-form-controls :deep(button[role="combobox"]),
body.mobile .connect-form-controls :deep(button[aria-haspopup="listbox"]),
body.mobile .connect-form-controls :deep(.remember-secret-button) {
min-height: 44px;
}
}
Expand Down
28 changes: 13 additions & 15 deletions ui/components/ConnectForm/fields.vue
Original file line number Diff line number Diff line change
Expand Up @@ -331,22 +331,20 @@ watchDebounced(
display: none;
}

@media (max-width: 767px), (max-height: 600px) {
.connection-form-fields {
display: flex;
flex: 1;
min-height: 0;
flex-direction: column;
overflow: hidden;
}
body.mobile .connection-form-fields {
display: flex;
flex: 1;
min-height: 0;
flex-direction: column;
overflow: hidden;
}

.connection-form-mobile-submit {
display: inline-flex;
}
body.mobile .connection-form-mobile-submit {
display: inline-flex;
}

.connection-form-remember,
.connection-form-actions {
margin-top: 0.75rem;
}
body.mobile .connection-form-remember,
body.mobile .connection-form-actions {
margin-top: 0.75rem;
}
</style>
64 changes: 31 additions & 33 deletions ui/components/Modal/connectionFormModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -138,43 +138,41 @@ watch(
display: none;
}

@media (max-width: 767px), (max-height: 600px) {
.connection-form-modal-action-target {
display: flex;
flex-shrink: 0;
order: -1;
margin-inline-end: 4px;
}
body.mobile .connection-form-modal-action-target {
display: flex;
flex-shrink: 0;
order: -1;
margin-inline-end: 4px;
}

.connection-form-modal {
width: calc(100vw - 1rem);
height: calc(100dvh - 1rem);
max-height: calc(100dvh - 1rem);
}
body.mobile .connection-form-modal {
width: calc(100vw - 1rem);
height: calc(100dvh - 1rem);
max-height: calc(100dvh - 1rem);
}

.connection-form-modal-body,
.connection-form-modal-fields {
display: flex;
flex: 1;
min-height: 0;
flex-direction: column;
overflow: hidden;
}
body.mobile .connection-form-modal-body,
body.mobile .connection-form-modal-fields {
display: flex;
flex: 1;
min-height: 0;
flex-direction: column;
overflow: hidden;
}

.connection-form-modal-body {
padding: 4px 12px 8px;
}
body.mobile .connection-form-modal-body {
padding: 4px 12px 8px;
}

.connection-form-modal-title {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
body.mobile .connection-form-modal-title {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

.connection-form-modal-close {
position: static;
flex-shrink: 0;
margin-inline-start: 4px;
}
body.mobile .connection-form-modal-close {
position: static;
flex-shrink: 0;
margin-inline-start: 4px;
}
</style>
80 changes: 39 additions & 41 deletions ui/components/Workspace/connectionSetupPane.vue
Original file line number Diff line number Diff line change
Expand Up @@ -546,54 +546,52 @@ onMounted(loadAsset);
0 16px 36px color-mix(in srgb, var(--app-fg) 5%, transparent);
}

@media (max-width: 767px), (max-height: 600px) {
.connection-setup-stage {
overflow: hidden;
padding: 8px;
}
body.mobile .connection-setup-stage {
overflow: hidden;
padding: 8px;
}

.connection-setup-content,
.connection-setup-group {
height: 100%;
min-height: 0;
}
body.mobile .connection-setup-content,
body.mobile .connection-setup-group {
height: 100%;
min-height: 0;
}

.connection-setup-group {
width: min(768px, 100%);
}
body.mobile .connection-setup-group {
width: min(768px, 100%);
}

.connection-setup-shell {
display: flex;
height: 100%;
min-height: 0;
flex-direction: column;
}
body.mobile .connection-setup-shell {
display: flex;
height: 100%;
min-height: 0;
flex-direction: column;
}

.connection-setup-header {
height: 44px;
padding-inline: 12px;
touch-action: auto;
cursor: default;
}
body.mobile .connection-setup-header {
height: 44px;
padding-inline: 12px;
touch-action: auto;
cursor: default;
}

.connection-setup-close {
min-width: 44px;
min-height: 44px;
}
body.mobile .connection-setup-close {
min-width: 44px;
min-height: 44px;
}

.connection-setup-body {
flex: 1;
min-height: 0;
}
body.mobile .connection-setup-body {
flex: 1;
min-height: 0;
}

.connection-setup-form {
padding: 4px 12px 8px;
}
body.mobile .connection-setup-form {
padding: 4px 12px 8px;
}

.connection-setup-form--fields {
display: flex;
overflow: hidden;
flex-direction: column;
}
body.mobile .connection-setup-form--fields {
display: flex;
overflow: hidden;
flex-direction: column;
}
</style>
8 changes: 0 additions & 8 deletions ui/components/Workspace/statusFooter.vue
Original file line number Diff line number Diff line change
Expand Up @@ -179,11 +179,3 @@ onBeforeUnmount(() => {
</div>
</footer>
</template>

<style scoped>
@media (pointer: coarse), (max-width: 767px) {
.workspace-status-footer {
height: 44px;
}
}
</style>
5 changes: 2 additions & 3 deletions ui/components/Workspace/subTabStrip.vue
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,10 @@ const localDraggedId = ref("");
const dropTargetId = ref("");
const dropPlacement = ref<"before" | "after">("before");
const editorTabMime = "application/x-jumpserver-editor-tab";
const SUB_TAB_MAX_WIDTH = 160;
const SUB_TAB_GAP = 4;
const tabStripIdealWidth = computed(() => {
const count = props.tabs.length;
return `${count * SUB_TAB_MAX_WIDTH + Math.max(0, count - 1) * SUB_TAB_GAP}px`;
return `calc(${count} * var(--workspace-sub-tab-width) + ${Math.max(0, count - 1) * SUB_TAB_GAP}px)`;
});
const tabMenuItems = computed<DropdownMenuItem[]>(() =>
props.tabs.map((tab) => ({
Expand Down Expand Up @@ -235,7 +234,7 @@ watch(
<div
v-for="tab in tabs"
:key="tab.id"
class="group relative flex h-full min-w-20 max-w-40 basis-40 grow shrink cursor-pointer items-center gap-1.5 pr-1.5 pl-2.5 text-left text-[11px] leading-none transition-colors"
class="group relative flex h-full min-w-20 max-w-(--workspace-sub-tab-width) basis-(--workspace-sub-tab-width) grow shrink cursor-pointer items-center gap-1.5 pr-1.5 pl-2.5 text-left text-[11px] leading-none transition-colors"
:class="[
activeId === tab.id ? 'text-primary' : 'text-muted',
reorderable ? 'active:cursor-grabbing' : '',
Expand Down
Loading
Loading