Skip to content

Commit 3ab9533

Browse files
feat(ui): add space rail navigation (#680)
* feat(ui): add space rail navigation * feat: add tooltips to space rail navigation
1 parent 321547d commit 3ab9533

File tree

15 files changed

+254
-34
lines changed

15 files changed

+254
-34
lines changed

src/main/i18n/locales/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,4 @@ fix(i18n): pt_BR translation
7575
2. Consider the context in which the phrase is used
7676
3. Maintain a consistent style and terminology throughout the translation
7777
4. Verify that your translation fits the UI components (text length, line breaks)
78+
5. For narrow navigation areas such as the space rail, keep product labels short and stable. If a locale would make the label too long, it is acceptable to omit that locale-specific label and rely on the `en_US` fallback while adding localized tooltip keys instead.

src/main/i18n/locales/en_US/ui.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,15 @@
122122
"copied": "Copied",
123123
"currencyUnavailable": "Currency rates service unavailable"
124124
},
125+
"spaces": {
126+
"label": "Spaces",
127+
"code": "Code",
128+
"tools": "Tools",
129+
"math": "Math",
130+
"codeTooltip": "Code snippets",
131+
"toolsTooltip": "Developer tools",
132+
"mathTooltip": "Math notebook"
133+
},
125134
"loading": "App loading...",
126135
"placeholder": {
127136
"emptyTagList": "Add tags to snippets to see them here",

src/main/i18n/locales/ru_RU/ui.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,10 +107,26 @@
107107
"selectedMultiple": "Выбрано сниппетов: {{count}}",
108108
"noSelected": "Нет выбранных сниппетов"
109109
},
110+
"mathNotebook": {
111+
"label": "Математический блокнот",
112+
"sheetList": "Список листов",
113+
"newSheet": "Новый лист",
114+
"untitled": "Без названия",
115+
"copied": "Скопировано",
116+
"currencyUnavailable": "Сервис курсов валют недоступен"
117+
},
118+
"spaces": {
119+
"label": "Пространства",
120+
"codeTooltip": "Сниппеты кода",
121+
"toolsTooltip": "Инструменты разработчика",
122+
"mathTooltip": "Математический блокнот"
123+
},
124+
"loading": "Загрузка приложения...",
110125
"placeholder": {
111126
"emptyTagList": "Добавьте теги к сниппетам, чтобы увидеть их здесь",
112127
"emptyFoldersList": "Нет папок",
113128
"emptySnippetsList": "Нет сниппетов",
129+
"emptySheetList": "Нет листов",
114130
"search": "Поиск",
115131
"addTag": "Добавить тег"
116132
}

src/renderer/App.vue

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import { useApp, useTheme } from '@/composables'
33
import { i18n, ipc } from '@/electron'
44
import { RouterName } from '@/router'
5+
import { isSpaceRouteName } from '@/spaceDefinitions'
56
import { LoaderCircle } from 'lucide-vue-next'
67
import { loadWASM } from 'onigasm'
78
import onigasmFile from 'onigasm/lib/onigasm.wasm?url'
@@ -11,7 +12,7 @@ import { loadGrammars } from './components/editor/grammars'
1112
import { registerIPCListeners } from './ipc'
1213
import { notifications } from './services/notifications'
1314
14-
const { isSponsored, isAppLoading } = useApp()
15+
const { isAppLoading } = useApp()
1516
const route = useRoute()
1617
1718
const showLoader = ref(false)
@@ -64,13 +65,15 @@ init()
6465
data-title-bar
6566
class="absolute top-0 z-50 h-[var(--title-bar-height)] w-full select-none"
6667
/>
67-
<div
68-
v-if="!isSponsored"
69-
class="text-text-muted absolute top-1 right-2 z-50 text-[11px] uppercase"
70-
>
71-
{{ i18n.t("messages:special.unsponsored") }}
72-
</div>
73-
<RouterView />
68+
<RouterView v-slot="{ Component, route: currentRoute }">
69+
<AppSpaceShell v-if="isSpaceRouteName(currentRoute.name)">
70+
<component :is="Component" />
71+
</AppSpaceShell>
72+
<component
73+
:is="Component"
74+
v-else
75+
/>
76+
</RouterView>
7477
<div
7578
v-if="isLoaderVisible"
7679
class="bg-bg absolute inset-0 z-50 flex flex-col items-center justify-center"
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<template>
2+
<div class="grid h-screen grid-cols-[72px_1fr] overflow-hidden">
3+
<div class="bg-bg border-border/70 mt-2 border-r">
4+
<SpaceRail />
5+
</div>
6+
<div class="min-h-0 min-w-0 overflow-hidden">
7+
<slot />
8+
</div>
9+
</div>
10+
</template>
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<script setup lang="ts">
2+
import { useApp } from '@/composables'
3+
4+
const { isSidebarHidden } = useApp()
5+
</script>
6+
7+
<template>
8+
<div
9+
class="grid h-screen"
10+
:class="[
11+
isSidebarHidden
12+
? 'grid-cols-1'
13+
: 'grid-cols-[var(--sidebar-width)_var(--snippet-list-width)_1fr]',
14+
]"
15+
>
16+
<Sidebar v-show="!isSidebarHidden" />
17+
<SnippetList v-show="!isSidebarHidden" />
18+
<Editor />
19+
</div>
20+
</template>

