Skip to content
Open
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
1 change: 1 addition & 0 deletions .agents/06-design-system.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Two shadow-root gotchas the ahead-of-time CSS builder MUST compensate for (both
- **A surface keeping its own component CSS** (inspect, a11y) sources every color from `@antfu/design`'s semantic shortcuts via `--at-apply` (expanded by `transformerDirectives`) rather than hardcoding a palette, so it tracks the shared theme and the `.dark` class.
- **Plain `.ts`/vanilla views** MUST opt `.ts` into UnoCSS extraction (`content.pipeline.include` for Vite, or `content.filesystem` globs for the `@unocss/postcss` setup Next uses), since UnoCSS only scans framework files by default.
- Before making new components or inline DOM elements, always check whether an `@antfu/design` component can be reused.
- **hub-ui text goes through `t()`.** Every user-facing string in `packages/hub-ui/src/client` (templates, tooltips, `aria-label`s, command titles, confirm copy) MUST be a key in `client/i18n/locales/en.json` rendered with `t()` / `tAround()` from `client/i18n`, never a literal. A new key lands in every locale file in the same change; the i18n test enforces the lockstep. Brand names and `DF` diagnostics stay untranslated.

## Storybook

Expand Down
3 changes: 3 additions & 0 deletions docs/content/1.guide/18.hub-initiate.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ interface DevframeHubUi {
- `'normal'` (default): shows immediately.
- `'passive'`: hidden until `Shift+Alt+D`, then persisted per-origin (later browser sessions start shown).
- `'hidden'`: hidden until `Shift+Alt+D`, that browser session only.
- **`locale`** sets the default UI language, one of the tags in `HUB_UI_LOCALES` (`en`, `zh-CN`, `zh-TW`, `ja`, `ko`, `es`, `fr`, `de`, `pt-BR`, `ru`). Without it the UI follows the browser language and falls back to English. A visitor's own pick in Settings → Appearance wins and persists per browser. The hub UI writes the language it resolved back into `ConnectionMeta.configs.ui.locale` on its window, so a same-origin frame that inherits the connection (see [Client](/guide/client)) reads the effective language at boot.

To add a language, add its tag and native name to `packages/hub-ui/src/locales.ts`, translate a copy of `packages/hub-ui/src/client/i18n/locales/en.json` under that tag, and register the file in the `messages` map in `packages/hub-ui/src/client/i18n/index.ts`. The i18n test fails on a missing key or a changed `{slot}`.

## Renderer modules

Expand Down
5 changes: 5 additions & 0 deletions packages/devframe/src/client/connection-storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ export function readStoredAuthToken(userAuthToken?: string): string | undefined
return readFromWindows<string>(CONNECTION_AUTH_TOKEN_KEY)
}

/**
* Publish the connection on this window for same-origin frames to inherit.
* Call again after mutating `connection.connectionMeta` so the legacy meta
* copy below follows.
*/
export function storeConnection(connection: DevframeConnection): void {
;(globalThis as any)[DEVFRAME_CONNECTION_KEY] = connection
// Keep the established metadata/auth globals in sync for viewers that still
Expand Down
1 change: 1 addition & 0 deletions packages/devframe/src/client/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { getDevframeRpcClient } from './rpc'

export * from './connection'
export { storeConnection } from './connection-storage'
export * from './otp'
export * from './rpc'
export type { DevframeServiceClientHandle, DevframeServicesClient } from './rpc-services'
Expand Down
13 changes: 13 additions & 0 deletions packages/hub-ui/.storybook/preview.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import type { Decorator, Preview } from '@storybook/vue3-vite'
import { setLocalePreference } from '../src/client/i18n'
import { HUB_UI_LOCALES } from '../src/locales'
// Reset first so uno utilities win over its resets, matching the production
// `[reset, userStyle, unoCss, ...]` order; otherwise stories lose the reset.
import '@unocss/reset/tailwind.css'
Expand All @@ -18,6 +20,7 @@ function applyTheme(theme: string): void {

const withTheme: Decorator = (story, context) => {
applyTheme(context.globals.theme ?? 'dark')
setLocalePreference(context.globals.locale ?? 'en')
return { components: { story }, template: '<story />' }
}

Expand Down Expand Up @@ -46,6 +49,16 @@ const preview: Preview = {
dynamicTitle: true,
},
},
locale: {
description: 'UI language',
defaultValue: 'en',
toolbar: {
title: 'Language',
icon: 'globe',
items: Object.entries(HUB_UI_LOCALES).map(([value, title]) => ({ value, title })),
dynamicTitle: true,
},
},
},
decorators: [withTheme],
}
Expand Down
2 changes: 2 additions & 0 deletions packages/hub-ui/playground/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useLocalStorage } from '@vueuse/core'
import { watchEffect } from 'vue'
import { isDark } from '../src/client/state/color-mode'
import { DEFAULT_DOCK_PANEL_STORE } from '../src/client/state/docks'
import { setupLocale } from '../src/client/state/locale'

