From a36080742287bad8f657f688d7fec0022dd696a1 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Mon, 7 Sep 2026 00:48:55 +0200 Subject: feat(client): HelloWorld, the reference application MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every other test of the plugin architecture reads source for the *absence* of app names. That proves nobody wrote a special case for Videos; it cannot prove a genuinely new application works, because there was no new application. This is one. It stores directories, appears as a tab, has a settings pane and lists files, and the node has never heard its name outside a single allow-list entry. Two files and one registry line, which is the claim `docs/refactor-groups.md` §4.1 makes. It ships hidden behind `?dev=1` (`dev: true` in the registry, the same opt-in shape as transport.js's `?trace=1`). Registering it normally would put a toy app in every operator's group; not registering it would prove nothing, since registration is exactly what is claimed to be sufficient. **Adding it found two places where the claim was nearly true rather than true, and both are fixed by making the code less app-specific:** `group-settings.js` fell back to the whole registry when a group had no `enabled_apps` yet — which would have turned a hidden app on for everyone. It asks `availableApps()` now. `group-page.js` wrote out `videoDirectories` / `musicDirectories` / `photoDirectories` by hand, so a fifth app would have needed that file edited. It derives `Directories` from the registry. Neither was found by reading; both were found by adding the app, which is the whole reason it exists. Verified in a real Electron window as well as by the tests: hidden by default, present with the flag, offered its own settings section, and listing exactly the files under its configured folder and its subfolders — not the ones beside it. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_011pvMdvLBG92jyhvD5pD6us --- .../meshbay-hub/src/meshbay_hub/static/apps.js | 47 ++++++++++++++-- .../src/meshbay_hub/static/group-page.js | 24 ++++++--- .../src/meshbay_hub/static/group-settings.js | 9 +++- .../meshbay_hub/static/helloworld-app-settings.js | 50 +++++++++++++++++ .../src/meshbay_hub/static/helloworld-app.js | 62 ++++++++++++++++++++++ .../src/meshbay_hub/static/locales/de.js | 6 +++ .../src/meshbay_hub/static/locales/en.js | 6 +++ .../src/meshbay_hub/static/locales/es.js | 6 +++ .../src/meshbay_hub/static/locales/fr.js | 6 +++ .../src/meshbay_hub/static/locales/it.js | 6 +++ .../src/meshbay_hub/static/locales/ja.js | 6 +++ .../src/meshbay_hub/static/locales/nl.js | 6 +++ .../src/meshbay_hub/static/locales/pl.js | 6 +++ .../src/meshbay_hub/static/locales/pt-BR.js | 6 +++ .../src/meshbay_hub/static/locales/zh-CN.js | 6 +++ .../meshbay-hub/src/meshbay_hub/static/style.css | 12 +++++ 16 files changed, 250 insertions(+), 14 deletions(-) create mode 100644 packages/meshbay-hub/src/meshbay_hub/static/helloworld-app-settings.js create mode 100644 packages/meshbay-hub/src/meshbay_hub/static/helloworld-app.js (limited to 'packages/meshbay-hub/src/meshbay_hub/static') diff --git a/packages/meshbay-hub/src/meshbay_hub/static/apps.js b/packages/meshbay-hub/src/meshbay_hub/static/apps.js index 6e62a42..461bd57 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/apps.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/apps.js @@ -7,6 +7,33 @@ import { ChatSettings } from './chat-app-settings.js'; import { VideoSettings } from './video-app-settings.js'; import { MusicSettings } from './music-app-settings.js'; import { PhotoSettings } from './photos-app-settings.js'; +import { HelloWorldApp } from './helloworld-app.js'; +import { HelloWorldSettings } from './helloworld-app-settings.js'; + +/** + * Whether apps marked `dev` are shown. Opt in once with `?dev=1`, off with + * `?dev=0` — persisted, the same shape as transport.js's `?trace=1`. + * + * HelloWorld exists to prove that adding an application is a registry entry + * and two files. Registering it normally would put a toy app in every + * operator's group; leaving it unregistered would prove nothing, since the + * claim is precisely that registration is enough. So it is registered, and + * hidden behind a flag that changes nothing else. + */ +const DEV_KEY = 'mb_dev_apps'; +(function _initDevFlag() { + try { + const params = new URLSearchParams(location.search); + if (params.has('dev')) { + if (params.get('dev') === '0') localStorage.removeItem(DEV_KEY); + else localStorage.setItem(DEV_KEY, '1'); + } + } catch { /* localStorage unavailable — dev apps stay hidden */ } +})(); + +function devAppsShown() { + try { return localStorage.getItem(DEV_KEY) === '1'; } catch { return false; } +} /** * Every group "application", in tab order. @@ -43,18 +70,30 @@ const APPS = [ Component: MusicApp, Settings: MusicSettings }, { key: 'photo', icon: 'image', labelKey: 'group.tab_photos', Component: PhotosApp, Settings: PhotoSettings }, + // The reference implementation (docs/refactor-groups.md §4.1). `dev` keeps + // it out of an operator's way; everything else about it is an ordinary + // entry, which is the point. + { key: 'helloworld', icon: 'chat', labelKey: 'group.tab_helloworld', + Component: HelloWorldApp, Settings: HelloWorldSettings, dev: true }, ]; +/** Everything a reader of this page is allowed to see, in registry order. */ +function availableApps() { + const dev = devAppsShown(); + return APPS.filter(a => dev || !a.dev); +} + /** The registry filtered to what this group has enabled, in registry order. */ function visibleApps(enabledKeys) { + const registered = availableApps(); const enabled = new Set( - enabledKeys && enabledKeys.length ? enabledKeys : APPS.map(a => a.key)); - return APPS.filter(a => a.alwaysEnabled || enabled.has(a.key)); + enabledKeys && enabledKeys.length ? enabledKeys : registered.map(a => a.key)); + return registered.filter(a => a.alwaysEnabled || enabled.has(a.key)); } /** The apps the Settings page offers a section for, in registry order. */ function configurableApps() { - return APPS.filter(a => !a.alwaysEnabled && a.Settings); + return availableApps().filter(a => !a.alwaysEnabled && a.Settings); } -export { APPS, visibleApps, configurableApps }; +export { APPS, availableApps, visibleApps, configurableApps }; diff --git a/packages/meshbay-hub/src/meshbay_hub/static/group-page.js b/packages/meshbay-hub/src/meshbay_hub/static/group-page.js index 0a43724..6f41426 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/group-page.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/group-page.js @@ -9,7 +9,7 @@ import { HUB, session, cacheGroupIndex, hubFetch, ensureFreshToken, _loadBundleKey, _loadRecoveryKey, _storeBundleKey, } from './hub-client.js'; -import { visibleApps } from './apps.js'; +import { APPS, visibleApps } from './apps.js'; import { GroupName } from './group-name.js'; import { FilePreview } from './files-app.js'; import { VideoPlayer } from './video-player.js'; @@ -654,10 +654,19 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, // `group-settings.js` untouched — that page renders the panes without // knowing what any of them is for, which is what makes adding an app a // registry entry rather than an edit to the page. + // `Directories` for every registered app, derived from the registry + // rather than written out. Naming them here would mean adding an app + // required editing this file, which is the one thing the plugin + // architecture is supposed to have removed — and the reference app + // (docs/refactor-groups.md §4.1) is what made the difference visible. + const perAppDirectories = useMemo(() => { + const out = {}; + for (const app of APPS) out[`${app.key}Directories`] = appDirs(app.key); + return out; + }, [appDirs]); + const appSettings = useMemo(() => ({ - videoDirectories: appDirs('video'), - musicDirectories: appDirs('music'), - photoDirectories: appDirs('photo'), + ...perAppDirectories, chatDirectory, chatLinkPreview, tmdbEnabled: tmdbConfig ? tmdbConfig.enabled !== false : true, @@ -665,7 +674,8 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, tmdbTokenCustomized: Boolean(tmdbConfig && tmdbConfig.tokenCustomized), musicbrainzEnabled: musicbrainzConfig ? musicbrainzConfig.enabled !== false : true, - }), [appDirs, chatDirectory, chatLinkPreview, tmdbConfig, musicbrainzConfig]); + }), [perAppDirectories, chatDirectory, chatLinkPreview, tmdbConfig, + musicbrainzConfig]); const commonProps = { groupId, transportRef, gekRef, status, username, @@ -676,9 +686,7 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, // Plural everywhere: Videos and Music read a list now, and Photos always // did. The scalar `videoRoot`/`audioRoot` shapes survive only on the wire, // for a node that speaks MNP 1.0 — nothing in the client carries them. - videoDirectories: appDirs('video'), - musicDirectories: appDirs('music'), - photoDirectories: appDirs('photo'), + ...perAppDirectories, tmdbConfig, musicbrainzConfig, onPlayQueue, }; diff --git a/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js b/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js index 6c6f3d9..c80cff5 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js @@ -5,7 +5,7 @@ import { t } from './i18n.js'; import { Icon } from './icon.js'; import { CollapsibleSection, ToggleSwitch } from './settings-ui.js'; import { hubFetch, navigate } from './hub-client.js'; -import { APPS, configurableApps } from './apps.js'; +import { availableApps, configurableApps } from './apps.js'; import * as platform from './platform.js'; @@ -559,7 +559,12 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef, const [appsBusy, setAppsBusy] = useState(false); const [appsMsg, setAppsMsg] = useState(''); - const activeApps = enabledApps && enabledApps.length ? enabledApps : APPS.map(a => a.key); + // `availableApps()` rather than the raw registry: an app the reader is not + // shown must not be turned on for the whole group by falling back to "all of + // them". Found by adding one that is hidden by default — an ordinary app + // would never have exposed the difference. + const activeApps = enabledApps && enabledApps.length + ? enabledApps : availableApps().map(a => a.key); /** * Toggle one app in or out of the group's enabled set. Same shape as diff --git a/packages/meshbay-hub/src/meshbay_hub/static/helloworld-app-settings.js b/packages/meshbay-hub/src/meshbay_hub/static/helloworld-app-settings.js new file mode 100644 index 0000000..9058199 --- /dev/null +++ b/packages/meshbay-hub/src/meshbay_hub/static/helloworld-app-settings.js @@ -0,0 +1,50 @@ +import { html, useState, useEffect } from './vendor/htm-preact.js'; +import { t } from './i18n.js'; +import { useSaver } from './settings-ui.js'; +import { FolderPickerField } from './folder-tree.js'; + +/** + * HelloWorld's settings: one folder, and nothing else. + * + * This is the whole server-side surface an application needs — no op, no MNP + * message, no route, no roster accessor. `saveDirectories` is bound to this + * app's registry key by the page, and `ops.set_app_directories` stores the row + * under that name without knowing what it is. + * + * It takes the shared props and no others (`docs/apps.md` §3b), which is what + * `test_app_settings_plugin.py` checks of every pane — including this one, so + * the reference implementation is held to the contract it demonstrates. + */ +function HelloWorldSettings({ roots, dirs, settings, saveDirectories }) { + const { busy, msg, run } = useSaver(); + const [directories, setDirectories] = useState( + settings.helloworldDirectories || []); + + useEffect(() => { + setDirectories(settings.helloworldDirectories || []); + }, [settings.helloworldDirectories]); + + const current = settings.helloworldDirectories || []; + const dirty = directories.length !== current.length + || directories.some((d, i) => d !== current[i]); + + return html` +
+ <${FolderPickerField} + label=${t('settings_app.helloworld_directory_label')} + hint=${t('settings_app.helloworld_directory_hint')} + roots=${roots} dirs=${dirs} mode="single" + value=${directories[0] || ''} disabled=${busy} + onChange=${(path) => setDirectories(path ? [path] : [])} /> + + + ${msg && html`

${msg}

`} +
+ `; +} + +export { HelloWorldSettings }; diff --git a/packages/meshbay-hub/src/meshbay_hub/static/helloworld-app.js b/packages/meshbay-hub/src/meshbay_hub/static/helloworld-app.js new file mode 100644 index 0000000..01b4ed4 --- /dev/null +++ b/packages/meshbay-hub/src/meshbay_hub/static/helloworld-app.js @@ -0,0 +1,62 @@ +import { html, useMemo } from './vendor/htm-preact.js'; +import { t } from './i18n.js'; +import { Icon } from './icon.js'; +import { formatSize } from './file-utils.js'; + +/** + * The smallest application this platform can host, and the proof that adding + * one costs nothing outside its own two files. + * + * Everything else in `docs/refactor-groups.md` §3 is asserted by tests that + * read source. This is the other kind of evidence: an app nobody wrote a line + * of plumbing for, that stores directories, appears as a tab, and lists files — + * because the registry entry beside it is genuinely all there is. + * + * **Not shipped to operators.** It is registered with `dev: true`, which keeps + * it out of the tab bar and the settings page unless the reader opts in with + * `?dev=1` (same shape as transport.js's `?trace=1`). Deleting the flag would + * put a toy app in everybody's group; deleting the app would leave the claim + * resting entirely on tests that read text. + * + * It takes the standard props — see `docs/apps.md` §2 — and reads + * `helloworldDirectories`, which nothing on the node knows about by name: + * `ops.set_app_directories` keys the row by whatever the app is called. + */ +function HelloWorldApp({ entries, availableEntries, helloworldDirectories, status }) { + const dirs = helloworldDirectories || []; + const pool = availableEntries || entries || []; + + const files = useMemo(() => pool.filter((e) => { + const p = e.path || ''; + return dirs.some((d) => p === d || p.startsWith(d + '/')); + }).slice(0, 200), [pool, dirs]); + + if (status !== 'connected') { + return html`