src/renderer/components/layout/TwoColumn.vue

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ interface Props {
77
title: string
88
leftSize?: string
99
rightSize?: string
10+
showBack?: boolean
11+
topSpace?: number
1012
}
1113
1214
interface Emits {
@@ -16,13 +18,21 @@ interface Emits {
1618
const props = withDefaults(defineProps<Props>(), {
1719
leftSize: '220px',
1820
rightSize: '1fr',
21+
showBack: true,
22+
topSpace: 0,
1923
})
2024
2125
const emit = defineEmits<Emits>()
2226
2327
const gridTemplateColumns = computed(() => {
2428
return `${props.leftSize} 1px ${props.rightSize}`
2529
})
30+
31+
const leftHeaderStyle = computed(() => {
32+
return {
33+
paddingTop: `calc(var(--title-bar-height) + ${props.topSpace}px)`,
34+
}
35+
})
2636
</script>
2737

2838
<template>
@@ -32,12 +42,14 @@ const gridTemplateColumns = computed(() => {
3242
>
3343
<div class="grid h-full min-h-0 grid-rows-[auto_1fr] overflow-hidden">
3444
<div
35-
class="mt-2 flex items-center justify-between gap-2 overflow-hidden px-2 pt-[var(--title-bar-height)] pb-2"
45+
class="flex items-center justify-between gap-2 overflow-hidden px-2 pb-2"
46+
:style="leftHeaderStyle"
3647
>
3748
<div class="truncate font-bold">
3849
{{ title }}
3950
</div>
4051
<UiActionButton
52+
v-if="showBack"
4153
:tooltip="i18n.t('button.back')"
4254
class="shrink-0"
4355
@click="() => emit('back')"
@@ -50,7 +62,7 @@ const gridTemplateColumns = computed(() => {
5062
</div>
5163
</div>
5264
<div class="bg-border" />
53-
<div class="mt-2 h-full min-h-0 overflow-auto pt-[var(--title-bar-height)]">
65+
<div class="h-full min-h-0 overflow-auto pt-[var(--title-bar-height)]">
5466
<slot name="right" />
5567
</div>
5668
</div>

src/renderer/components/math-notebook/ResultsPanel.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ function openDocumentation() {
156156
</transition>
157157

158158
<div
159-
class="border-border/40 mb-2 flex h-[28px] shrink-0 items-center justify-between border-t px-2"
159+
class="border-border/40 mb-1 flex h-[28px] shrink-0 items-center justify-between border-t px-2"
160160
>
161161
<UiActionButton
162162
type="iconText"
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
<script setup lang="ts">
2+
import * as Tooltip from '@/components/ui/shadcn/tooltip'
3+
import { useApp, useTheme } from '@/composables'
4+
import { i18n, ipc } from '@/electron'
5+
import { RouterName } from '@/router'
6+
import { getSpaceDefinitions } from '@/spaceDefinitions'
7+
import { Settings } from 'lucide-vue-next'
8+
import { RouterLink, useRoute } from 'vue-router'
9+
import packageJson from '../../../../package.json'
10+
11+
const { isSponsored } = useApp()
12+
const { isDark } = useTheme()
13+
const route = useRoute()
14+
15+
function openDonatePage() {
16+
void ipc.invoke('system:open-external', 'https://masscode.io/donate/')
17+
}
18+
19+
const spaces = computed(() => {
20+
return getSpaceDefinitions().map(space => ({
21+
...space,
22+
active: space.isActive(route.name),
23+
}))
24+
})
25+
</script>
26+
27+
<template>
28+
<nav
29+
class="flex h-full flex-col items-center px-2 pt-[calc(var(--title-bar-height)+8px)] pb-3"
30+
:aria-label="i18n.t('spaces.label')"
31+
>
32+
<Tooltip.TooltipProvider>
33+
<div class="flex w-full flex-col gap-1">
34+
<RouterLink
35+
v-for="space in spaces"
36+
:key="space.id"
37+
v-slot="{ navigate }"
38+
custom
39+
:to="space.to"
40+
>
41+
<Tooltip.Tooltip>
42+
<Tooltip.TooltipTrigger as-child>
43+
<button
44+
type="button"
45+
class="text-text-muted hover:bg-list-selection flex w-full cursor-default flex-col items-center gap-1 rounded-lg px-2 py-2 transition-colors"
46+
:class="{
47+
'bg-list-selection text-list-selection-fg': space.active,
48+
}"
49+
@click="navigate"
50+
>
51+
<component
52+
:is="space.icon"
53+
class="h-4 w-4 shrink-0"
54+
/>
55+
<span class="text-[10px] leading-none font-medium select-none">
56+
{{ space.label }}
57+
</span>
58+
</button>
59+
</Tooltip.TooltipTrigger>
60+
<Tooltip.TooltipContent side="right">
61+
{{ space.tooltip }}
62+
</Tooltip.TooltipContent>
63+
</Tooltip.Tooltip>
64+
</RouterLink>
65+
</div>
66+
</Tooltip.TooltipProvider>
67+
<div
68+
v-if="!isSponsored"
69+
class="mt-auto flex flex-1 flex-col items-center justify-end gap-2 pb-2"
70+
>
71+
<span
72+
class="cursor-pointer text-center text-[9px] leading-none font-semibold tracking-[0.14em] uppercase select-none [writing-mode:sideways-lr]"
73+
:class="isDark ? 'text-amber-300/70' : 'text-violet-500/70'"
74+
role="link"
75+
tabindex="0"
76+
@click="openDonatePage"
77+
@keydown.enter="openDonatePage"
78+
@keydown.space.prevent="openDonatePage"
79+
>
80+
{{ i18n.t("messages:special.unsponsored") }}
81+
</span>
82+
<RouterLink
83+
v-slot="{ navigate }"
84+
custom
85+
:to="{ name: RouterName.preferencesStorage }"
86+
>
87+
<button
88+
type="button"
89+
class="text-text-muted hover:bg-list-selection hover:text-list-selection-fg flex h-8 w-8 cursor-default items-center justify-center rounded-lg transition-colors"
90+
:title="i18n.t('preferences:label')"
91+
@click="navigate"
92+
>
93+
<Settings class="h-4 w-4" />
94+
</button>
95+
</RouterLink>
96+
<div
97+
class="text-text-muted/55 text-[10px] leading-none font-medium select-none"
98+
>
99+
v{{ packageJson.version }}
100+
</div>
101+
</div>
102+
</nav>
103+
</template>

src/renderer/spaceDefinitions.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import type { Component } from 'vue'
2+
import type { RouteLocationRaw, RouteRecordName } from 'vue-router'
3+
import { i18n } from '@/electron'
4+
import { RouterName } from '@/router'
5+
import { Blocks, Calculator, Code2 } from 'lucide-vue-next'
6+
7+
export type SpaceId = 'code' | 'tools' | 'math'
8+
9+
export interface SpaceDefinition {
10+
id: SpaceId
11+
label: string
12+
tooltip: string
13+
icon: Component
14+
to: RouteLocationRaw
15+
isActive: (routeName: RouteRecordName | null | undefined) => boolean
16+
}
17+
18+
function isRouteNameInSpace(
19+
routeName: RouteRecordName | null | undefined,
20+
prefix: string,
21+
) {
22+
return (
23+
typeof routeName === 'string'
24+
&& (routeName === prefix || routeName.startsWith(`${prefix}/`))
25+
)
26+
}
27+
28+
export function getSpaceDefinitions(): SpaceDefinition[] {
29+
return [
30+
{
31+
id: 'code',
32+
label: i18n.t('spaces.code'),
33+
tooltip: i18n.t('spaces.codeTooltip'),
34+
icon: Code2,
35+
to: { name: RouterName.main },
36+
isActive: routeName => routeName === RouterName.main,
37+
},
38+
{
39+
id: 'tools',
40+
label: i18n.t('spaces.tools'),
41+
tooltip: i18n.t('spaces.toolsTooltip'),
42+
icon: Blocks,
43+
to: { name: RouterName.devtoolsCaseConverter },
44+
isActive: routeName =>
45+
isRouteNameInSpace(routeName, RouterName.devtools),
46+
},
47+
{
48+
id: 'math',
49+
label: i18n.t('spaces.math'),
50+
tooltip: i18n.t('spaces.mathTooltip'),
51+
icon: Calculator,
52+
to: { name: RouterName.mathNotebook },
53+
isActive: routeName => routeName === RouterName.mathNotebook,
54+
},
55+
]
56+
}
57+
58+
export function isSpaceRouteName(
59+
routeName: RouteRecordName | null | undefined,
60+
) {
61+
return getSpaceDefinitions().some(space => space.isActive(routeName))
62+
}

0 commit comments

Comments
 (0)