/**
* The base `hub-plugin.ts` mounts the playground's hub instance at. Kept as
Expand Down Expand Up @@ -34,6 +35,7 @@ watchEffect(() => {

async function main(): Promise<void> {
const rpc = await getDevframeRpcClient({ baseURL: HUB_BASE, simpleAuth: false })
setupLocale(rpc)
const { createDocksContext } = await import('../src/client/state/context')

if (mode === 'embedded') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { DocksContext } from '@devframes/hub/client'
import type { PaletteCrumb, PaletteFlatItem } from '../../state/palette'
import Fuse from 'fuse.js'
import { computed, nextTick, ref, useTemplateRef, watch } from 'vue'
import { t, tAround } from '../../i18n'
import { flattenPaletteCommands, paletteActionKeepsOpen, paletteScopeTrail, paletteTrailScopeId, reconcilePaletteTrail, resolvePaletteSelection } from '../../state/palette'
import BrandWordmark from '../icons/BrandWordmark.vue'
import CommandPaletteItem from './CommandPaletteItem.vue'
Expand All @@ -13,6 +14,7 @@ const props = defineProps<{
}>()

const commandsCtx = computed(() => props.context.commands)
const noResults = computed(() => tAround('palette.noResults', 'query'))
const show = computed({
get: () => commandsCtx.value.paletteOpen,
set: (v) => { commandsCtx.value.paletteOpen = v },
Expand Down Expand Up @@ -311,7 +313,7 @@ function getKeybindings(id: string) {
ref="searchInput"
v-model="search"
class="flex-1 bg-transparent py-3 outline-none text-sm color-base"
placeholder="Type a command..."
:placeholder="t('palette.placeholder')"
@keydown="onKeyDown"
>
</header>
Expand All @@ -334,10 +336,10 @@ function getKeybindings(id: string) {
<div v-if="!filtered.length" class="py-8 flex flex-col items-center justify-center gap-2 op50 text-sm">
<div class="i-ph-magnifying-glass-duotone w-6 h-6" />
<div v-if="search">
No results for "<strong class="text-primary op100">{{ search }}</strong>"
{{ noResults[0] }}<strong class="text-primary op100">{{ search }}</strong>{{ noResults[1] }}
</div>
<div v-else>
No commands available
{{ t('palette.empty') }}
</div>
</div>
</div>
Expand All @@ -346,15 +348,15 @@ function getKeybindings(id: string) {
<footer class="border-t border-base flex items-center justify-between gap-4 px-3 py-1.5 text-[10px] op50">
<div class="flex items-center gap-1.5">
<kbd class="px-1 py-0.5 rounded border border-base bg-base font-mono">&darr;&uarr;</kbd>
<span>navigate</span>
<span>{{ t('palette.navigate') }}</span>
</div>
<div class="flex items-center gap-1.5">
<kbd class="px-1 py-0.5 rounded border border-base bg-base font-mono">esc</kbd>
<span>{{ breadcrumb.length > 0 || dynamicItems ? 'back' : 'close' }}</span>
<span>{{ breadcrumb.length > 0 || dynamicItems ? t('palette.back') : t('palette.close') }}</span>
</div>
<div class="flex items-center gap-1.5">
<kbd class="px-1 py-0.5 rounded border border-base bg-base font-mono">&crarr;</kbd>
<span>select</span>
<span>{{ t('palette.select') }}</span>
</div>
</footer>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<script setup lang="ts">
import type { DevframeCommandEntry, DevframeCommandKeybinding } from '@devframes/hub'
import DisplayKbd from '@antfu/design/components/Display/DisplayKbd.vue'
import { t } from '../../i18n'
import DockIcon from '../dock/DockIcon.vue'

defineProps<{
Expand Down Expand Up @@ -44,7 +45,7 @@ defineEmits<{
<span
v-if="entry.source === 'server'"
class="text-[10px] px-1 py-0 rounded bg-blue/10 text-blue shrink-0 leading-4"
>server</span>
>{{ t('palette.server') }}</span>
</div>
<div v-if="selected && entry.description" class="truncate text-xs op40 mt-0.5">
{{ entry.description }}
Expand Down
5 changes: 3 additions & 2 deletions packages/hub-ui/src/client/components/display/Confirm.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<script setup lang="ts">
import ActionButton from '@antfu/design/components/Action/ActionButton.vue'
import { useTemplateRef, watchPostEffect } from 'vue'
import { t } from '../../i18n'
import { ConfirmPromise } from '../../state/confirm'

const confirmButton = useTemplateRef<InstanceType<typeof ActionButton>>('confirmButton')
Expand Down Expand Up @@ -44,10 +45,10 @@ function resolveConfirm(resolve: (value: boolean) => void, value: boolean) {

<div class="flex items-center justify-end gap-2 mt-6">
<ActionButton variant="text" size="sm" @click="resolveConfirm(resolve, false)">
{{ options.cancelText ?? 'Cancel' }}
{{ options.cancelText ?? t('common.cancel') }}
</ActionButton>
<ActionButton ref="confirmButton" variant="primary" size="sm" @click="resolveConfirm(resolve, true)">
{{ options.confirmText ?? 'OK' }}
{{ options.confirmText ?? t('common.ok') }}
</ActionButton>
</div>
</div>
Expand Down
6 changes: 3 additions & 3 deletions packages/hub-ui/src/client/components/display/OtpInput.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<script setup lang="ts">
import type { ComponentPublicInstance } from 'vue'
import { computed, nextTick, ref } from 'vue'
import { t } from '../../i18n'

const props = withDefaults(defineProps<{
/** The current code (controlled). */
Expand All @@ -19,7 +20,6 @@ const props = withDefaults(defineProps<{
disabled: false,
invalid: false,
autofocus: true,
label: 'One-time authorization code',
})

