Compare commits

...
Author SHA1 Message Date
root-at-skic b54304fef8 Merge pull request '0.10.5: icon-only Sync button that keeps its size and colour' (#36) from fix/sync-button-icon into main 2026-11-08 07:59:32 -08:00
root-at-skic 8407c9a956 0.10.5: icon-only Sync button that keeps its size and colour
On a diagram page it goes last in Archify's toolbar, after Export, instead
of floating over it. Every rule is !important and flex: none, so the page's
button styles, narrow windows and high contrast themes no longer squeeze it
or strip its colour.
2026-11-08 17:45:17 +02:00
root-at-skic c2d33780f9 Merge pull request '0.10.4: a real Sync button on every Archify page' (#35) from feat/sync-page-button into main 2026-11-07 07:07:15 -08:00
root-at-skic 90c7be06de 0.10.4: a real Sync button on every Archify page
A labelled Sync button sits top right on every Archify page, also in Source, and opens the Sync menu. On a shared model it turns green and reads Shared. The title-bar icon and the pre-share status bar entry are gone, with their settings.
2026-11-07 01:25:34 +02:00
root-at-skic 7d2d55543e Merge pull request '0.10.3: Sync button in the editor title bar' (#34) from feat/sync-title-button into main 2026-11-06 14:56:31 -08:00
10 changed files with 108 additions and 58 deletions

No files matched your search

+9
View File
@@ -1,5 +1,14 @@
# Changelog
## 0.10.5 — 2026-11-08
- **Sync button fixed.** It is now an icon only (a cloud; green with a broadcast icon once the model is shared), the same size on every page and theme. On a diagram it sits in the toolbar after **Export** instead of over it; in narrow windows and high contrast themes it no longer shrinks or loses its colour.
## 0.10.4 — 2026-11-07
- **A real Sync button.** Every Archify page (diagram, model, graph, registry, decision, and in Source) shows a **Sync** button top right. It opens the Sync menu: sign in, share the model, join one, set the server address. On a model that is shared it turns green and reads **Shared**.
- Removed the small title-bar icon and the status bar entry shown before anything was shared, with their settings `archify.sync.titleButton` and `archify.sync.statusBar`. A shared model still shows its live state in the status bar.
## 0.10.3 — 2026-11-07
- **Sync button in the title bar.** Every Archify page (and `.archify` text editors) has a **Sync** button top right, next to *Open in Text Editor*: sign in, share a model, join one, set the server address. The status bar entry stays for those who show the status bar. `archify.sync.titleButton: false` hides the button.
+1 -1
View File
@@ -140,7 +140,7 @@ Optional. Without a sync server the extension works exactly as described above a
[Archify Sync](https://gitea.lego-cloud.eu/vscode-extensions/archify-vscode-ext-sync) is a small server you run yourself (one container and PostgreSQL; a Helm chart is included). With it, a model is edited by several people at once, like a shared document, and its files stay ordinary files on each person's disk, so Git, agents and every page of this extension keep working.
- **Where.** The **Sync** button in the title bar of every Archify page (top right, next to *Open in Text Editor*) opens a menu: sign in, share a model, join one, the server address, settings. The same menu is **Archify Sync** in the status bar (in workspaces with Archify files) and *Archify | Sync…* in the Command Palette. `archify.sync.titleButton: false` and `archify.sync.statusBar: false` hide them.
- **Where.** Every Archify page (diagram, model, graph, registry, decision, also in Source) has a Sync button (a cloud) top right; on a diagram it sits after **Export**. It opens the Sync menu: sign in, share the model, join one, the server address, settings. On a shared model it turns green with a broadcast icon. The same menu is *Archify | Sync…* in the Command Palette.
- **Server address.** Set `archify.sync.server` once (Settings › Archify › Sync, or *Archify | Set Sync Server Address…*), for example `https://archify.example.com`. Put it in the workspace settings and everyone in the project uses the same server. Left empty, the address is asked for once and remembered.
- **Sign in.** *Archify | Sign in to Sync Server…* uses that address (or asks for it) and signs in through the browser; the session is kept in VS Code's secret storage and shows under **Accounts** as *Archify Sync*. A new server is set up first on its `/setup` page with the code from its log.
- **Share.** *Archify | Share Model…* (also on `main.archify` in the Explorer) puts the model on the server and writes a small `.archify-sync` file next to `main.archify`: the server and the model's id, no secrets. Commit it, and everyone who clones the repository and is a member opens the shared model.
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "archify-vscode-ext",
"version": "0.10.3",
"version": "0.10.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "archify-vscode-ext",
"version": "0.10.3",
"version": "0.10.5",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.20.4",
+2 -17
View File
@@ -2,7 +2,7 @@
"name": "archify-vscode-ext",
"displayName": "Archify Diagram Viewer",
"description": "Archify diagrams in VS Code. Write diagrams as .archify files in YAML: they open as the interactive diagram, with live updates, validation, templates and completion. Also previews Archify's own JSON files (*.architecture.json, …) and opens rendered Archify HTML.",
"version": "0.10.3",
"version": "0.10.5",
"publisher": "root-at-skic",
"license": "MIT",
"sponsor": {
@@ -270,11 +270,6 @@
"command": "archify.openModelGraph",
"when": "archify.inModel && (archify.isDiagramSource || activeCustomEditorId == 'archify.diagram' || activeCustomEditorId == 'archify.registry' || activeCustomEditorId == 'archify.decision') && !notebookEditorFocused",
"group": "navigation@61"
},
{
"command": "archify.sync.menu",
"when": "config.archify.sync.titleButton && (archify.isDiagramSource || activeCustomEditorId == 'archify.diagram' || activeCustomEditorId == 'archify.registry' || activeCustomEditorId == 'archify.decision') && !notebookEditorFocused",
"group": "navigation@62"
}
],
"explorer/context": [
@@ -526,17 +521,7 @@
"scope": "window",
"pattern": "^$|^https?://",
"patternErrorMessage": "An address such as https://archify.example.com",
"markdownDescription": "Address of your Archify Sync server, for example `https://archify.example.com`. *Sign in*, *Share Model* and the status bar use it without asking. Set it in the workspace settings so everyone in the project uses the same server. Empty: asked once and remembered. Archify Sync is optional; see the README."
},
"archify.sync.statusBar": {
"type": "boolean",
"default": true,
"markdownDescription": "Show **Archify Sync** in the status bar in workspaces with Archify files. Clicking it offers sign in, share, join and the server address. Shared models always show their state there."
},
"archify.sync.titleButton": {
"type": "boolean",
"default": true,
"markdownDescription": "Show the **Archify Sync** button in the title bar of Archify pages and `.archify` editors: sign in, share, join, server address. Archify Sync is optional; without a server the button only offers to set one up."
"markdownDescription": "Address of your Archify Sync server, for example `https://archify.example.com`. The **Sync** button on Archify pages, *Sign in* and *Share Model* use it without asking. Set it in the workspace settings so everyone in the project uses the same server. Empty: asked once and remembered. Archify Sync is optional; see the README."
}
}
},
+1 -15
View File
@@ -41,8 +41,6 @@ export class SyncFeature implements vscode.Disposable, LiveProvider {
private cursorTimer: ReturnType<typeof setTimeout> | undefined;
/** Servers with a session on this machine, for the status bar before any model is shared. */
private signedIn: string[] = [];
/** Whether the workspace has Archify files; elsewhere the entry stays hidden until a model is shared. */
private archifyHere = false;
constructor(
private readonly context: vscode.ExtensionContext,
@@ -80,7 +78,6 @@ export class SyncFeature implements vscode.Disposable, LiveProvider {
async start(): Promise<void> {
const found = await vscode.workspace.findFiles(`**/${LINK_FILE}`, SKIP, 50);
await Promise.all(found.map((uri) => this.open(uri, 'open')));
this.archifyHere = (await vscode.workspace.findFiles('**/*.{archify,archify-registry,archify-decision}', SKIP, 1)).length > 0;
await this.refreshSignedIn();
}
@@ -139,19 +136,8 @@ export class SyncFeature implements vscode.Disposable, LiveProvider {
private render(): void {
const spaces = [...this.spaces.values()];
if (!spaces.length) {
this.status.hide();
void vscode.commands.executeCommand('setContext', 'archify.sync.active', false);
// Nothing shared here yet: the way in, as before the sync bundle loaded.
if (!this.archifyHere || !vscode.workspace.getConfiguration('archify.sync').get<boolean>('statusBar', true)) {
this.status.hide();
return;
}
const hosts = this.signedIn.map((server) => new URL(server).host);
this.status.text = hosts.length ? `$(cloud) Archify Sync · ${hosts.join(', ')}` : '$(cloud) Archify Sync';
this.status.tooltip = hosts.length
? `Signed in to ${hosts.join(', ')}. Click to share a model, join one or sign out.`
: 'Archify Sync (optional): sign in, share a model with others or join one. Set the server under Settings › Archify › Sync.';
this.status.backgroundColor = undefined;
this.status.show();
this.bridge?.changed();
return;
}
-20
View File
@@ -27,26 +27,8 @@ const COMMANDS = [
*/
export function registerSync(context: vscode.ExtensionContext, models: ModelService): vscode.Disposable[] {
let feature: Promise<SyncFeature> | undefined;
// Until the sync bundle loads, this entry is the way in; then the bundle's own one takes over.
const entry = vscode.window.createStatusBarItem('archify.sync', vscode.StatusBarAlignment.Left, 50);
entry.name = 'Archify Sync';
entry.text = '$(cloud) Archify Sync';
entry.tooltip = 'Archify Sync (optional): sign in, share a model with others or join one. Set the server under Settings › Archify › Sync.';
entry.command = 'archify.sync.menu';
// Only where there are Archify files: the extension also starts for JSON and HTML.
let archifyHere = false;
const showEntry = () => {
const wanted = archifyHere && !feature && vscode.workspace.getConfiguration('archify.sync').get<boolean>('statusBar', true);
if (wanted) entry.show();
else entry.hide();
};
void Promise.resolve(vscode.workspace.findFiles('**/*.{archify,archify-registry,archify-decision}', '{**/node_modules/**,**/.git/**}', 1)).then((found) => {
archifyHere = found.length > 0;
showEntry();
});
const load = (): Promise<SyncFeature> =>
(feature ??= (async () => {
showEntry();
// A runtime path, so esbuild leaves the sync bundle out of this one.
const bundle = path.join(__dirname, 'sync.js');
const mod = require(bundle) as typeof import('./index');
@@ -63,8 +45,6 @@ export function registerSync(context: vscode.ExtensionContext, models: ModelServ
})();
return [
entry,
vscode.workspace.onDidChangeConfiguration((e) => e.affectsConfiguration('archify.sync.statusBar') && showEntry()),
liveBridge,
...COMMANDS.map((id) =>
vscode.commands.registerCommand(id, async (...args: unknown[]) => (await load()).run(id, ...args)),
+32
View File
@@ -17,6 +17,8 @@ export type WebviewMessage =
| { type: 'showModel' }
| { type: 'liveReady' }
| { type: 'liveAction'; action: LiveAction }
| { type: 'syncMenu' }
| { type: 'syncReady' }
| { type: 'error'; message: string };
/** Column for opening files next to a webview without covering it. */
@@ -66,6 +68,28 @@ async function openSource(
vscode.window.showWarningMessage(`Archify: source file "${message.path}" was not found in the workspace.`);
}
/**
* Whether the file's model is shared: an .archify-sync file next to its
* main.archify. Read from disk, so the sync bundle stays unloaded.
*/
async function isShared(file: vscode.Uri): Promise<boolean> {
const folder = vscode.workspace.getWorkspaceFolder(file);
let dir = vscode.Uri.joinPath(file, '..');
for (let depth = 0; depth < 16; depth++) {
if (await exists(vscode.Uri.joinPath(dir, '.archify-sync'))) return true;
if (await exists(vscode.Uri.joinPath(dir, 'main.archify'))) return false;
const parent = vscode.Uri.joinPath(dir, '..');
if (!folder || dir.path === folder.uri.path || parent.path === dir.path) return false;
dir = parent;
}
return false;
}
async function postSyncState(file: vscode.Uri, panel: vscode.WebviewPanel): Promise<void> {
const shared = file.scheme === 'file' && (await isShared(file));
await Promise.resolve(panel.webview.postMessage({ type: 'syncState', shared })).catch(() => undefined);
}
export async function handleWebviewMessage(
message: WebviewMessage,
context: vscode.Uri,
@@ -99,6 +123,14 @@ export async function handleWebviewMessage(
case 'error':
vscode.window.showErrorMessage(`Archify: ${message.message}`);
break;
case 'syncReady':
await postSyncState(context, panel);
break;
case 'syncMenu':
await vscode.commands.executeCommand('archify.sync.menu');
// Share or Stop may have changed it.
await postSyncState(context, panel);
break;
case 'hash':
case 'openDiagram':
case 'showGraph':
+60
View File
@@ -887,6 +887,66 @@ function bridgeScript(options: PageOptions): string {
onReady(renderDecisionsRail);
onReady(describeElements);
onReady(renderModeSwitch);
// Archify Sync: one icon button, top right, on every page of an Archify
// file (diagram, model, graph, registry, decision). It opens the Sync menu.
function renderSyncButton() {
if (!(config.source || config.model || config.registry || config.decision || config.graph)) return;
if (document.getElementById('archify-sync-button')) return;
var style = document.createElement('style');
style.textContent = SYNC_BUTTON_STYLE;
document.head.appendChild(style);
var button = document.createElement('button');
button.id = 'archify-sync-button';
button.type = 'button';
button.title = 'Archify Sync: sign in, share this model with others, or join one';
button.addEventListener('click', function (event) { event.stopPropagation(); post({ type: 'syncMenu' }); });
// A diagram page has Archify's toolbar at the top right: the button goes
// last in it, after Export. Other pages have nothing there; it floats.
var toolbar = document.querySelector('body > .toolbar');
if (toolbar) {
button.classList.add('in-toolbar');
toolbar.appendChild(button);
} else {
document.body.appendChild(button);
}
showSyncState(false);
post({ type: 'syncReady' });
}
// Not shared: a cloud. Shared (the model has an .archify-sync file): broadcast waves on green.
var SYNC_ICONS = {
off: '<svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true"><path fill="currentColor" d="M12.5 13H4.25a3.25 3.25 0 0 1-.6-6.44 4.5 4.5 0 0 1 8.7-.81A3.6 3.6 0 0 1 12.5 13Zm-8.25-1h8.25a2.6 2.6 0 0 0 0-5.2h-.5l-.12-.39a3.5 3.5 0 0 0-6.82.66l-.04.4-.4.05A2.25 2.25 0 0 0 4.25 12Z"/></svg>',
on: '<svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true"><circle cx="8" cy="8" r="1.75" fill="currentColor"/><path fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" d="M5.2 5.2a4 4 0 0 0 0 5.6M10.8 5.2a4 4 0 0 1 0 5.6M3.1 3.1a7 7 0 0 0 0 9.8M12.9 3.1a7 7 0 0 1 0 9.8"/></svg>',
};
function showSyncState(shared) {
var button = document.getElementById('archify-sync-button');
if (!button) return;
button.classList.toggle('is-shared', !!shared);
button.innerHTML = shared ? SYNC_ICONS.on : SYNC_ICONS.off;
button.title = shared
? 'Archify Sync: this model is shared. Invite people, publish a version, see versions'
: 'Archify Sync: sign in, share this model with others, or join one';
button.setAttribute('aria-label', shared ? 'Archify Sync (shared)' : 'Archify Sync');
}
window.addEventListener('message', function (event) {
var message = event.data || {};
if (message.type === 'syncState') showSyncState(message.shared);
});
// Every rule is !important: the page's own button styles (Archify's toolbar,
// VS Code's high contrast themes) must not resize or recolour it.
var SYNC_BUTTON_STYLE = '#archify-sync-button { all: unset; position: fixed !important; top: 12px !important; right: 28px !important; z-index: 1001 !important;'
+ ' box-sizing: border-box !important; width: 30px !important; height: 30px !important; min-width: 0 !important; min-height: 0 !important; margin: 0 !important; padding: 0 !important;'
+ ' display: flex !important; align-items: center !important; justify-content: center !important; border-radius: 8px !important; cursor: pointer !important;'
+ ' color: #fff !important; background: var(--vscode-button-background, #0e639c) !important; border: 1px solid var(--vscode-contrastBorder, transparent) !important;'
+ ' box-shadow: 0 1px 4px rgba(0,0,0,.35) !important; }'
+ ' #archify-sync-button.in-toolbar { position: relative !important; top: auto !important; right: auto !important; align-self: center !important; flex: none !important; width: 36px !important; height: 36px !important; }'
+ ' body.vscode-high-contrast #archify-sync-button { background: #0e639c !important; }'
+ ' #archify-sync-button svg { display: block !important; width: 16px !important; height: 16px !important; flex: none !important; }'
+ ' #archify-sync-button:hover { background: var(--vscode-button-hoverBackground, #1177bb) !important; }'
+ ' #archify-sync-button.is-shared { background: #1f8f5f !important; }'
+ ' #archify-sync-button.is-shared:hover { background: #23a36c !important; }'
+ ' #archify-sync-button:focus-visible { outline: 2px solid var(--vscode-focusBorder, #007fd4) !important; outline-offset: 2px !important; }';
onReady(renderSyncButton);
if (config.source) {
// The host answers with the problem count for the Source badge.
post({ type: 'sourceReady' });
+1 -1
View File
@@ -20,7 +20,7 @@ type HostMessage =
const STYLE = `
html.archify-source-on .container > :not(.header):not(#archify-source),
html.archify-source-on body > :not(.toolbar):not(.container):not(.archify-model-head):not(.archify-model-switch-row):not(#archify-source):not(script):not(style):not(.archify-modal-backdrop),
html.archify-source-on body > :not(.toolbar):not(.container):not(.archify-model-head):not(.archify-model-switch-row):not(#archify-source):not(#archify-sync-button):not(script):not(style):not(.archify-modal-backdrop),
html.archify-source-on #btn-3d,
html.archify-source-on .archify-vscode-notice { display: none !important; }
html:not(.archify-source-on) #archify-source { display: none !important; }
-2
View File
@@ -493,8 +493,6 @@ export async function run(): Promise<void> {
// No server of anyone's built in: empty until the user sets one.
const config = vscode.workspace.getConfiguration('archify.sync');
assert.equal(config.get('server'), '');
assert.equal(config.get('statusBar'), true);
assert.equal(config.get('titleButton'), true);
// The extension host's module cache: dist/sync.js (Yjs, the WebSocket) is not loaded.
const loaded = Object.keys(require.cache).filter((file) => file.endsWith(`${path.sep}dist${path.sep}sync.js`));
assert.deepEqual(loaded, [], 'the sync bundle is not loaded');