feat: add taskbar
This commit is contained in:
parent
e43449f8b4
commit
baecaa2e2d
|
@ -19,6 +19,9 @@
|
|||
--taskbar-item-text-color: #ffffff;
|
||||
--taskbar-item-border-color: rgba(255, 255, 255, 0.2);
|
||||
|
||||
--start-menu-background: var(--window-background);
|
||||
--start-menu-border-color: var(--window-border-color);
|
||||
|
||||
/* Shadows */
|
||||
--shadow-window: 0 10px 30px rgba(0, 0, 0, 0.2);
|
||||
--shadow-button: 0 4px 10px rgba(0, 0, 0, 0.2);
|
||||
|
@ -31,6 +34,7 @@
|
|||
/* Z-Indexes */
|
||||
--z-window-base: 100;
|
||||
--z-taskbar: 9999;
|
||||
--z-start-menu: 10000;
|
||||
--z-resizer: 1;
|
||||
}
|
||||
|
||||
|
@ -50,6 +54,9 @@
|
|||
--taskbar-item-background-active: rgba(0, 0, 0, 0.15);
|
||||
--taskbar-item-text-color: #000000;
|
||||
--taskbar-item-border-color: rgba(0, 0, 0, 0.1);
|
||||
|
||||
--start-menu-background: var(--window-background);
|
||||
--start-menu-border-color: var(--window-border-color);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
import { ref } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useWindowsStore } from '../stores/windows';
|
||||
import { useSettingsStore } from '../stores/settings';
|
||||
import { useUIStore } from '../stores/ui';
|
||||
import { useSettingsStore } from '../stores/settings'; // Import settings store
|
||||
import Window from './Window.vue';
|
||||
import Taskbar from './Taskbar.vue';
|
||||
import SnapPreview from './SnapPreview.vue';
|
||||
|
@ -11,23 +11,37 @@ import StartMenu from './StartMenu.vue';
|
|||
import type { SnapType } from '../composables/useDraggable';
|
||||
|
||||
const windowsStore = useWindowsStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
const uiStore = useUIStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
|
||||
const { orderedWindows } = storeToRefs(windowsStore);
|
||||
const { createWindow, snapWindow } = windowsStore;
|
||||
const { toggleTheme } = settingsStore;
|
||||
const { isStartMenuOpen } = storeToRefs(uiStore); // Get start menu state
|
||||
const { createWindow, snapWindow, closeAllWindows } = windowsStore;
|
||||
const { closeStartMenu } = uiStore;
|
||||
const { toggleTheme } = settingsStore;
|
||||
|
||||
const snapPreview = ref<{ x: number; y: number; width: number; height: number; } | null>(null);
|
||||
|
||||
// --- Start Menu Event Handlers ---
|
||||
function handleMenuAction(action: () => void) {
|
||||
action();
|
||||
closeStartMenu();
|
||||
}
|
||||
|
||||
function handleAbout() { handleMenuAction(() => console.log('About clicked')); }
|
||||
function handleSettings() { handleMenuAction(() => console.log('Settings clicked')); }
|
||||
function handleSignOut() { handleMenuAction(() => console.log('Sign Out clicked')); }
|
||||
function handleToggleTheme() { handleMenuAction(toggleTheme); }
|
||||
function handleCloseAllWindows() { handleMenuAction(closeAllWindows); }
|
||||
// --------------------------------
|
||||
|
||||
function handleSnapPreview(snapType: SnapType) {
|
||||
if (!snapType) {
|
||||
snapPreview.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const taskbarHeight = 48;
|
||||
const taskbarHeight = 22;
|
||||
const screenWidth = window.innerWidth;
|
||||
const screenHeight = window.innerHeight - taskbarHeight;
|
||||
|
||||
|
@ -68,15 +82,19 @@ function handleDesktopClick() {
|
|||
/>
|
||||
|
||||
<SnapPreview v-if="snapPreview" :preview="snapPreview" />
|
||||
<StartMenu />
|
||||
<StartMenu
|
||||
v-if="isStartMenuOpen"
|
||||
@about="handleAbout"
|
||||
@settings="handleSettings"
|
||||
@toggle-theme="handleToggleTheme"
|
||||
@sign-out="handleSignOut"
|
||||
@close-all-windows="handleCloseAllWindows"
|
||||
/>
|
||||
|
||||
<div class="fixed top-14 left-4 z-[9999] flex flex-col gap-2">
|
||||
<button @click="() => createWindow('New App')" class="bg-white/20 backdrop-blur-md text-white font-bold py-2 px-4 rounded-lg shadow-lg">
|
||||
+ Create Window
|
||||
</button>
|
||||
<button @click="toggleTheme" class="bg-white/20 backdrop-blur-md text-white font-bold py-2 px-4 rounded-lg shadow-lg">
|
||||
Toggle Theme
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Taskbar />
|
||||
|
@ -90,6 +108,6 @@ function handleDesktopClick() {
|
|||
height: 100vh;
|
||||
background: var(--background-desktop);
|
||||
overflow: hidden;
|
||||
padding-top: 48px;
|
||||
padding-top: 22px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,122 +1,67 @@
|
|||
<script setup lang="ts">
|
||||
import { useUIStore } from '../stores/ui';
|
||||
import { storeToRefs } from 'pinia';
|
||||
|
||||
const uiStore = useUIStore();
|
||||
const { isStartMenuOpen } = storeToRefs(uiStore);
|
||||
|
||||
// Mock data for app list
|
||||
const apps = [
|
||||
{ name: 'File Explorer', icon: '📁' },
|
||||
{ name: 'Web Browser', icon: '🌐' },
|
||||
{ name: 'Settings', icon: '⚙️' },
|
||||
{ name: 'Calculator', icon: '🧮' },
|
||||
{ name: 'Notepad', icon: '📝' },
|
||||
];
|
||||
// This component now only emits events and has no internal logic.
|
||||
const emit = defineEmits([
|
||||
'about',
|
||||
'settings',
|
||||
'toggle-theme',
|
||||
'sign-out',
|
||||
'close-all-windows'
|
||||
]);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<transition name="start-menu-fade">
|
||||
<div v-if="isStartMenuOpen" class="start-menu">
|
||||
<div class="user-profile">
|
||||
<div class="avatar"></div>
|
||||
<span>Daniel</span>
|
||||
</div>
|
||||
<ul class="app-list">
|
||||
<li v-for="app in apps" :key="app.name" class="app-item">
|
||||
<span class="icon">{{ app.icon }}</span>
|
||||
<span class="name">{{ app.name }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="power-controls">
|
||||
<button class="power-btn">Power Off</button>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
<div class="start-menu">
|
||||
<ul>
|
||||
<li @click="emit('about')">About This Project</li>
|
||||
<li class="separator"></li>
|
||||
<li @click="emit('settings')">System Settings...</li>
|
||||
<li @click="emit('toggle-theme')">Toggle Theme</li>
|
||||
<li class="separator"></li>
|
||||
<li @click="emit('sign-out')">Sign Out</li>
|
||||
<li @click="emit('close-all-windows')">Close All Windows</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.start-menu-fade-enter-active,
|
||||
.start-menu-fade-leave-active {
|
||||
transition: opacity 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.start-menu-fade-enter-from,
|
||||
.start-menu-fade-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
|
||||
.start-menu {
|
||||
position: fixed;
|
||||
top: 52px; /* Taskbar height + a small gap */
|
||||
left: 4px;
|
||||
width: 320px;
|
||||
height: 450px;
|
||||
background: var(--taskbar-background);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--taskbar-item-border-color);
|
||||
z-index: calc(var(--z-taskbar) - 1); /* Below taskbar, but above other UI */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: var(--taskbar-item-text-color);
|
||||
top: 26px; /* Adjusted for new 22px taskbar height */
|
||||
left: 8px;
|
||||
width: 240px;
|
||||
background-color: var(--start-menu-background);
|
||||
border: 1px solid var(--start-menu-border-color);
|
||||
border-radius: 12px;
|
||||
z-index: 10000;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
|
||||
padding: 6px;
|
||||
color: var(--content-text-color);
|
||||
}
|
||||
|
||||
.user-profile {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid var(--taskbar-item-border-color);
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background-color: #ccc;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.app-list {
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 8px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
flex-grow: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.app-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px 12px;
|
||||
border-radius: 6px;
|
||||
li {
|
||||
padding: 6px 12px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
border-radius: 6px;
|
||||
transition: background-color 0.15s ease-in-out;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.app-item:hover {
|
||||
li:not(.separator):hover {
|
||||
background-color: var(--taskbar-item-background-hover);
|
||||
}
|
||||
|
||||
.app-item .icon {
|
||||
font-size: 20px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.power-controls {
|
||||
padding: 12px;
|
||||
border-top: 1px solid var(--taskbar-item-border-color);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.power-btn {
|
||||
background: var(--taskbar-item-background);
|
||||
color: var(--taskbar-item-text-color);
|
||||
border: 1px solid var(--taskbar-item-border-color);
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
.separator {
|
||||
height: 1px;
|
||||
background-color: var(--start-menu-border-color);
|
||||
margin: 6px 0;
|
||||
padding: 0;
|
||||
cursor: default;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, watch } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useWindowsStore } from '../stores/windows';
|
||||
import { useUIStore } from '../stores/ui';
|
||||
|
@ -10,6 +11,59 @@ const { windows } = storeToRefs(windowsStore);
|
|||
const { focusWindow } = windowsStore;
|
||||
const { toggleStartMenu } = uiStore;
|
||||
|
||||
// --- Datetime Logic ---
|
||||
const dateString = ref('');
|
||||
const timeString = ref('');
|
||||
let timer: number;
|
||||
|
||||
const updateTime = () => {
|
||||
const now = new Date();
|
||||
dateString.value = now.toLocaleDateString(undefined, { weekday: 'short', month: 'short', day: 'numeric' });
|
||||
timeString.value = now.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit', hour12: false });
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
updateTime();
|
||||
timer = window.setInterval(updateTime, 1000);
|
||||
});
|
||||
|
||||
// --- Input Mode Logic ---
|
||||
const inputMode = ref('A');
|
||||
const isInputMenuOpen = ref(false);
|
||||
const inputSwitcherWrapper = ref<HTMLElement | null>(null);
|
||||
const availableInputModes = [
|
||||
{ key: '注', label: '注音' },
|
||||
{ key: 'A', label: 'English (US)' },
|
||||
];
|
||||
|
||||
function toggleInputMenu() {
|
||||
isInputMenuOpen.value = !isInputMenuOpen.value;
|
||||
}
|
||||
|
||||
function selectInputMode(mode: 'A' | '注') {
|
||||
inputMode.value = mode;
|
||||
isInputMenuOpen.value = false;
|
||||
}
|
||||
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (inputSwitcherWrapper.value && !inputSwitcherWrapper.value.contains(event.target as Node)) {
|
||||
isInputMenuOpen.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
watch(isInputMenuOpen, (isOpen) => {
|
||||
if (isOpen) {
|
||||
document.addEventListener('click', handleClickOutside);
|
||||
} else {
|
||||
document.removeEventListener('click', handleClickOutside);
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
clearInterval(timer);
|
||||
document.removeEventListener('click', handleClickOutside);
|
||||
});
|
||||
|
||||
function handleTaskbarButtonClick(windowId: string) {
|
||||
focusWindow(windowId);
|
||||
}
|
||||
|
@ -29,47 +83,85 @@ function handleTaskbarButtonClick(windowId: string) {
|
|||
{{ window.title }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="taskbar-right-controls">
|
||||
<div class="input-switcher-wrapper" ref="inputSwitcherWrapper">
|
||||
<button @click="toggleInputMenu" class="input-switcher">
|
||||
{{ inputMode }}
|
||||
</button>
|
||||
<div v-if="isInputMenuOpen" class="input-menu">
|
||||
<ul>
|
||||
<li v-for="mode in availableInputModes" :key="mode.key" @click="selectInputMode(mode.key as 'A' | '注')">
|
||||
<span class="checkmark" :style="{ visibility: inputMode === mode.key ? 'visible' : 'hidden' }">✓</span>
|
||||
<span>{{ mode.label }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="datetime">
|
||||
<span>{{ dateString }}</span>
|
||||
<span>{{ timeString }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.taskbar {
|
||||
position: fixed;
|
||||
top: 0; /* Changed from bottom to top */
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 48px;
|
||||
height: 22px;
|
||||
background-color: var(--taskbar-background);
|
||||
backdrop-filter: blur(10px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
padding: 0 8px;
|
||||
z-index: var(--z-taskbar);
|
||||
color: var(--taskbar-item-text-color);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.start-button {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 24px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
padding: 0 12px;
|
||||
color: var(--taskbar-item-text-color);
|
||||
margin-right: 8px;
|
||||
padding: 0 8px;
|
||||
color: inherit;
|
||||
margin-right: 4px;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.window-list {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
gap: 4px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
/* Subtle scrollbar styling */
|
||||
.window-list::-webkit-scrollbar {
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
.window-list::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(255, 255, 255, 0.3);
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
.taskbar-item {
|
||||
background-color: var(--taskbar-item-background);
|
||||
color: var(--taskbar-item-text-color);
|
||||
color: inherit;
|
||||
border: 1px solid var(--taskbar-item-border-color);
|
||||
border-radius: var(--rounded-button);
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
padding: 2px 6px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.taskbar-item:hover {
|
||||
|
@ -80,4 +172,71 @@ function handleTaskbarButtonClick(windowId: string) {
|
|||
background-color: var(--taskbar-item-background-active);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.taskbar-right-controls {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-shrink: 0; /* Prevent right controls from shrinking */
|
||||
}
|
||||
|
||||
.input-switcher-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.input-switcher {
|
||||
background: none;
|
||||
border: none;
|
||||
color: inherit;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.datetime {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.input-menu {
|
||||
position: absolute;
|
||||
top: calc(100% + 4px);
|
||||
right: 0;
|
||||
width: 160px;
|
||||
background-color: var(--start-menu-background);
|
||||
border: 1px solid var(--start-menu-border-color);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||
padding: 4px;
|
||||
z-index: 10001;
|
||||
}
|
||||
|
||||
.input-menu ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.input-menu li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.input-menu li:hover {
|
||||
background-color: var(--taskbar-item-background-hover);
|
||||
}
|
||||
|
||||
.checkmark {
|
||||
width: 16px;
|
||||
text-align: center;
|
||||
margin-right: 4px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
import en from './lang/en.json';
|
||||
import zhTW from './lang/zh-TW.json';
|
||||
|
||||
export default defineI18nConfig(() => ({
|
||||
legacy: false,
|
||||
locale: 'zh-TW',
|
||||
messages: {
|
||||
en,
|
||||
'zh-TW': zhTW,
|
||||
}
|
||||
}))
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"startMenu": {
|
||||
"about": "About This Project",
|
||||
"systemSettings": "System Settings...",
|
||||
"toggleTheme": "Toggle Theme",
|
||||
"signOut": "Sign Out",
|
||||
"closeAllWindows": "Close All Windows"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"startMenu": {
|
||||
"about": "About This Project",
|
||||
"systemSettings": "System Settings...",
|
||||
"toggleTheme": "Toggle Theme",
|
||||
"signOut": "Sign Out",
|
||||
"closeAllWindows": "Close All Windows"
|
||||
}
|
||||
}
|
|
@ -2,11 +2,29 @@
|
|||
export default defineNuxtConfig({
|
||||
devtools: { enabled: true },
|
||||
|
||||
// Register the Pinia module
|
||||
// Register modules
|
||||
modules: [
|
||||
'@pinia/nuxt',
|
||||
'@nuxtjs/i18n',
|
||||
],
|
||||
|
||||
// i18n configuration
|
||||
i18n: {
|
||||
strategy: 'no_prefix', // No URL prefix for locales
|
||||
locales: [
|
||||
{
|
||||
code: 'en',
|
||||
name: 'English'
|
||||
},
|
||||
{
|
||||
code: 'zh-TW',
|
||||
name: '繁體中文'
|
||||
}
|
||||
],
|
||||
defaultLocale: 'zh-TW', // Set default language to Traditional Chinese
|
||||
vueI18n: './i18n.config.ts', // Point to a separate config file for cleaner setup
|
||||
},
|
||||
|
||||
// Global CSS - We will import this in app.vue instead
|
||||
css: [],
|
||||
})
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -25,5 +25,8 @@
|
|||
"typescript": "^5.9.2",
|
||||
"vue": "^3.5.21",
|
||||
"vue-router": "^4.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nuxtjs/i18n": "^10.1.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -82,6 +82,11 @@ export const useWindowsStore = defineStore('windows', () => {
|
|||
}
|
||||
}
|
||||
|
||||
function closeAllWindows() {
|
||||
windows.value = [];
|
||||
focusedWindowId.value = null;
|
||||
}
|
||||
|
||||
function minimizeWindow(id: string) {
|
||||
const windowToMinimize = windows.value.find(w => w.id === id);
|
||||
if (windowToMinimize) {
|
||||
|
@ -108,7 +113,7 @@ export const useWindowsStore = defineStore('windows', () => {
|
|||
const windowToSnap = windows.value.find(w => w.id === id);
|
||||
if (!windowToSnap) return;
|
||||
|
||||
const taskbarHeight = 48;
|
||||
const taskbarHeight = 22; // Updated taskbar height
|
||||
const screenWidth = window.innerWidth;
|
||||
const screenHeight = window.innerHeight - taskbarHeight;
|
||||
|
||||
|
@ -162,6 +167,7 @@ export const useWindowsStore = defineStore('windows', () => {
|
|||
createWindow,
|
||||
focusWindow,
|
||||
closeWindow,
|
||||
closeAllWindows,
|
||||
minimizeWindow,
|
||||
toggleMaximize,
|
||||
snapWindow,
|
||||
|
|
Loading…
Reference in New Issue