@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
package/README.md ADDED
@@ -0,0 +1,84 @@
1
+ # App Hub Frontend
2
+
3
+ Vue 3 **Windows-style desktop** for App Hub. Modular apps run in draggable windows.
4
+
5
+ ## Modules (independent)
6
+
7
+ | Module | Path | Role |
8
+ |--------|------|------|
9
+ | **desktop** | `src/modules/desktop` | Wallpaper, icons, taskbar, Start menu |
10
+ | **window-manager** | `src/modules/window-manager` | Open / close / focus / minimize windows |
11
+ | **app-store** | `src/modules/app-store` | Built-in **App Store** (default Hub app) |
12
+
13
+ Each module has its own `index.js` and can be imported alone:
14
+
15
+ ```js
16
+ import { AppHubDesktop } from '@kennofizet/apphub-frontend/modules/desktop'
17
+ import { useWindowManager } from '@kennofizet/apphub-frontend/modules/window-manager'
18
+ ```
19
+
20
+ ## Install (host app)
21
+
22
+ ```bash
23
+ npm install @kennofizet/apphub-frontend
24
+ ```
25
+
26
+ ```js
27
+ import { createApp } from 'vue'
28
+ import { installAppHubModule, AppHubDesktop } from '@kennofizet/apphub-frontend'
29
+
30
+ const app = createApp(App)
31
+ const hostApi = installAppHubModule(app, {
32
+ coreUrl: 'https://your-api/api/knf',
33
+ backendUrl: 'https://your-api/api/knf/apphub',
34
+ token: sessionTokenFromHost,
35
+ hostAccessSecret: import.meta.env.VITE_APPHUB_HOST_ACCESS_SECRET,
36
+ language: 'vi',
37
+ theme: 'dark',
38
+ themeToggle: false,
39
+ allowedRuntimeOrigins: ['https://publisher-app.example.com'],
40
+ })
41
+ ```
42
+
43
+ **Local dev:** `backendUrl` + `token` is enough on `localhost` — the package auto-relaxes origin checks.
44
+
45
+ **Production:** Bootstrap auto-derives URLs from existing Laravel env (no extra App Hub vars):
46
+
47
+ - Hub host → browser `Origin` on `GET /bootstrap` (where the Vue app runs)
48
+ - Runtime API → `{APP_URL}/{KNF_CORE_API_PREFIX}/{APPHUB_API_PREFIX}` (Laravel backend)
49
+
50
+ Set `APP_URL` to your Laravel API host. Hub SPA can be on a different origin (e.g. Vite `:3000` in dev, `apphub.` subdomain in prod) — bootstrap learns it from the request. Optional overrides: `hubOrigin`, `runtimePublicUrl`. Embed the Hub URL in your product iframe — do not mount Hub inside the product Vue app.
51
+
52
+ `hostApi.grantBridgeScope`, `integrationDocsInternal` — host app only, not `inject()`.
53
+
54
+ Hub shell components use `useAppHubHostApi()` from the package. The host token and `hostAccessSecret` stay in private module credentials — not in `inject('apphubOptions')` and not via `provide('apphubApi')`.
55
+
56
+ ### Who can read internal docs?
57
+
58
+ | Role | Internal docs (`integration-docs/internal`)? |
59
+ |------|---------------------------------------------|
60
+ | End user on Hub desktop | No — use Guide or public `integration-docs` |
61
+ | Publisher | No |
62
+ | packages-core **zone/server manager** (settings in other packages) | **No** — that is not host integrator |
63
+ | **Host team** embedding App Hub | Yes — `APPHUB_HOST_ACCESS_SECRET` + `hostAccessSecret` in `installAppHubModule` |
64
+
65
+ Do not pass `hostAccessSecret` to every logged-in user. Only your host app build/config (dev/ops), never user login API.
66
+
67
+ ```vue
68
+ <template>
69
+ <AppHubDesktop language="vi" theme="light" :theme-toggle="false" />
70
+ </template>
71
+ ```
72
+
73
+ Full-screen route example: `/apphub` → only `<AppHubDesktop />`.
74
+
75
+ ## UX
76
+
77
+ - Desktop icons (double-click to open).
78
+ - **App Store** icon is always on the desktop (install user apps).
79
+ - On first load, App Store window opens automatically (`openAppStoreOnMount`, default `true`).
80
+ - Installed apps from the store appear as new desktop icons (demo placeholder window until backend launch).
81
+
82
+ ## Verify
83
+
84
+ Use `____TEST/test` frontend (manual `npm install` there). Do not run `npm install` inside `apphub-packages` per project rules.
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@kennofizet/apphub-frontend",
3
+ "version": "0.1.0",
4
+ "description": "App Hub Vue 3 UI — Windows-style desktop shell and modular apps (App Store default).",
5
+ "main": "./src/index.js",
6
+ "module": "./src/index.js",
7
+ "type": "module",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./src/index.js",
11
+ "require": "./src/index.js",
12
+ "default": "./src/index.js"
13
+ },
14
+ "./modules/desktop": "./src/modules/desktop/index.js",
15
+ "./modules/app-store": "./src/modules/app-store/index.js",
16
+ "./modules/window-manager": "./src/modules/window-manager/index.js"
17
+ },
18
+ "keywords": ["vue", "vue3", "apphub", "desktop", "frontend"],
19
+ "author": "KennoFizet",
20
+ "license": "MIT",
21
+ "dependencies": {
22
+ "axios": "^1.1.2",
23
+ "vue": "^3.2.0"
24
+ },
25
+ "peerDependencies": {
26
+ "vue": "^3.2.0"
27
+ },
28
+ "engines": {
29
+ "node": ">=16.0.0"
30
+ }
31
+ }
@@ -0,0 +1,25 @@
1
+ import axios from 'axios'
2
+
3
+ const REQUEST_TIMEOUT_MS = 30_000
4
+
5
+ /**
6
+ * packages-core client (zones, auth) — same pattern as workpoint/rewardplay.
7
+ */
8
+ export function createCoreApi(coreUrl, token) {
9
+ const baseURL = (coreUrl || '').replace(/\/$/, '')
10
+
11
+ const client = axios.create({
12
+ baseURL,
13
+ timeout: REQUEST_TIMEOUT_MS,
14
+ headers: {
15
+ Accept: 'application/json',
16
+ 'Content-Type': 'application/json',
17
+ ...(token ? { 'X-Knf-Token': token } : {}),
18
+ },
19
+ })
20
+
21
+ return {
22
+ authCheck: () => client.get('/auth/check'),
23
+ getPlayerZones: () => client.get('/player/zones'),
24
+ }
25
+ }
@@ -0,0 +1,80 @@
1
+ import axios from 'axios'
2
+
3
+ const REQUEST_TIMEOUT_MS = 30_000
4
+
5
+ /**
6
+ * HTTP client for apphub-backend.
7
+ */
8
+ export function createAppHubApi(backendUrl, token, options = {}) {
9
+ const baseURL = (backendUrl || '').replace(/\/$/, '')
10
+ const hostAccessSecret = options.hostAccessSecret || ''
11
+ const getZoneHeaderId = options.getZoneHeaderId
12
+
13
+ const client = axios.create({
14
+ baseURL,
15
+ timeout: REQUEST_TIMEOUT_MS,
16
+ headers: {
17
+ Accept: 'application/json',
18
+ 'Content-Type': 'application/json',
19
+ ...(token ? { 'X-Knf-Token': token } : {}),
20
+ },
21
+ })
22
+
23
+ client.interceptors.request.use((config) => {
24
+ const zoneId = typeof getZoneHeaderId === 'function' ? getZoneHeaderId() : null
25
+ if (zoneId) {
26
+ config.headers = config.headers || {}
27
+ config.headers['X-Knf-Zone-Id'] = zoneId
28
+ }
29
+ return config
30
+ })
31
+
32
+ function bridgeHeaders(launchToken, appSlug) {
33
+ return {
34
+ 'X-AppHub-Launch-Token': launchToken,
35
+ 'X-AppHub-App-Slug': appSlug,
36
+ }
37
+ }
38
+
39
+ function hostHeaders() {
40
+ return hostAccessSecret ? { 'X-AppHub-Host-Access': hostAccessSecret } : {}
41
+ }
42
+
43
+ return {
44
+ bootstrap: () => client.get('/bootstrap'),
45
+ integrationDocs: () => client.get('/integration-docs'),
46
+ integrationDocsInternal: () =>
47
+ client.get('/integration-docs/internal', { headers: hostHeaders() }),
48
+ apps: (params) => client.get('/apps', { params }),
49
+ launch: (slug, payload) => client.post(`/apps/${encodeURIComponent(slug)}/launch`, payload ?? {}),
50
+ ping: (slug) => client.post(`/apps/${encodeURIComponent(slug)}/ping`),
51
+ verifyLaunchToken: (launchToken, appSlug) =>
52
+ client.post('/verify-launch-token', {
53
+ launch_token: launchToken,
54
+ ...(appSlug ? { app_slug: appSlug } : {}),
55
+ }),
56
+ usage: (slug, payload) =>
57
+ client.post(`/apps/${encodeURIComponent(slug)}/usage`, payload),
58
+ devApps: (params) => client.get('/dev/apps', { params }),
59
+ devInspectBundle: (slug) =>
60
+ client.get(`/dev/apps/${encodeURIComponent(slug)}/bundle-inspect`),
61
+ devDisableApp: (slug) =>
62
+ client.post(`/dev/apps/${encodeURIComponent(slug)}/disable`),
63
+ devSetAppStatus: (slug, status) =>
64
+ client.post(`/dev/apps/${encodeURIComponent(slug)}/status`, { status }),
65
+ registerApp: (formData) =>
66
+ client.post('/apps/register', formData, {
67
+ headers: { 'Content-Type': 'multipart/form-data' },
68
+ timeout: 120_000,
69
+ }),
70
+ appVersions: (slug) => client.get(`/apps/${encodeURIComponent(slug)}/versions`),
71
+ grantBridgeScope: (launchToken, scope) =>
72
+ client.post('/bridge/scopes', { launch_token: launchToken, scope }),
73
+ bridgeUser: (launchToken, appSlug) =>
74
+ client.get('/bridge/user', { headers: bridgeHeaders(launchToken, appSlug) }),
75
+ bridgeDesktopMessage: (launchToken, appSlug, payload) =>
76
+ client.post('/bridge/desktop/message', payload, {
77
+ headers: bridgeHeaders(launchToken, appSlug),
78
+ }),
79
+ }
80
+ }
@@ -0,0 +1,156 @@
1
+ import { computed, reactive } from 'vue'
2
+ import {
3
+ loadStoredZone,
4
+ parseZonesFromResponse,
5
+ parseZonesMeta,
6
+ saveStoredZone,
7
+ } from '../utils/zoneContext.js'
8
+
9
+ function parseUserFromBootstrap(resp) {
10
+ const data = resp?.data?.data ?? resp?.data?.datas ?? resp?.data ?? {}
11
+ const user = data.user
12
+ if (!user || user.id == null) return null
13
+ return {
14
+ id: user.id,
15
+ name: user.name ?? String(user.id),
16
+ }
17
+ }
18
+
19
+ /**
20
+ * Hub session — user (apphub bootstrap) + zones (packages-core).
21
+ */
22
+ export function createZoneContextState(getCoreApi, getHostApi, hooks = {}) {
23
+ const state = reactive({
24
+ user: { id: null, name: null },
25
+ zones: [],
26
+ selectedZoneId: null,
27
+ viewAllZones: false,
28
+ loading: false,
29
+ error: '',
30
+ timezone: null,
31
+ isManager: false,
32
+ authOk: false,
33
+ })
34
+
35
+ const selectedZone = computed(() =>
36
+ state.zones.find((z) => z.id === state.selectedZoneId) ?? null,
37
+ )
38
+
39
+ const activeZoneIds = computed(() => {
40
+ if (state.viewAllZones) {
41
+ return state.zones.map((z) => z.id).filter((id) => id != null)
42
+ }
43
+ return state.selectedZoneId != null ? [state.selectedZoneId] : []
44
+ })
45
+
46
+ function selectZone(zoneOrId) {
47
+ const id = typeof zoneOrId === 'object' ? zoneOrId?.id : zoneOrId
48
+ const zone = state.zones.find((z) => z.id === id)
49
+ if (!zone) return
50
+ state.selectedZoneId = zone.id
51
+ state.viewAllZones = false
52
+ saveStoredZone(zone)
53
+ }
54
+
55
+ function setViewAllZones(enabled) {
56
+ state.viewAllZones = !!enabled
57
+ if (!state.viewAllZones && state.selectedZoneId == null && state.zones.length) {
58
+ selectZone(state.zones[0])
59
+ }
60
+ }
61
+
62
+ function resolveInitialZone(list) {
63
+ const stored = loadStoredZone()
64
+ if (stored?.id && list.some((z) => z.id === stored.id)) {
65
+ state.selectedZoneId = stored.id
66
+ return
67
+ }
68
+ if (list.length === 1) {
69
+ selectZone(list[0])
70
+ } else if (list.length > 1 && state.selectedZoneId == null) {
71
+ selectZone(list[0])
72
+ }
73
+ }
74
+
75
+ async function refreshUser() {
76
+ if (hooks.ensureBootstrapSession) {
77
+ await hooks.ensureBootstrapSession()
78
+ return
79
+ }
80
+
81
+ const hostApi = getHostApi?.()
82
+ if (!hostApi?.bootstrap) return
83
+
84
+ try {
85
+ const res = await hostApi.bootstrap()
86
+ hooks.onBootstrap?.(res)
87
+ const user = parseUserFromBootstrap(res)
88
+ if (user) {
89
+ state.user.id = user.id
90
+ state.user.name = user.name
91
+ }
92
+ } catch {
93
+ /* keep previous user */
94
+ }
95
+ }
96
+
97
+ async function refreshZones() {
98
+ const api = getCoreApi?.()
99
+ if (!api?.getPlayerZones) {
100
+ state.error = 'no_core_api'
101
+ state.zones = []
102
+ return
103
+ }
104
+
105
+ state.error = ''
106
+ try {
107
+ if (api.authCheck) {
108
+ try {
109
+ const authRes = await api.authCheck()
110
+ state.authOk = authRes?.data?.success !== false
111
+ } catch {
112
+ state.authOk = false
113
+ }
114
+ }
115
+
116
+ const resp = await api.getPlayerZones()
117
+ const list = parseZonesFromResponse(resp)
118
+ const meta = parseZonesMeta(resp)
119
+ state.zones = list
120
+ state.timezone = meta.timezone
121
+ state.isManager = meta.isManager
122
+ resolveInitialZone(list)
123
+ } catch {
124
+ state.error = 'load_failed'
125
+ state.zones = []
126
+ }
127
+ }
128
+
129
+ async function refresh(options = {}) {
130
+ state.loading = true
131
+ try {
132
+ if (options.skipBootstrap) {
133
+ await refreshZones()
134
+ } else {
135
+ await Promise.all([refreshUser(), refreshZones()])
136
+ }
137
+ } finally {
138
+ state.loading = false
139
+ }
140
+ }
141
+
142
+ function getZoneHeaderId() {
143
+ if (state.viewAllZones || state.selectedZoneId == null) return null
144
+ return String(state.selectedZoneId)
145
+ }
146
+
147
+ return {
148
+ state,
149
+ selectedZone,
150
+ activeZoneIds,
151
+ selectZone,
152
+ setViewAllZones,
153
+ refresh,
154
+ getZoneHeaderId,
155
+ }
156
+ }
@@ -0,0 +1,24 @@
1
+ import { getCurrentInstance } from 'vue'
2
+ import { getAppHubStore } from '../moduleStore.js'
3
+
4
+ export function resolveRootApp(instance = getCurrentInstance()) {
5
+ if (!instance) return null
6
+ return instance.appContext?.app ?? instance.app ?? null
7
+ }
8
+
9
+ export function getHostApiForApp(app) {
10
+ return getAppHubStore(app)?.facade ?? null
11
+ }
12
+
13
+ export function isBackendReadyForApp(app) {
14
+ const store = getAppHubStore(app)
15
+ return !!(store?.credentials?.backendUrl && store?.credentials?.token)
16
+ }
17
+
18
+ /**
19
+ * Host-only API — use in Hub shell components. Not provided via inject
20
+ * so publisher app code cannot access grantBridgeScope or internal docs.
21
+ */
22
+ export function useAppHubHostApi() {
23
+ return getHostApiForApp(resolveRootApp())
24
+ }
@@ -0,0 +1,11 @@
1
+ import { inject } from 'vue'
2
+
3
+ export const APPHUB_ZONE_CONTEXT_KEY = 'apphubZoneContext'
4
+
5
+ export function useAppHubZoneContext() {
6
+ const ctx = inject(APPHUB_ZONE_CONTEXT_KEY, null)
7
+ if (!ctx) {
8
+ throw new Error('useAppHubZoneContext() requires installAppHubModule()')
9
+ }
10
+ return ctx
11
+ }
@@ -0,0 +1,40 @@
1
+ import { computed, getCurrentInstance, inject } from 'vue'
2
+ import { getAppHubStore } from '../moduleStore.js'
3
+ import { saveDevFriendlyOriginsPreference } from '../utils/devOriginSettings.js'
4
+ import { isLocalDevHostPage } from '../utils/originSafety.js'
5
+
6
+ function reconcileAfterToggle(vueApp, enabled) {
7
+ import('../index.js').then(({ installAppHubModule }) => {
8
+ installAppHubModule(vueApp, { enforceDevFriendlyOrigins: enabled, isDevUser: true })
9
+ })
10
+ }
11
+
12
+ /** Dev-only localhost toggle — requires bootstrap is_dev_user (APPHUB_DEV_USER_IDS). */
13
+ export function useDevOriginToggle() {
14
+ const moduleOptions = inject('apphubOptions', {})
15
+ const vueApp = getCurrentInstance()?.appContext?.app
16
+
17
+ const visible = computed(
18
+ () => moduleOptions?.isDevUser === true && isLocalDevHostPage(),
19
+ )
20
+
21
+ const devFriendlyOn = computed(
22
+ () => moduleOptions?.enforceDevFriendlyOrigins !== false,
23
+ )
24
+
25
+ function toggle() {
26
+ if (!vueApp || moduleOptions?.isDevUser !== true) return
27
+ const next = !devFriendlyOn.value
28
+ saveDevFriendlyOriginsPreference(next)
29
+ const store = getAppHubStore(vueApp)
30
+ if (store) {
31
+ store.options.enforceDevFriendlyOrigins = next
32
+ if (!next) {
33
+ store.options.hubOrigin = ''
34
+ }
35
+ }
36
+ reconcileAfterToggle(vueApp, next)
37
+ }
38
+
39
+ return { visible, devFriendlyOn, toggle }
40
+ }
@@ -0,0 +1,16 @@
1
+ import en from './translations/en.js'
2
+ import vi from './translations/vi.js'
3
+ import { resolveLang } from './resolveLang.js'
4
+
5
+ const catalogs = { en, vi }
6
+
7
+ export function t(key, lang = 'vi', params = {}) {
8
+ const code = resolveLang(lang)
9
+ let text = catalogs[code]?.[key] ?? catalogs.en?.[key] ?? String(key)
10
+ if (params && typeof params === 'object') {
11
+ Object.entries(params).forEach(([k, v]) => {
12
+ text = text.replace(new RegExp(`\\{${k}\\}`, 'g'), String(v ?? ''))
13
+ })
14
+ }
15
+ return text
16
+ }
@@ -0,0 +1,6 @@
1
+ /** Normalize language from prop, inject, or ref. */
2
+ export function resolveLang(source, fallback = 'vi') {
3
+ if (typeof source === 'string' && source) return source
4
+ if (source && typeof source.value === 'string' && source.value) return source.value
5
+ return fallback
6
+ }
@@ -0,0 +1,30 @@
1
+ const VALID = new Set(['dark', 'light'])
2
+
3
+ /** Normalize theme from prop, inject, ref, or saved setting. Returns null when unset/auto. */
4
+ export function resolveTheme(source, fallback = null) {
5
+ const read = (value) => {
6
+ if (typeof value !== 'string') return null
7
+ const code = value.trim().toLowerCase()
8
+ if (code === 'auto' || code === '') return null
9
+ return VALID.has(code) ? code : null
10
+ }
11
+
12
+ const direct = read(typeof source === 'string' ? source : null)
13
+ if (direct) return direct
14
+
15
+ if (source && typeof source === 'object' && 'value' in source) {
16
+ const fromRef = read(source.value)
17
+ if (fromRef) return fromRef
18
+ }
19
+
20
+ return read(fallback)
21
+ }
22
+
23
+ /** True when host passes theme via prop or installAppHubModule (not auto). */
24
+ export function isThemeLocked(propTheme, injectTheme) {
25
+ return resolveTheme(propTheme) != null || resolveTheme(injectTheme) != null
26
+ }
27
+
28
+ export function normalizeTheme(value, fallback = 'dark') {
29
+ return resolveTheme(value, fallback) ?? fallback
30
+ }