const emit = defineEmits<{
Expand Down Expand Up @@ -160,7 +160,7 @@ defineExpose({
<template>
<div
role="group"
:aria-label="label"
:aria-label="label ?? t('auth.otpGroupLabel')"
class="flex items-center justify-center gap-2 sm:gap-2.5"
:class="invalid ? 'devframes-shake' : ''"
>
Expand All @@ -177,7 +177,7 @@ defineExpose({
data-bwignore="true"
data-form-type="other"
:autofocus="autofocus && i === 0"
:aria-label="`Digit ${i + 1} of ${length}`"
:aria-label="t('auth.otpDigit', { index: i + 1, length })"
:aria-invalid="invalid || undefined"
:disabled="disabled"
maxlength="1"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<script setup lang="ts">
import { locale } from '../../i18n'
import { isDark } from '../../state/color-mode'
</script>

Expand All @@ -9,10 +10,12 @@ import { isDark } from '../../state/color-mode'
fixed/absolute-positioned children (dock anchor, floating layers) are
unaffected while every descendant still matches `.dark …` selectors, inside
the dock's shadow root and the light-DOM standalone page alike. The native
`color-scheme` inherits down for scrollbars and form controls.
`color-scheme` inherits down for scrollbars and form controls, as does
`lang` for hyphenation, fonts and screen readers.
-->
<div
class="devframes-color-root"
:lang="locale"
:class="isDark ? 'dark' : 'light'"
:style="{ display: 'contents', colorScheme: isDark ? 'dark' : 'light' }"
>
Expand Down
3 changes: 2 additions & 1 deletion packages/hub-ui/src/client/components/dock/Dock.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { DockLayout } from './dock-layout'
import { useEventListener, useScreenSafeArea, whenever } from '@vueuse/core'
import { computed, onMounted, reactive, ref, useTemplateRef, watch } from 'vue'
import { BUILTIN_ENTRY_CLIENT_AUTH_NOTICE } from '../../constants'
import { t } from '../../i18n'
import { docksSplitGroupsWithCapacity, getEntryGroup, resolveNextRecentDockId, resolveRecentDockEntry } from '../../state/dock-settings'
import { sharedStateToRef } from '../../state/docks'
import { setDocksOverflowPanel, useDocksGroupPanel } from '../../state/floating-tooltip'
Expand Down Expand Up @@ -325,7 +326,7 @@ onMounted(() => {
:class="context.panel.isVertical ? 'rotate-270' : 'rotate-0'"
/>
<div class="ws-nowrap text-amber">
Unauthorized
{{ t('dock.unauthorized') }}
</div>
</div>
</button>
Expand Down
9 changes: 5 additions & 4 deletions packages/hub-ui/src/client/components/dock/DockContextMenu.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { DevframeDockEntry } from '@devframes/hub'
import type { DocksContext } from '@devframes/hub/client'
import { h } from 'vue'
import { t } from '../../i18n'
import { setDockContextMenu } from '../../state/floating-tooltip'
import { isDockPopupSupported, requestDockPopupOpen, useIsDockPopupOpen } from '../../state/popup'

Expand Down Expand Up @@ -68,19 +69,19 @@ export function openDockContextMenu(options: {
const isEdgeMode = context.panel.store.mode === 'edge'
const items: DockMenuItem[] = [
{
label: 'Hide',
label: t('common.hide'),
icon: 'i-ph-eye-slash-duotone',
action: () => hideDock(context, entry),
visible: canHide(context, entry),
},
{
label: 'Refresh',
label: t('common.refresh'),
icon: 'i-ph-arrow-clockwise-duotone',
action: () => refreshDock(context, entry),
visible: canRefresh(entry),
},
{
label: isEdgeMode ? 'Float Mode' : 'Edge Mode',
label: isEdgeMode ? t('command.floatMode') : t('command.edgeMode'),
icon: isEdgeMode ? 'i-ph-arrows-out-duotone' : 'i-ph-square-half-bottom-duotone',
action: () => {
if (isEdgeMode) {
Expand Down Expand Up @@ -114,7 +115,7 @@ export function openDockContextMenu(options: {
visible: context.clientType === 'embedded',
},
{
label: 'Popup',
label: t('dock.popup'),
icon: 'i-ph-arrow-square-out-duotone',
action: () => {
setDockContextMenu(null)
Expand Down
9 changes: 5 additions & 4 deletions packages/hub-ui/src/client/components/dock/DockEdge.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { DockEdge as DockEdgePosition, DockLayout } from './dock-layout'
import { useEventListener } from '@vueuse/core'
import { computed, h, onMounted, ref, useTemplateRef, watch } from 'vue'
import { BUILTIN_ENTRY_SETTINGS } from '../../constants'
import { t } from '../../i18n'
import { docksSplitGroupsBySize, getEntryGroup } from '../../state/dock-settings'
import { setEdgePositionDropdown, setFloatingTooltip, useDocksGroupPanel, useDocksOverflowPanel, useEdgePositionDropdown } from '../../state/floating-tooltip'
import { useSettings } from '../../state/settings-defaults'
Expand Down Expand Up @@ -512,9 +513,9 @@ const dragPreviewStyle = computed<CSSProperties | undefined>(() => {
>
<button
ref="positionButton"
aria-label="Edge position"
:aria-label="t('dock.edgePosition')"
class="p1.5 rounded hover:bg-active transition op75 hover:op100"
@pointerenter="showTooltip(positionButton, 'Edge position')"
@pointerenter="showTooltip(positionButton, t('dock.edgePosition'))"
@pointerleave="hideTooltip"
@pointerdown="onPositionPointerDown"
@click="togglePositionDropdown"
Expand All @@ -523,9 +524,9 @@ const dragPreviewStyle = computed<CSSProperties | undefined>(() => {
</button>
<button
ref="floatButton"
aria-label="Float mode"
:aria-label="t('dock.floatMode')"
class="p1.5 rounded hover:bg-active transition op50 hover:op100"
@pointerenter="showTooltip(floatButton, 'Float mode')"
@pointerenter="showTooltip(floatButton, t('dock.floatMode'))"
@pointerleave="hideTooltip"
@pointerdown="hideTooltip"
@click="switchToFloat"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import type { DevframeDockEntriesGrouped, DevframeDockEntry, DevframeViewGroup } from '@devframes/hub'
import type { DocksContext } from '@devframes/hub/client'
import { computed } from 'vue'
import { t } from '../../i18n'
import { accentVarStyle } from '../../utils/accent-color'
import DockIcon from './DockIcon.vue'

Expand Down Expand Up @@ -55,7 +56,7 @@ const isEmpty = computed(() => props.members.every(([, items]) => items.length =
</button>
</template>
<div v-if="isEmpty" class="px2 py1.5 op50 text-sm italic">
No tools yet
{{ t('dock.noTools') }}
</div>
</div>
</template>
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { DevframeDockEntry, DevframeViewGroup } from '@devframes/hub'
import type { DocksContext } from '@devframes/hub/client'
import { watchDebounced } from '@vueuse/core'
import { computed, h, ref, useTemplateRef, watch } from 'vue'
import { t } from '../../i18n'
import { deriveSidebarCapacity, docksSplitGroupsWithCapacity, getGroupMembersGrouped } from '../../state/dock-settings'
import { setDocksSidebarOverflowPanel, setFloatingTooltip, useDocksSidebarOverflowPanel } from '../../state/floating-tooltip'
import { useSettings } from '../../state/settings-defaults'
Expand Down Expand Up @@ -199,10 +200,10 @@ const moreButtonActive = computed(() => selectedInOverflow.value || isOverflowPa
<button
v-if="hasOverflow"
ref="moreButton"
aria-label="Show more"
:aria-label="t('dock.showMore')"
class="relative flex items-center justify-center w-8 h-8 rounded-lg transition mt-auto flex-none"
:class="moreButtonActive ? 'text-primary bg-active' : 'op60 hover:op100 hover:bg-active'"
@pointerenter="showTooltip($event, 'Show more')"
@pointerenter="showTooltip($event, t('dock.showMore'))"
@pointerleave="hideTooltip"
@pointerdown="hideTooltip"
@click="toggleOverflowPanel"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { DevframeDockEntriesGrouped } from '../../state/dock-settings'
import type { FloatingPopoverProps } from '../../state/floating-tooltip'
import { watchDebounced } from '@vueuse/core'
import { computed, h, onBeforeUnmount, ref, useTemplateRef } from 'vue'
import { t } from '../../i18n'
import { setDocksOverflowPanel, useDocksOverflowPanel } from '../../state/floating-tooltip'
import DockEntriesWithCategories from './DockEntriesWithCategories.vue'
import DockEntry from './DockEntry.vue'
Expand Down Expand Up @@ -100,7 +101,7 @@ onBeforeUnmount(() => {
:context="context"
:dock="{
id: 'overflow',
title: 'Overflow',
title: t('dock.overflow'),
icon: 'ph:dots-three-circle-duotone',
}"
class="overflow-button"
Expand Down
Loading
Loading