${t('status.connecting_short')}

`; + } + + return html` +
+

${t('helloworld.greeting')}

+ ${dirs.length === 0 ? html` +

${t('helloworld.no_directory')}

+ ` : html` +

+ ${t('helloworld.counted', { n: files.length })} + ${' — '}${dirs.join(', ')} +

+
    + ${files.map((e) => html` +
  • + <${Icon} name="folder" /> + ${e.name} + ${formatSize(e.size || 0)} +
  • + `)} +
+ `} +
+ `; +} + +export { HelloWorldApp }; diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js index c6d0331..d05d3a3 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js @@ -126,6 +126,12 @@ export default { 'group.tab_video': 'Videos', 'group.tab_music': 'Musik', 'group.tab_photos': 'Fotos', + 'group.tab_helloworld': 'HelloWorld', + 'helloworld.greeting': 'Hallo, Welt!', + 'helloworld.no_directory': 'Noch kein Ordner gewählt. Wählen Sie einen in den Einstellungen.', + 'helloworld.counted': '{n} Datei(en)', + 'settings_app.helloworld_directory_label': 'Ordner', + 'settings_app.helloworld_directory_hint': 'Der Ordner, den diese Referenz-App auflistet. Sie zeigt, dass eine neue Anwendung nichts außer ihren beiden Dateien braucht.', 'group.tab_members': 'Mitglieder', 'group.tab_settings': "Einstellungen", 'members.danger_leave_hint': "Sie verlieren den Zugriff auf die Dateien und den Chat dieser Gruppe.", diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js index 0831aa3..6dbb70f 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js @@ -127,6 +127,12 @@ export default { 'group.tab_video': 'Videos', 'group.tab_music': 'Music', 'group.tab_photos': 'Photos', + 'group.tab_helloworld': 'HelloWorld', + 'helloworld.greeting': 'Hello, World!', + 'helloworld.no_directory': 'No folder chosen yet. Pick one in Settings.', + 'helloworld.counted': '{n} file(s)', + 'settings_app.helloworld_directory_label': 'Folder', + 'settings_app.helloworld_directory_hint': 'The folder this reference app lists. It exists to show that adding an application needs nothing beyond its own two files.', 'group.tab_members': 'Members', 'group.tab_settings': "Settings", 'members.danger_leave_hint': "You will lose access to this group's files and chat.", diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js index 18570cc..aa950cc 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js @@ -124,6 +124,12 @@ export default { 'group.tab_video': 'Vídeos', 'group.tab_music': 'Música', 'group.tab_photos': 'Fotos', + 'group.tab_helloworld': 'HelloWorld', + 'helloworld.greeting': '¡Hola, mundo!', + 'helloworld.no_directory': 'Aún no hay carpeta elegida. Elige una en los ajustes.', + 'helloworld.counted': '{n} archivo(s)', + 'settings_app.helloworld_directory_label': 'Carpeta', + 'settings_app.helloworld_directory_hint': 'La carpeta que lista esta aplicación de referencia. Existe para mostrar que añadir una aplicación no requiere más que sus dos archivos.', 'group.tab_members': 'Miembros', 'group.tab_settings': "Ajustes", 'members.danger_leave_hint': "Perderá el acceso a los archivos y al chat de este grupo.", diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js index 65021e3..daa9f94 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js @@ -125,6 +125,12 @@ export default { 'group.tab_video': 'Vidéos', 'group.tab_music': 'Musique', 'group.tab_photos': 'Photos', + 'group.tab_helloworld': 'HelloWorld', + 'helloworld.greeting': 'Bonjour, monde !', + 'helloworld.no_directory': 'Aucun dossier choisi. Choisissez-en un dans les réglages.', + 'helloworld.counted': '{n} fichier(s)', + 'settings_app.helloworld_directory_label': 'Dossier', + 'settings_app.helloworld_directory_hint': 'Le dossier que cette application de référence liste. Elle existe pour montrer qu\'ajouter une application ne demande rien de plus que ses deux fichiers.', 'group.tab_members': 'Membres', 'group.tab_settings': "Paramètres", 'members.danger_leave_hint': "Vous perdrez l’accès aux fichiers et à la discussion de ce groupe.", diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js index 7f58764..364c26c 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js @@ -125,6 +125,12 @@ export default { 'group.tab_video': 'Video', 'group.tab_music': 'Musica', 'group.tab_photos': 'Foto', + 'group.tab_helloworld': 'HelloWorld', + 'helloworld.greeting': 'Ciao, mondo!', + 'helloworld.no_directory': 'Nessuna cartella scelta. Scegline una nelle impostazioni.', + 'helloworld.counted': '{n} file', + 'settings_app.helloworld_directory_label': 'Cartella', + 'settings_app.helloworld_directory_hint': 'La cartella elencata da questa applicazione di riferimento. Serve a mostrare che aggiungerne una non richiede altro che i suoi due file.', 'group.tab_members': 'Membri', 'group.tab_settings': "Impostazioni", 'members.danger_leave_hint': "Perderai l’accesso ai file e alla chat di questo gruppo.", diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js index 0e2dd42..f778fc4 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js @@ -123,6 +123,12 @@ export default { 'group.tab_video': '動画', 'group.tab_music': '音楽', 'group.tab_photos': '写真', + 'group.tab_helloworld': 'HelloWorld', + 'helloworld.greeting': 'ハロー、ワールド!', + 'helloworld.no_directory': 'フォルダーが未選択です。設定で選んでください。', + 'helloworld.counted': '{n} 件のファイル', + 'settings_app.helloworld_directory_label': 'フォルダー', + 'settings_app.helloworld_directory_hint': 'この参照アプリが一覧表示するフォルダーです。アプリの追加に必要なのは自身の 2 つのファイルだけであることを示すために存在します。', 'group.tab_members': 'メンバー', 'group.tab_settings': "設定", 'members.danger_leave_hint': "このグループのファイルとチャットにアクセスできなくなります。", diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js index 9f74e19..6e6842e 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js @@ -126,6 +126,12 @@ export default { 'group.tab_video': "Video's", 'group.tab_music': 'Muziek', 'group.tab_photos': "Foto's", + 'group.tab_helloworld': 'HelloWorld', + 'helloworld.greeting': 'Hallo, wereld!', + 'helloworld.no_directory': 'Nog geen map gekozen. Kies er een bij Instellingen.', + 'helloworld.counted': '{n} bestand(en)', + 'settings_app.helloworld_directory_label': 'Map', + 'settings_app.helloworld_directory_hint': 'De map die deze referentie-app toont. Hij bestaat om te laten zien dat een app toevoegen niets meer vergt dan zijn eigen twee bestanden.', 'group.tab_members': 'Leden', 'group.tab_settings': "Instellingen", 'members.danger_leave_hint': "U verliest de toegang tot de bestanden en de chat van deze groep.", diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js index 083d72d..c8cbd16 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js @@ -130,6 +130,12 @@ export default { 'group.tab_video': 'Wideo', 'group.tab_music': 'Muzyka', 'group.tab_photos': 'Zdjęcia', + 'group.tab_helloworld': 'HelloWorld', + 'helloworld.greeting': 'Witaj, świecie!', + 'helloworld.no_directory': 'Nie wybrano folderu. Wybierz go w ustawieniach.', + 'helloworld.counted': 'Plików: {n}', + 'settings_app.helloworld_directory_label': 'Folder', + 'settings_app.helloworld_directory_hint': 'Folder wypisywany przez tę aplikację referencyjną. Istnieje, aby pokazać, że dodanie aplikacji nie wymaga niczego poza jej dwoma plikami.', 'group.tab_members': 'Członkowie', 'group.tab_settings': "Ustawienia", 'members.danger_leave_hint': "Utracisz dostęp do plików i czatu tej grupy.", diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js index bdc6c3b..6eb2242 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js @@ -126,6 +126,12 @@ export default { 'group.tab_video': 'Vídeos', 'group.tab_music': 'Música', 'group.tab_photos': 'Fotos', + 'group.tab_helloworld': 'HelloWorld', + 'helloworld.greeting': 'Olá, mundo!', + 'helloworld.no_directory': 'Nenhuma pasta escolhida. Escolha uma nas configurações.', + 'helloworld.counted': '{n} arquivo(s)', + 'settings_app.helloworld_directory_label': 'Pasta', + 'settings_app.helloworld_directory_hint': 'A pasta que este aplicativo de referência lista. Existe para mostrar que adicionar um aplicativo não exige nada além de seus dois arquivos.', 'group.tab_members': 'Membros', 'group.tab_settings': "Configurações", 'members.danger_leave_hint': "Você perderá o acesso aos arquivos e ao chat deste grupo.", diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js index 0a1fe1a..044611e 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js @@ -123,6 +123,12 @@ export default { 'group.tab_video': '视频', 'group.tab_music': '音乐', 'group.tab_photos': '照片', + 'group.tab_helloworld': 'HelloWorld', + 'helloworld.greeting': '你好,世界!', + 'helloworld.no_directory': '尚未选择文件夹。请在设置中选择。', + 'helloworld.counted': '{n} 个文件', + 'settings_app.helloworld_directory_label': '文件夹', + 'settings_app.helloworld_directory_hint': '该参考应用所列出的文件夹。它的存在是为了说明:新增一个应用只需要它自己的两个文件。', 'group.tab_members': '成员', 'group.tab_settings': "设置", 'members.danger_leave_hint': "您将无法再访问该群组的文件和聊天。", diff --git a/packages/meshbay-hub/src/meshbay_hub/static/style.css b/packages/meshbay-hub/src/meshbay_hub/static/style.css index c5d5ce3..9c0d869 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/style.css +++ b/packages/meshbay-hub/src/meshbay_hub/static/style.css @@ -4176,3 +4176,15 @@ h2 .gn-owner, h3 .gn-owner { font-size: 0.55em; } .folder-field-tbl { margin-bottom: 8px; } .folder-field-tbl .sdt-col-dir { font-size: 0.9em; } .folder-field-actions { display: flex; align-items: center; gap: 10px; } + +/* HelloWorld (docs/refactor-groups.md §4.1) — the reference app, hidden + behind ?dev=1. Deliberately plain: it exists to prove the plumbing, and + anything decorative here would be a second thing to keep working. */ +.hw-list { list-style: none; margin: 12px 0 0; padding: 0; } +.hw-item { + display: flex; align-items: center; gap: 8px; + padding: 6px 0; border-bottom: 1px solid var(--border); +} +.hw-item .icon { width: 15px; height: 15px; flex-shrink: 0; } +.hw-name { flex: 1 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.hw-size { flex: 0 0 auto; color: var(--text-dim); font-size: 0.85em; } -- cgit v1.2.3