@kennofizet/apphub-frontend 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/README.md +84 -0
  2. package/package.json +31 -0
  3. package/src/api/coreApi.js +25 -0
  4. package/src/api/index.js +80 -0
  5. package/src/composables/createZoneContext.js +156 -0
  6. package/src/composables/useAppHubHostApi.js +24 -0
  7. package/src/composables/useAppHubZoneContext.js +11 -0
  8. package/src/composables/useDevOriginToggle.js +40 -0
  9. package/src/i18n/index.js +16 -0
  10. package/src/i18n/resolveLang.js +6 -0
  11. package/src/i18n/resolveTheme.js +30 -0
  12. package/src/i18n/translations/en.js +303 -0
  13. package/src/i18n/translations/vi.js +302 -0
  14. package/src/index.js +427 -0
  15. package/src/moduleStore.js +10 -0
  16. package/src/modules/app-store/components/AppHubAppStoreApp.vue +210 -0
  17. package/src/modules/app-store/components/AppHubAppStoreCard.vue +88 -0
  18. package/src/modules/app-store/components/AppHubAppStoreSettingsPanel.vue +266 -0
  19. package/src/modules/app-store/components/AppHubAppVersionHistory.vue +77 -0
  20. package/src/modules/app-store/components/AppHubDevReviewPanel.vue +206 -0
  21. package/src/modules/app-store/components/AppHubDraftStoreApp.vue +184 -0
  22. package/src/modules/app-store/components/AppHubDraftStoreCard.vue +116 -0
  23. package/src/modules/app-store/composables/useAppStore.js +206 -0
  24. package/src/modules/app-store/composables/useCatalogInfiniteScroll.js +47 -0
  25. package/src/modules/app-store/constants/catalogModes.js +2 -0
  26. package/src/modules/app-store/data/defaultCatalog.js +19 -0
  27. package/src/modules/app-store/index.js +9 -0
  28. package/src/modules/app-store/utils/normalizeCatalogApp.js +37 -0
  29. package/src/modules/desktop/components/AppHubDesktop.vue +1510 -0
  30. package/src/modules/desktop/components/AppHubDesktopDevOriginBar.vue +57 -0
  31. package/src/modules/desktop/components/AppHubDesktopDropLayer.vue +15 -0
  32. package/src/modules/desktop/components/AppHubDesktopDropTarget.vue +32 -0
  33. package/src/modules/desktop/components/AppHubDesktopIconContextMenu.vue +74 -0
  34. package/src/modules/desktop/components/AppHubDesktopIconFolder.vue +60 -0
  35. package/src/modules/desktop/components/AppHubDesktopIconGroup.vue +58 -0
  36. package/src/modules/desktop/components/AppHubDesktopIconInfoDialog.vue +33 -0
  37. package/src/modules/desktop/components/AppHubDesktopIconRenameDialog.vue +62 -0
  38. package/src/modules/desktop/components/AppHubDesktopSettings.vue +28 -0
  39. package/src/modules/desktop/components/AppHubDropInstallBadge.vue +65 -0
  40. package/src/modules/desktop/components/AppHubDuplicateAppDialog.vue +38 -0
  41. package/src/modules/desktop/components/AppHubGuideApp.vue +278 -0
  42. package/src/modules/desktop/components/AppHubOriginBlockScreen.vue +105 -0
  43. package/src/modules/desktop/components/AppHubOriginLoadingScreen.vue +23 -0
  44. package/src/modules/desktop/components/AppHubPlaceholderApp.vue +14 -0
  45. package/src/modules/desktop/components/AppHubSettingsApp.vue +319 -0
  46. package/src/modules/desktop/components/AppHubStartButton.vue +24 -0
  47. package/src/modules/desktop/components/AppHubStartMenu.vue +182 -0
  48. package/src/modules/desktop/components/AppHubTaskbarPins.vue +23 -0
  49. package/src/modules/desktop/components/settings/AppHubSettingsKeyboardPanel.vue +82 -0
  50. package/src/modules/desktop/components/settings/AppHubSettingsScreenPanel.vue +41 -0
  51. package/src/modules/desktop/components/settings/AppHubSettingsStartMenuPanel.vue +95 -0
  52. package/src/modules/desktop/composables/simulateInstallProgress.js +15 -0
  53. package/src/modules/desktop/composables/useDesktopDropInstall.js +272 -0
  54. package/src/modules/desktop/composables/useDesktopHubSettings.js +51 -0
  55. package/src/modules/desktop/composables/useDesktopIconDrag.js +207 -0
  56. package/src/modules/desktop/composables/useDesktopShell.js +335 -0
  57. package/src/modules/desktop/data/builtinApps.js +77 -0
  58. package/src/modules/desktop/index.js +12 -0
  59. package/src/modules/desktop/styles/desktop.css +3104 -0
  60. package/src/modules/desktop/styles/theme.css +616 -0
  61. package/src/modules/desktop/utils/desktopGrid.js +43 -0
  62. package/src/modules/desktop/utils/desktopIconGroups.js +103 -0
  63. package/src/modules/desktop/utils/desktopSession.js +40 -0
  64. package/src/modules/desktop/utils/desktopSettings.js +37 -0
  65. package/src/modules/desktop/utils/dropPackageParser.js +140 -0
  66. package/src/modules/desktop/utils/duplicateAppUtils.js +28 -0
  67. package/src/modules/desktop/utils/hubKeyboardSettings.js +63 -0
  68. package/src/modules/desktop/utils/recentApps.js +148 -0
  69. package/src/modules/desktop/utils/startMenuFavorites.js +100 -0
  70. package/src/modules/desktop/utils/startMenuPins.js +90 -0
  71. package/src/modules/notifications/components/AppHubDesktopNotifications.vue +54 -0
  72. package/src/modules/notifications/composables/createDesktopNotifications.js +86 -0
  73. package/src/modules/notifications/index.js +9 -0
  74. package/src/modules/notifications/styles/notifications.css +118 -0
  75. package/src/modules/notifications/utils/parseApiError.js +29 -0
  76. package/src/modules/runner/components/AppHubRunner.vue +292 -0
  77. package/src/modules/runner/index.js +1 -0
  78. package/src/modules/window-manager/components/AppHubWindowFrame.vue +224 -0
  79. package/src/modules/window-manager/composables/useWindowManager.js +652 -0
  80. package/src/modules/window-manager/index.js +7 -0
  81. package/src/modules/window-manager/utils/sessionLayout.js +28 -0
  82. package/src/modules/window-manager/utils/windowLayout.js +236 -0
  83. package/src/modules/window-manager/utils/windowSnap.js +146 -0
  84. package/src/utils/bootstrapCache.js +47 -0
  85. package/src/utils/devOriginSettings.js +22 -0
  86. package/src/utils/launchUrl.js +111 -0
  87. package/src/utils/originSafety.js +267 -0
  88. package/src/utils/safeStorage.js +191 -0
  89. package/src/utils/semver.js +30 -0
  90. package/src/utils/zoneContext.js +38 -0
@@ -0,0 +1,319 @@
1
+ <template>
2
+ <div class="apphub-settings-app">
3
+ <header class="apphub-settings-app__header">
4
+ <h2 class="apphub-settings-app__title">{{ labels.title }}</h2>
5
+ <p class="apphub-settings-app__subtitle">{{ labels.subtitle }}</p>
6
+ </header>
7
+
8
+ <div class="apphub-settings-app__layout">
9
+ <nav class="apphub-settings-app__nav" :aria-label="labels.nav_label">
10
+ <button
11
+ v-for="item in menuItems"
12
+ :key="item.id"
13
+ type="button"
14
+ class="apphub-settings-app__nav-item"
15
+ :class="{ 'apphub-settings-app__nav-item--active': section === item.id }"
16
+ @click="section = item.id"
17
+ >
18
+ <span class="apphub-settings-app__nav-icon" aria-hidden="true">{{ item.icon }}</span>
19
+ <span class="apphub-settings-app__nav-label">{{ item.label }}</span>
20
+ </button>
21
+ </nav>
22
+
23
+ <div class="apphub-settings-app__body">
24
+ <AppHubSettingsScreenPanel v-if="section === 'screen'" />
25
+ <AppHubSettingsKeyboardPanel v-else-if="section === 'keyboard'" />
26
+ <AppHubSettingsStartMenuPanel v-else-if="section === 'start'" />
27
+ </div>
28
+ </div>
29
+ </div>
30
+ </template>
31
+
32
+ <script setup>
33
+ import { computed, inject, ref } from 'vue'
34
+ import { t } from '../../../i18n/index.js'
35
+ import { resolveLang } from '../../../i18n/resolveLang.js'
36
+ import AppHubSettingsKeyboardPanel from './settings/AppHubSettingsKeyboardPanel.vue'
37
+ import AppHubSettingsScreenPanel from './settings/AppHubSettingsScreenPanel.vue'
38
+ import AppHubSettingsStartMenuPanel from './settings/AppHubSettingsStartMenuPanel.vue'
39
+
40
+ const section = ref('screen')
41
+ const lang = computed(() => resolveLang(inject('apphubOptions', {})?.language, 'vi'))
42
+
43
+ const labels = computed(() => ({
44
+ title: t('hub_settings_app_title', lang.value),
45
+ subtitle: t('hub_settings_app_subtitle', lang.value),
46
+ nav_label: t('hub_settings_app_nav', lang.value),
47
+ menu_screen: t('hub_settings_menu_screen', lang.value),
48
+ menu_keyboard: t('hub_settings_menu_keyboard', lang.value),
49
+ menu_pin_favorite: t('hub_settings_menu_pin_favorite', lang.value),
50
+ }))
51
+
52
+ const menuItems = computed(() => [
53
+ { id: 'screen', label: labels.value.menu_screen, icon: '🖥' },
54
+ { id: 'start', label: labels.value.menu_pin_favorite, icon: '📌' },
55
+ { id: 'keyboard', label: labels.value.menu_keyboard, icon: '⌨' },
56
+ ])
57
+ </script>
58
+
59
+ <style scoped>
60
+ .apphub-settings-app {
61
+ display: flex;
62
+ flex-direction: column;
63
+ height: 100%;
64
+ min-height: 0;
65
+ color: var(--ah-text-secondary, #cbd5e1);
66
+ background: var(--ah-surface, #1e293b);
67
+ }
68
+
69
+ .apphub-settings-app__header {
70
+ flex: 0 0 auto;
71
+ padding: 20px 24px 12px;
72
+ border-bottom: 1px solid var(--ah-border-subtle, rgba(255, 255, 255, 0.08));
73
+ }
74
+
75
+ .apphub-settings-app__title {
76
+ margin: 0;
77
+ font-size: 1.35rem;
78
+ color: var(--ah-text, #f0f4fc);
79
+ }
80
+
81
+ .apphub-settings-app__subtitle {
82
+ margin: 6px 0 0;
83
+ font-size: 0.88rem;
84
+ color: var(--ah-text-muted, #94a3b8);
85
+ }
86
+
87
+ .apphub-settings-app__layout {
88
+ flex: 1;
89
+ min-height: 0;
90
+ display: flex;
91
+ }
92
+
93
+ .apphub-settings-app__nav {
94
+ flex: 0 0 200px;
95
+ display: flex;
96
+ flex-direction: column;
97
+ gap: 4px;
98
+ padding: 12px;
99
+ border-right: 1px solid var(--ah-border-subtle, rgba(255, 255, 255, 0.08));
100
+ overflow-y: auto;
101
+ }
102
+
103
+ .apphub-settings-app__nav-item {
104
+ display: flex;
105
+ align-items: center;
106
+ gap: 10px;
107
+ width: 100%;
108
+ padding: 10px 12px;
109
+ border: 1px solid transparent;
110
+ border-radius: 6px;
111
+ background: transparent;
112
+ color: var(--ah-text-muted, #94a3b8);
113
+ font-size: 0.875rem;
114
+ font-weight: 600;
115
+ text-align: left;
116
+ cursor: pointer;
117
+ transition: background 0.15s ease, color 0.15s ease;
118
+ }
119
+
120
+ .apphub-settings-app__nav-item:hover {
121
+ background: var(--ah-hover, rgba(255, 255, 255, 0.08));
122
+ color: var(--ah-text, #f0f4fc);
123
+ }
124
+
125
+ .apphub-settings-app__nav-item--active {
126
+ background: var(--ah-hover-strong, rgba(255, 255, 255, 0.12));
127
+ border-color: var(--ah-border, rgba(255, 255, 255, 0.1));
128
+ color: var(--ah-text, #f0f4fc);
129
+ }
130
+
131
+ .apphub-settings-app__nav-icon {
132
+ flex-shrink: 0;
133
+ font-size: 1.1rem;
134
+ }
135
+
136
+ .apphub-settings-app__body {
137
+ flex: 1;
138
+ min-width: 0;
139
+ overflow-y: auto;
140
+ padding: 16px 24px 24px;
141
+ }
142
+
143
+ @media (max-width: 520px) {
144
+ .apphub-settings-app__layout {
145
+ flex-direction: column;
146
+ }
147
+
148
+ .apphub-settings-app__nav {
149
+ flex: 0 0 auto;
150
+ flex-direction: row;
151
+ flex-wrap: wrap;
152
+ border-right: none;
153
+ border-bottom: 1px solid var(--ah-border-subtle, rgba(255, 255, 255, 0.08));
154
+ }
155
+
156
+ .apphub-settings-app__nav-item {
157
+ flex: 1 1 auto;
158
+ min-width: 140px;
159
+ }
160
+ }
161
+ </style>
162
+
163
+ <style>
164
+ .apphub-settings-panel__title {
165
+ margin: 0 0 8px;
166
+ font-size: 1rem;
167
+ color: var(--ah-text, #f0f4fc);
168
+ }
169
+
170
+ .apphub-settings-panel__hint {
171
+ margin: 0 0 16px;
172
+ font-size: 0.85rem;
173
+ color: var(--ah-text-muted, #94a3b8);
174
+ line-height: 1.5;
175
+ }
176
+
177
+ .apphub-settings-panel .apphub-desktop-settings {
178
+ display: flex;
179
+ flex-direction: column;
180
+ gap: 4px;
181
+ }
182
+
183
+ .apphub-settings-panel .apphub-desktop-settings__row {
184
+ padding: 10px 12px;
185
+ border-radius: 6px;
186
+ }
187
+
188
+ .apphub-settings-panel__row {
189
+ display: flex;
190
+ align-items: center;
191
+ gap: 10px;
192
+ padding: 10px 12px;
193
+ margin-bottom: 12px;
194
+ border-radius: 6px;
195
+ cursor: pointer;
196
+ }
197
+
198
+ .apphub-settings-panel__row:hover {
199
+ background: var(--ah-hover, rgba(255, 255, 255, 0.06));
200
+ }
201
+
202
+ .apphub-settings-panel__field {
203
+ margin-bottom: 20px;
204
+ }
205
+
206
+ .apphub-settings-panel__label {
207
+ display: block;
208
+ margin-bottom: 6px;
209
+ font-size: 0.85rem;
210
+ color: var(--ah-text-muted, #94a3b8);
211
+ }
212
+
213
+ .apphub-settings-panel__select {
214
+ width: 100%;
215
+ max-width: 320px;
216
+ padding: 8px 10px;
217
+ border-radius: 6px;
218
+ border: 1px solid var(--ah-border, #475569);
219
+ background: var(--ah-surface-raised, #0f172a);
220
+ color: var(--ah-text, #e2e8f0);
221
+ }
222
+
223
+ .apphub-settings-panel__table {
224
+ width: 100%;
225
+ max-width: 520px;
226
+ border-collapse: collapse;
227
+ font-size: 0.875rem;
228
+ margin-bottom: 16px;
229
+ }
230
+
231
+ .apphub-settings-panel__table th,
232
+ .apphub-settings-panel__table td {
233
+ padding: 10px 12px;
234
+ text-align: left;
235
+ border-bottom: 1px solid var(--ah-border-subtle, rgba(255, 255, 255, 0.08));
236
+ }
237
+
238
+ .apphub-settings-panel__table th {
239
+ color: var(--ah-text-muted, #94a3b8);
240
+ font-weight: 600;
241
+ }
242
+
243
+ .apphub-settings-panel__kbd {
244
+ display: inline-block;
245
+ padding: 3px 8px;
246
+ border-radius: 4px;
247
+ background: var(--ah-hover, rgba(0, 0, 0, 0.25));
248
+ border: 1px solid var(--ah-border-subtle, rgba(255, 255, 255, 0.1));
249
+ font-family: inherit;
250
+ font-size: 0.8rem;
251
+ }
252
+
253
+ .apphub-settings-panel__note {
254
+ margin: 8px 0 0;
255
+ font-size: 0.8rem;
256
+ color: var(--ah-text-muted, #94a3b8);
257
+ line-height: 1.5;
258
+ }
259
+
260
+ .apphub-settings-panel__callout {
261
+ margin: 0 0 16px;
262
+ padding: 10px 12px;
263
+ border-radius: 6px;
264
+ border: 1px solid rgba(251, 191, 36, 0.35);
265
+ background: rgba(251, 191, 36, 0.1);
266
+ font-size: 0.85rem;
267
+ color: var(--ah-text-secondary, #cbd5e1);
268
+ line-height: 1.5;
269
+ }
270
+
271
+ .apphub-settings-panel__msg {
272
+ color: var(--ah-text-muted, #94a3b8);
273
+ font-size: 0.875rem;
274
+ }
275
+
276
+ .apphub-settings-panel__pin-list {
277
+ list-style: none;
278
+ margin: 0 0 16px;
279
+ padding: 0;
280
+ display: flex;
281
+ flex-direction: column;
282
+ gap: 8px;
283
+ max-width: 520px;
284
+ }
285
+
286
+ .apphub-settings-panel__pin-item {
287
+ display: flex;
288
+ align-items: center;
289
+ gap: 10px;
290
+ padding: 10px 12px;
291
+ border-radius: 8px;
292
+ background: var(--ah-hover, rgba(255, 255, 255, 0.04));
293
+ border: 1px solid var(--ah-border-subtle, rgba(255, 255, 255, 0.08));
294
+ }
295
+
296
+ .apphub-settings-panel__pin-icon {
297
+ flex-shrink: 0;
298
+ font-size: 1.25rem;
299
+ }
300
+
301
+ .apphub-settings-panel__pin-name {
302
+ flex: 1;
303
+ min-width: 0;
304
+ font-size: 0.9rem;
305
+ white-space: nowrap;
306
+ overflow: hidden;
307
+ text-overflow: ellipsis;
308
+ }
309
+
310
+ .apphub-settings-panel__pin-toggle {
311
+ display: flex;
312
+ align-items: center;
313
+ gap: 8px;
314
+ font-size: 0.8rem;
315
+ color: var(--ah-text-muted, #94a3b8);
316
+ cursor: pointer;
317
+ flex-shrink: 0;
318
+ }
319
+ </style>
@@ -0,0 +1,24 @@
1
+ <template>
2
+ <button
3
+ type="button"
4
+ class="apphub-start-btn"
5
+ :class="{ 'apphub-start-btn--active': active }"
6
+ :title="title"
7
+ :aria-label="title"
8
+ :aria-expanded="active"
9
+ @click.stop="emit('toggle')"
10
+ >
11
+ <svg class="apphub-start-btn__logo" viewBox="0 0 24 24" aria-hidden="true">
12
+ <path fill="currentColor" d="M3 3h8.5v8.5H3V3zm9.5 0H21v8.5h-8.5V3zM3 12.5h8.5V21H3v-8.5zm9.5 0H21V21h-8.5v-8.5z" />
13
+ </svg>
14
+ </button>
15
+ </template>
16
+
17
+ <script setup>
18
+ defineProps({
19
+ active: { type: Boolean, default: false },
20
+ title: { type: String, default: 'Start' },
21
+ })
22
+
23
+ const emit = defineEmits(['toggle'])
24
+ </script>
@@ -0,0 +1,182 @@
1
+ <template>
2
+ <div v-if="open" class="apphub-start" @click.stop="emit('close')">
3
+ <div class="apphub-start__panel" @click.stop>
4
+ <aside class="apphub-start__left">
5
+ <div class="apphub-start__search-wrap">
6
+ <svg class="apphub-start__search-icon" viewBox="0 0 24 24" aria-hidden="true">
7
+ <path
8
+ fill="currentColor"
9
+ d="M15.5 14h-.79l-.28-.27A6.471 6.471 0 0 0 16 9.5 6.5 6.5 0 1 0 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"
10
+ />
11
+ </svg>
12
+ <input
13
+ v-model="query"
14
+ type="search"
15
+ class="apphub-start__search"
16
+ :placeholder="searchPlaceholder"
17
+ @keydown.esc="emit('close')"
18
+ />
19
+ </div>
20
+
21
+ <div class="apphub-start__lists">
22
+ <template v-if="isSearching">
23
+ <p class="apphub-start__section-label">{{ searchResultsLabel }}</p>
24
+ <ul class="apphub-start__list">
25
+ <li v-for="app in searchResults" :key="app.id">
26
+ <button type="button" class="apphub-start__list-item" @click="onOpen(app)">
27
+ <span class="apphub-start__list-icon-wrap">
28
+ <span class="apphub-start__list-icon">{{ app.icon }}</span>
29
+ </span>
30
+ <span class="apphub-start__list-name">{{ app.name }}</span>
31
+ </button>
32
+ </li>
33
+ <li v-if="!searchResults.length" class="apphub-start__empty">{{ emptyLabel }}</li>
34
+ </ul>
35
+ </template>
36
+
37
+ <template v-else>
38
+ <p class="apphub-start__section-label">{{ favoritesLabel }}</p>
39
+ <ul class="apphub-start__list">
40
+ <li v-for="app in favoriteApps" :key="`fav-${app.id}`">
41
+ <button type="button" class="apphub-start__list-item" @click="onOpen(app)">
42
+ <span class="apphub-start__list-icon-wrap">
43
+ <span class="apphub-start__list-icon">{{ app.icon }}</span>
44
+ </span>
45
+ <span class="apphub-start__list-name">{{ app.name }}</span>
46
+ </button>
47
+ </li>
48
+ <li v-if="!favoriteApps.length" class="apphub-start__empty">{{ emptyLabel }}</li>
49
+ </ul>
50
+
51
+ <p class="apphub-start__section-label apphub-start__section-label--spaced">{{ recentLabel }}</p>
52
+ <ul class="apphub-start__list">
53
+ <li v-for="app in recentApps" :key="`recent-${app.id}`">
54
+ <button type="button" class="apphub-start__list-item" @click="onOpen(app)">
55
+ <span class="apphub-start__list-icon-wrap">
56
+ <span class="apphub-start__list-icon">{{ app.icon }}</span>
57
+ </span>
58
+ <span class="apphub-start__list-name">{{ app.name }}</span>
59
+ </button>
60
+ </li>
61
+ <li v-if="!recentApps.length" class="apphub-start__empty">{{ emptyLabel }}</li>
62
+ </ul>
63
+ </template>
64
+ </div>
65
+ </aside>
66
+
67
+ <section class="apphub-start__right">
68
+ <button
69
+ v-if="featuredApp"
70
+ type="button"
71
+ class="apphub-start__hero"
72
+ @click="onOpen(featuredApp)"
73
+ >
74
+ <span class="apphub-start__hero-icon">{{ featuredApp.icon }}</span>
75
+ <span class="apphub-start__hero-body">
76
+ <strong class="apphub-start__hero-title">{{ featuredApp.name }}</strong>
77
+ <span class="apphub-start__hero-hint">{{ featuredApp.hint }}</span>
78
+ </span>
79
+ <span class="apphub-start__hero-arrow" aria-hidden="true">›</span>
80
+ </button>
81
+
82
+ <button
83
+ v-if="guideApp"
84
+ type="button"
85
+ class="apphub-start__hero apphub-start__hero--guide"
86
+ @click="onOpen(guideApp)"
87
+ >
88
+ <span class="apphub-start__hero-icon">{{ guideApp.icon }}</span>
89
+ <span class="apphub-start__hero-body">
90
+ <strong class="apphub-start__hero-title">{{ guideApp.name }}</strong>
91
+ <span class="apphub-start__hero-hint">{{ guideApp.hint }}</span>
92
+ </span>
93
+ <span class="apphub-start__hero-arrow" aria-hidden="true">›</span>
94
+ </button>
95
+
96
+ <button
97
+ v-if="settingsApp"
98
+ type="button"
99
+ class="apphub-start__hero apphub-start__hero--settings"
100
+ @click="onOpen(settingsApp)"
101
+ >
102
+ <span class="apphub-start__hero-icon">{{ settingsApp.icon }}</span>
103
+ <span class="apphub-start__hero-body">
104
+ <strong class="apphub-start__hero-title">{{ settingsApp.name }}</strong>
105
+ <span class="apphub-start__hero-hint">{{ settingsApp.hint }}</span>
106
+ </span>
107
+ <span class="apphub-start__hero-arrow" aria-hidden="true">›</span>
108
+ </button>
109
+
110
+ <p v-if="suggestedApps.length" class="apphub-start__section-label">{{ suggestedLabel }}</p>
111
+ <div v-if="suggestedApps.length" class="apphub-start__grid">
112
+ <button
113
+ v-for="app in suggestedApps"
114
+ :key="app.id"
115
+ type="button"
116
+ class="apphub-start__app-tile"
117
+ @click="onOpen(app)"
118
+ >
119
+ <span class="apphub-start__app-tile-icon">{{ app.icon }}</span>
120
+ <span class="apphub-start__app-tile-name">{{ app.name }}</span>
121
+ </button>
122
+ </div>
123
+ <p v-else-if="!featuredApp && !guideApp && !settingsApp" class="apphub-start__empty apphub-start__empty--right">
124
+ {{ emptyLabel }}
125
+ </p>
126
+ </section>
127
+ </div>
128
+ </div>
129
+ </template>
130
+
131
+ <script setup>
132
+ import { computed, ref, watch } from 'vue'
133
+
134
+ const props = defineProps({
135
+ open: { type: Boolean, default: false },
136
+ favoriteApps: { type: Array, default: () => [] },
137
+ recentApps: { type: Array, default: () => [] },
138
+ suggestedApps: { type: Array, default: () => [] },
139
+ catalogApps: { type: Array, default: () => [] },
140
+ visibleInStartIds: { type: Array, default: () => [] },
141
+ searchPlaceholder: { type: String, default: '' },
142
+ favoritesLabel: { type: String, default: '' },
143
+ recentLabel: { type: String, default: '' },
144
+ searchResultsLabel: { type: String, default: '' },
145
+ suggestedLabel: { type: String, default: '' },
146
+ emptyLabel: { type: String, default: '' },
147
+ })
148
+
149
+ const emit = defineEmits(['close', 'open-app'])
150
+
151
+ const query = ref('')
152
+
153
+ watch(() => props.open, (isOpen) => {
154
+ if (!isOpen) query.value = ''
155
+ })
156
+
157
+ const visibleSet = computed(() => new Set(props.visibleInStartIds ?? []))
158
+
159
+ const catalog = computed(() => props.catalogApps ?? [])
160
+
161
+ const isSearching = computed(() => query.value.trim().length > 0)
162
+
163
+ const searchResults = computed(() => {
164
+ const q = query.value.trim().toLowerCase()
165
+ if (!q) return []
166
+ return catalog.value.filter((app) => app.name?.toLowerCase().includes(q))
167
+ })
168
+
169
+ function findVisibleBuiltin(module) {
170
+ const app = catalog.value.find((a) => a.builtin && a.module === module)
171
+ return app && visibleSet.value.has(app.id) ? app : null
172
+ }
173
+
174
+ const featuredApp = computed(() => findVisibleBuiltin('app-store'))
175
+ const guideApp = computed(() => findVisibleBuiltin('guide'))
176
+ const settingsApp = computed(() => findVisibleBuiltin('settings'))
177
+
178
+ function onOpen(app) {
179
+ emit('open-app', app)
180
+ emit('close')
181
+ }
182
+ </script>
@@ -0,0 +1,23 @@
1
+ <template>
2
+ <div v-if="apps.length" class="apphub-taskbar-pins" role="toolbar" :aria-label="ariaLabel">
3
+ <button
4
+ v-for="app in apps"
5
+ :key="app.id"
6
+ type="button"
7
+ class="apphub-taskbar-pins__btn"
8
+ :title="app.name"
9
+ @click="emit('open-app', app)"
10
+ >
11
+ <span class="apphub-taskbar-pins__icon" aria-hidden="true">{{ app.icon }}</span>
12
+ </button>
13
+ </div>
14
+ </template>
15
+
16
+ <script setup>
17
+ defineProps({
18
+ apps: { type: Array, default: () => [] },
19
+ ariaLabel: { type: String, default: '' },
20
+ })
21
+
22
+ const emit = defineEmits(['open-app'])
23
+ </script>
@@ -0,0 +1,82 @@
1
+ <template>
2
+ <div class="apphub-settings-panel">
3
+ <h3 class="apphub-settings-panel__title">{{ labels.title }}</h3>
4
+ <p class="apphub-settings-panel__hint">{{ labels.hint }}</p>
5
+
6
+ <p class="apphub-settings-panel__callout">{{ labels.browser_note }}</p>
7
+
8
+ <label class="apphub-settings-panel__row">
9
+ <input
10
+ type="checkbox"
11
+ :checked="hub.keyboardSettings.enabled"
12
+ @change="onEnabledChange"
13
+ />
14
+ <span>{{ labels.enabled }}</span>
15
+ </label>
16
+
17
+ <p class="apphub-settings-panel__note">{{ labels.modifier_note }}</p>
18
+
19
+ <table class="apphub-settings-panel__table">
20
+ <thead>
21
+ <tr>
22
+ <th>{{ labels.col_action }}</th>
23
+ <th>{{ labels.col_keys }}</th>
24
+ </tr>
25
+ </thead>
26
+ <tbody>
27
+ <tr v-for="row in shortcutRows" :key="row.id">
28
+ <td>{{ row.label }}</td>
29
+ <td><kbd class="apphub-settings-panel__kbd">{{ row.keys }}</kbd></td>
30
+ </tr>
31
+ </tbody>
32
+ </table>
33
+
34
+ <p class="apphub-settings-panel__note">{{ labels.titlebar_note }}</p>
35
+ </div>
36
+ </template>
37
+
38
+ <script setup>
39
+ import { computed, inject } from 'vue'
40
+ import { t } from '../../../../i18n/index.js'
41
+ import { resolveLang } from '../../../../i18n/resolveLang.js'
42
+ import { useDesktopHubSettings } from '../../composables/useDesktopHubSettings.js'
43
+
44
+ const hub = useDesktopHubSettings()
45
+ const lang = computed(() => resolveLang(inject('apphubOptions', {})?.language, 'vi'))
46
+
47
+ const labels = computed(() => ({
48
+ title: t('hub_settings_keyboard_title', lang.value),
49
+ hint: t('hub_settings_keyboard_hint', lang.value),
50
+ browser_note: t('hub_settings_keyboard_browser_note', lang.value),
51
+ enabled: t('hub_settings_keyboard_enabled', lang.value),
52
+ modifier_note: t('hub_settings_keyboard_modifier_note', lang.value),
53
+ col_action: t('hub_settings_keyboard_col_action', lang.value),
54
+ col_keys: t('hub_settings_keyboard_col_keys', lang.value),
55
+ titlebar_note: t('hub_settings_keyboard_titlebar_note', lang.value),
56
+ snap_left: t('hub_settings_keyboard_snap_left', lang.value),
57
+ snap_right: t('hub_settings_keyboard_snap_right', lang.value),
58
+ snap_up: t('hub_settings_keyboard_snap_up', lang.value),
59
+ snap_down: t('hub_settings_keyboard_snap_down', lang.value),
60
+ modifier_ctrl_alt: t('hub_settings_keyboard_modifier_ctrl_alt', lang.value),
61
+ }))
62
+
63
+ const shortcutRows = computed(() => {
64
+ const mod = labels.value.modifier_ctrl_alt
65
+ const l = lang.value
66
+ return [
67
+ { id: 'left', label: labels.value.snap_left, keys: t('hub_settings_keyboard_key_left', l, { mod }) },
68
+ { id: 'right', label: labels.value.snap_right, keys: t('hub_settings_keyboard_key_right', l, { mod }) },
69
+ { id: 'up', label: labels.value.snap_up, keys: t('hub_settings_keyboard_key_up', l, { mod }) },
70
+ { id: 'down', label: labels.value.snap_down, keys: t('hub_settings_keyboard_key_down', l, { mod }) },
71
+ ]
72
+ })
73
+
74
+ function persist() {
75
+ hub.saveKeyboardSettings?.()
76
+ }
77
+
78
+ function onEnabledChange(event) {
79
+ hub.keyboardSettings.enabled = event.target.checked
80
+ persist()
81
+ }
82
+ </script>
@@ -0,0 +1,41 @@
1
+ <template>
2
+ <div class="apphub-settings-panel">
3
+ <h3 class="apphub-settings-panel__title">{{ labels.title }}</h3>
4
+ <p class="apphub-settings-panel__hint">{{ labels.hint }}</p>
5
+
6
+ <AppHubDesktopSettings
7
+ :snap-to-grid="hub.desktopSettings.snapToGrid"
8
+ :snap-label="labels.snap_grid"
9
+ :theme="activeTheme"
10
+ :theme-label="labels.light_mode"
11
+ :show-theme-toggle="showThemeToggle"
12
+ @update:snap-to-grid="hub.setSnapToGrid"
13
+ @update:theme="hub.setTheme"
14
+ />
15
+
16
+ <AppHubDesktopDevOriginBar v-if="devOriginVisible" placement="settings" class="apphub-settings-panel__dev-origin" />
17
+ </div>
18
+ </template>
19
+
20
+ <script setup>
21
+ import { computed, inject, unref } from 'vue'
22
+ import { t } from '../../../../i18n/index.js'
23
+ import { resolveLang } from '../../../../i18n/resolveLang.js'
24
+ import { useDesktopHubSettings } from '../../composables/useDesktopHubSettings.js'
25
+ import { useDevOriginToggle } from '../../../../composables/useDevOriginToggle.js'
26
+ import AppHubDesktopSettings from '../AppHubDesktopSettings.vue'
27
+ import AppHubDesktopDevOriginBar from '../AppHubDesktopDevOriginBar.vue'
28
+
29
+ const hub = useDesktopHubSettings()
30
+ const { visible: devOriginVisible } = useDevOriginToggle()
31
+ const lang = computed(() => resolveLang(inject('apphubOptions', {})?.language, 'vi'))
32
+ const activeTheme = computed(() => unref(hub.activeTheme) ?? 'dark')
33
+ const showThemeToggle = computed(() => unref(hub.showThemeToggle) !== false)
34
+
35
+ const labels = computed(() => ({
36
+ title: t('hub_settings_screen_title', lang.value),
37
+ hint: t('hub_settings_screen_hint', lang.value),
38
+ snap_grid: t('settings_snap_grid', lang.value),
39
+ light_mode: t('settings_light_mode', lang.value),
40
+ }))
41
+ </script>