Compare commits
18
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b36136432 | ||
|
|
7dde9ffffe | ||
|
|
f6f59fae67 | ||
|
|
0752128789 | ||
|
|
517697197e | ||
|
|
c71296b01a | ||
|
|
86a8adf03b | ||
|
|
6153c7f145 | ||
|
|
885977c4aa | ||
|
|
95c6e60199 | ||
|
|
b54304fef8 | ||
|
|
8407c9a956 | ||
|
|
c2d33780f9 | ||
|
|
90c7be06de | ||
|
|
7d2d55543e | ||
|
|
c262bb06a7 | ||
|
|
63f0633daa | ||
|
|
ef272e4d96 |
No files matched your search
@@ -1,5 +1,44 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.10.10 — 2026-11-08
|
||||||
|
|
||||||
|
- README: the Buy me a coffee button at the top.
|
||||||
|
|
||||||
|
## 0.10.9 — 2026-11-08
|
||||||
|
|
||||||
|
- The Sync button is square again on the model, registry, decision and Graph pages (0.10.8 squeezed it narrow there).
|
||||||
|
|
||||||
|
## 0.10.8 — 2026-11-08
|
||||||
|
|
||||||
|
- **A model opens as its outline.** On `main.archify` the top-level panels (Shared, Diagrams, Registries, Decisions) start collapsed; open the one you need. What you open or close is kept while the page is open.
|
||||||
|
- **Sync button in line on every page.** On the model, registry and decision pages it sits next to Model | Graph | Source instead of floating above it, and on the Graph page it comes after Export PNG instead of covering it. It takes the height of the controls beside it.
|
||||||
|
|
||||||
|
## 0.10.7 — 2026-11-08
|
||||||
|
|
||||||
|
- **Model | Graph | Source looks like 2D | 3D.** The selected view is the same inset pill, the same height as the other toolbar controls.
|
||||||
|
- The Sync button of a shared model is green in high contrast themes too.
|
||||||
|
|
||||||
|
## 0.10.6 — 2026-11-08
|
||||||
|
|
||||||
|
- **Sync sign-in comes back by itself.** The browser now returns the sign-in to a one-off port on this machine (`127.0.0.1`) instead of a `vscode://` link. Browsers open such links only after a click, so when you were already signed in to the identity provider the editor kept waiting. Remote and web editors still use the link.
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
## 0.10.2 — 2026-11-07
|
||||||
|
|
||||||
|
- Fixed: going back from **Source** to **Model**, **Diagram** or **View** left the YAML editor on the page under the picture when nothing had been edited. It now hides at once.
|
||||||
|
|
||||||
## 0.10.1 — 2026-11-06
|
## 0.10.1 — 2026-11-06
|
||||||
|
|
||||||
- **Archify Sync is easier to find.** In workspaces with Archify files, **Archify Sync** sits in the status bar. Click it for a menu: sign in, share a model, join one, set the server address, open the settings. It shows the server you are signed in to, and the live state once a model is shared. `archify.sync.statusBar: false` hides it; nothing else changes without a server.
|
- **Archify Sync is easier to find.** In workspaces with Archify files, **Archify Sync** sits in the status bar. Click it for a menu: sign in, share a model, join one, set the server address, open the settings. It shows the server you are signed in to, and the live state once a model is shared. `archify.sync.statusBar: false` hides it; nothing else changes without a server.
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
# Archify Diagram Viewer
|
# Archify Diagram Viewer
|
||||||
|
|
||||||
|
[](https://buymeacoffee.com/root.at.skic)
|
||||||
|
|
||||||
Create and view [Archify](https://github.com/tt-a1i/archify) diagrams inside VS Code. You write a diagram as a **`.archify` file in YAML**; clicking it opens the interactive diagram, and the diagram follows your edits as you type.
|
Create and view [Archify](https://github.com/tt-a1i/archify) diagrams inside VS Code. You write a diagram as a **`.archify` file in YAML**; clicking it opens the interactive diagram, and the diagram follows your edits as you type.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
This is an unofficial extension. It bundles the Archify renderer (MIT, © tt-a1i) and runs it on VS Code's own Node runtime, so nothing else needs to be installed.
|
This is an unofficial extension. It bundles the Archify renderer (MIT, © tt-a1i) and runs it on VS Code's own Node runtime, so nothing else needs to be installed.
|
||||||
|
|
||||||
Free and open source. If it saves you time, you can [buy me a coffee](https://buymeacoffee.com/root.at.skic) ☕
|
|
||||||
|
|
||||||
## Supported files
|
## Supported files
|
||||||
|
|
||||||
| File | Format | What the extension does |
|
| File | Format | What the extension does |
|
||||||
@@ -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.
|
[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.** **Archify Sync** in the status bar (in workspaces with Archify files) opens a menu: sign in, share a model, join one, the server address, settings. The same menu is *Archify | Sync…*. Hide the entry with `archify.sync.statusBar: false`.
|
- **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.
|
- **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.
|
- **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.
|
- **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.
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "archify-vscode-ext",
|
"name": "archify-vscode-ext",
|
||||||
"version": "0.10.1",
|
"version": "0.10.10",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "archify-vscode-ext",
|
"name": "archify-vscode-ext",
|
||||||
"version": "0.10.1",
|
"version": "0.10.10",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/autocomplete": "^6.20.4",
|
"@codemirror/autocomplete": "^6.20.4",
|
||||||
|
|||||||
+3
-8
@@ -2,7 +2,7 @@
|
|||||||
"name": "archify-vscode-ext",
|
"name": "archify-vscode-ext",
|
||||||
"displayName": "Archify Diagram Viewer",
|
"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.",
|
"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.1",
|
"version": "0.10.10",
|
||||||
"publisher": "root-at-skic",
|
"publisher": "root-at-skic",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"sponsor": {
|
"sponsor": {
|
||||||
@@ -163,7 +163,7 @@
|
|||||||
{
|
{
|
||||||
"command": "archify.sync.menu",
|
"command": "archify.sync.menu",
|
||||||
"title": "Archify | Sync…",
|
"title": "Archify | Sync…",
|
||||||
"icon": "$(broadcast)"
|
"icon": "$(live-share)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"command": "archify.sync.setServer",
|
"command": "archify.sync.setServer",
|
||||||
@@ -521,12 +521,7 @@
|
|||||||
"scope": "window",
|
"scope": "window",
|
||||||
"pattern": "^$|^https?://",
|
"pattern": "^$|^https?://",
|
||||||
"patternErrorMessage": "An address such as https://archify.example.com",
|
"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."
|
"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."
|
||||||
},
|
|
||||||
"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."
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
+36
-3
@@ -1,4 +1,5 @@
|
|||||||
import * as crypto from 'node:crypto';
|
import * as crypto from 'node:crypto';
|
||||||
|
import * as http from 'node:http';
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import { SyncApi, SyncApiError } from './api';
|
import { SyncApi, SyncApiError } from './api';
|
||||||
import { Account, TokenResponse } from './protocol';
|
import { Account, TokenResponse } from './protocol';
|
||||||
@@ -100,8 +101,13 @@ export class SyncAuth implements vscode.AuthenticationProvider, vscode.Disposabl
|
|||||||
const verifier = crypto.randomBytes(32).toString('base64url');
|
const verifier = crypto.randomBytes(32).toString('base64url');
|
||||||
const challenge = crypto.createHash('sha256').update(verifier).digest('base64url');
|
const challenge = crypto.createHash('sha256').update(verifier).digest('base64url');
|
||||||
const state = crypto.randomBytes(16).toString('base64url');
|
const state = crypto.randomBytes(16).toString('base64url');
|
||||||
const callback = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://${this.context.extension.id}/auth`));
|
// On the desktop the browser comes back to a port on this machine: no
|
||||||
const redirectUri = callback.toString(true);
|
// click needed, and it reaches this window. Remote and web editors use
|
||||||
|
// the extension's URI handler (vscode://…/auth) instead.
|
||||||
|
const loopback = vscode.env.remoteName === undefined && vscode.env.uiKind === vscode.UIKind.Desktop ? await this.listen().catch(() => undefined) : undefined;
|
||||||
|
const redirectUri = loopback
|
||||||
|
? `http://127.0.0.1:${loopback.port}/auth`
|
||||||
|
: (await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://${this.context.extension.id}/auth`))).toString(true);
|
||||||
const query = new URLSearchParams({
|
const query = new URLSearchParams({
|
||||||
response_type: 'code',
|
response_type: 'code',
|
||||||
redirect_uri: redirectUri,
|
redirect_uri: redirectUri,
|
||||||
@@ -119,6 +125,7 @@ export class SyncAuth implements vscode.AuthenticationProvider, vscode.Disposabl
|
|||||||
const done = () => {
|
const done = () => {
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
this.pending.delete(state);
|
this.pending.delete(state);
|
||||||
|
loopback?.close();
|
||||||
};
|
};
|
||||||
this.pending.set(state, {
|
this.pending.set(state, {
|
||||||
resolve: (c) => (done(), resolve(c)),
|
resolve: (c) => (done(), resolve(c)),
|
||||||
@@ -137,9 +144,35 @@ export class SyncAuth implements vscode.AuthenticationProvider, vscode.Disposabl
|
|||||||
return session;
|
return session;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** A one-off HTTP listener on 127.0.0.1 for the browser's way back; closed when the sign-in ends. */
|
||||||
|
private listen(): Promise<{ port: number; close: () => void }> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const server = http.createServer((req, res) => {
|
||||||
|
const url = new URL(req.url ?? '/', 'http://127.0.0.1');
|
||||||
|
const known = url.pathname === '/auth' && this.pending.has(url.searchParams.get('state') ?? '');
|
||||||
|
res.writeHead(known ? 200 : 404, { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store' });
|
||||||
|
res.end(
|
||||||
|
known
|
||||||
|
? '<!doctype html><meta charset="utf-8"><title>Archify Sync</title><body style="font:15px system-ui;margin:48px auto;max-width:420px"><h2>Signed in</h2><p>You can close this tab and go back to the editor.</p></body>'
|
||||||
|
: 'Not found',
|
||||||
|
);
|
||||||
|
if (known) this.completeSignIn(url.searchParams);
|
||||||
|
});
|
||||||
|
server.once('error', reject);
|
||||||
|
server.listen(0, '127.0.0.1', () => {
|
||||||
|
const address = server.address();
|
||||||
|
if (!address || typeof address === 'string') return reject(new Error('no port'));
|
||||||
|
resolve({ port: address.port, close: () => server.close() });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/** The browser came back: `…/auth?code=&state=`. */
|
/** The browser came back: `…/auth?code=&state=`. */
|
||||||
handleAuthUri(uri: vscode.Uri): void {
|
handleAuthUri(uri: vscode.Uri): void {
|
||||||
const query = new URLSearchParams(uri.query);
|
this.completeSignIn(new URLSearchParams(uri.query));
|
||||||
|
}
|
||||||
|
|
||||||
|
private completeSignIn(query: URLSearchParams): void {
|
||||||
const state = query.get('state') ?? '';
|
const state = query.get('state') ?? '';
|
||||||
const code = query.get('code');
|
const code = query.get('code');
|
||||||
const pending = this.pending.get(state);
|
const pending = this.pending.get(state);
|
||||||
|
|||||||
+1
-15
@@ -41,8 +41,6 @@ export class SyncFeature implements vscode.Disposable, LiveProvider {
|
|||||||
private cursorTimer: ReturnType<typeof setTimeout> | undefined;
|
private cursorTimer: ReturnType<typeof setTimeout> | undefined;
|
||||||
/** Servers with a session on this machine, for the status bar before any model is shared. */
|
/** Servers with a session on this machine, for the status bar before any model is shared. */
|
||||||
private signedIn: string[] = [];
|
private signedIn: string[] = [];
|
||||||
/** Whether the workspace has Archify files; elsewhere the entry stays hidden until a model is shared. */
|
|
||||||
private archifyHere = false;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly context: vscode.ExtensionContext,
|
private readonly context: vscode.ExtensionContext,
|
||||||
@@ -80,7 +78,6 @@ export class SyncFeature implements vscode.Disposable, LiveProvider {
|
|||||||
async start(): Promise<void> {
|
async start(): Promise<void> {
|
||||||
const found = await vscode.workspace.findFiles(`**/${LINK_FILE}`, SKIP, 50);
|
const found = await vscode.workspace.findFiles(`**/${LINK_FILE}`, SKIP, 50);
|
||||||
await Promise.all(found.map((uri) => this.open(uri, 'open')));
|
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();
|
await this.refreshSignedIn();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,19 +136,8 @@ export class SyncFeature implements vscode.Disposable, LiveProvider {
|
|||||||
private render(): void {
|
private render(): void {
|
||||||
const spaces = [...this.spaces.values()];
|
const spaces = [...this.spaces.values()];
|
||||||
if (!spaces.length) {
|
if (!spaces.length) {
|
||||||
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();
|
this.status.hide();
|
||||||
return;
|
void vscode.commands.executeCommand('setContext', 'archify.sync.active', false);
|
||||||
}
|
|
||||||
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();
|
this.bridge?.changed();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,26 +27,8 @@ const COMMANDS = [
|
|||||||
*/
|
*/
|
||||||
export function registerSync(context: vscode.ExtensionContext, models: ModelService): vscode.Disposable[] {
|
export function registerSync(context: vscode.ExtensionContext, models: ModelService): vscode.Disposable[] {
|
||||||
let feature: Promise<SyncFeature> | undefined;
|
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> =>
|
const load = (): Promise<SyncFeature> =>
|
||||||
(feature ??= (async () => {
|
(feature ??= (async () => {
|
||||||
showEntry();
|
|
||||||
// A runtime path, so esbuild leaves the sync bundle out of this one.
|
// A runtime path, so esbuild leaves the sync bundle out of this one.
|
||||||
const bundle = path.join(__dirname, 'sync.js');
|
const bundle = path.join(__dirname, 'sync.js');
|
||||||
const mod = require(bundle) as typeof import('./index');
|
const mod = require(bundle) as typeof import('./index');
|
||||||
@@ -63,8 +45,6 @@ export function registerSync(context: vscode.ExtensionContext, models: ModelServ
|
|||||||
})();
|
})();
|
||||||
|
|
||||||
return [
|
return [
|
||||||
entry,
|
|
||||||
vscode.workspace.onDidChangeConfiguration((e) => e.affectsConfiguration('archify.sync.statusBar') && showEntry()),
|
|
||||||
liveBridge,
|
liveBridge,
|
||||||
...COMMANDS.map((id) =>
|
...COMMANDS.map((id) =>
|
||||||
vscode.commands.registerCommand(id, async (...args: unknown[]) => (await load()).run(id, ...args)),
|
vscode.commands.registerCommand(id, async (...args: unknown[]) => (await load()).run(id, ...args)),
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ export type WebviewMessage =
|
|||||||
| { type: 'showModel' }
|
| { type: 'showModel' }
|
||||||
| { type: 'liveReady' }
|
| { type: 'liveReady' }
|
||||||
| { type: 'liveAction'; action: LiveAction }
|
| { type: 'liveAction'; action: LiveAction }
|
||||||
|
| { type: 'syncMenu' }
|
||||||
|
| { type: 'syncReady' }
|
||||||
| { type: 'error'; message: string };
|
| { type: 'error'; message: string };
|
||||||
|
|
||||||
/** Column for opening files next to a webview without covering it. */
|
/** 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.`);
|
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(
|
export async function handleWebviewMessage(
|
||||||
message: WebviewMessage,
|
message: WebviewMessage,
|
||||||
context: vscode.Uri,
|
context: vscode.Uri,
|
||||||
@@ -99,6 +123,14 @@ export async function handleWebviewMessage(
|
|||||||
case 'error':
|
case 'error':
|
||||||
vscode.window.showErrorMessage(`Archify: ${message.message}`);
|
vscode.window.showErrorMessage(`Archify: ${message.message}`);
|
||||||
break;
|
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 'hash':
|
||||||
case 'openDiagram':
|
case 'openDiagram':
|
||||||
case 'showGraph':
|
case 'showGraph':
|
||||||
|
|||||||
+81
-5
@@ -208,6 +208,7 @@ function bridgeScript(options: PageOptions): string {
|
|||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
// Collapsible panels of the model page; open/closed is kept in the webview state.
|
// Collapsible panels of the model page; open/closed is kept in the webview state.
|
||||||
|
// The model page's top-level panels start closed: a model opens as its outline.
|
||||||
function panelStates() { return (vscode.getState() || {}).modelPanels || {}; }
|
function panelStates() { return (vscode.getState() || {}).modelPanels || {}; }
|
||||||
function panel(key, className, title, count, defaultOpen) {
|
function panel(key, className, title, count, defaultOpen) {
|
||||||
// count: a number or a short text such as "3 files".
|
// count: a number or a short text such as "3 files".
|
||||||
@@ -624,7 +625,7 @@ function bridgeScript(options: PageOptions): string {
|
|||||||
live.hidden = true;
|
live.hidden = true;
|
||||||
body.appendChild(live);
|
body.appendChild(live);
|
||||||
var count = model.groups.reduce(function (n, g) { return n + g.diagrams.length; }, 0);
|
var count = model.groups.reduce(function (n, g) { return n + g.diagrams.length; }, 0);
|
||||||
var diagramsPanel = panel('diagrams', 'archify-model-diagrams', 'Diagrams', count, true);
|
var diagramsPanel = panel('diagrams', 'archify-model-diagrams', 'Diagrams', count, false);
|
||||||
body.appendChild(diagramsPanel);
|
body.appendChild(diagramsPanel);
|
||||||
if (!count) diagramsPanel.appendChild(el('p', 'archify-model-empty', 'No diagrams yet. Add .archify files next to main.archify or in subfolders.'));
|
if (!count) diagramsPanel.appendChild(el('p', 'archify-model-empty', 'No diagrams yet. Add .archify files next to main.archify or in subfolders.'));
|
||||||
model.groups.forEach(function (group) {
|
model.groups.forEach(function (group) {
|
||||||
@@ -658,7 +659,7 @@ function bridgeScript(options: PageOptions): string {
|
|||||||
byFile[element.file].push(element);
|
byFile[element.file].push(element);
|
||||||
});
|
});
|
||||||
files.sort();
|
files.sort();
|
||||||
var registryPanel = panel('registry', 'archify-model-registry', 'Registries', files.length + (files.length === 1 ? ' file' : ' files'), true);
|
var registryPanel = panel('registry', 'archify-model-registry', 'Registries', files.length + (files.length === 1 ? ' file' : ' files'), false);
|
||||||
body.appendChild(registryPanel);
|
body.appendChild(registryPanel);
|
||||||
files.forEach(function (file) {
|
files.forEach(function (file) {
|
||||||
var elements = byFile[file].slice().sort(function (a, b) { return a.line - b.line; });
|
var elements = byFile[file].slice().sort(function (a, b) { return a.line - b.line; });
|
||||||
@@ -676,7 +677,7 @@ function bridgeScript(options: PageOptions): string {
|
|||||||
}
|
}
|
||||||
// Decisions: one list, live ones first, superseded and rejected dimmed and last.
|
// Decisions: one list, live ones first, superseded and rejected dimmed and last.
|
||||||
if (model.decisions && model.decisions.length) {
|
if (model.decisions && model.decisions.length) {
|
||||||
var decisionsPanel = panel('decisions', 'archify-model-decisions', 'Decisions', model.decisions.length, true);
|
var decisionsPanel = panel('decisions', 'archify-model-decisions', 'Decisions', model.decisions.length, false);
|
||||||
body.appendChild(decisionsPanel);
|
body.appendChild(decisionsPanel);
|
||||||
var decisionList = el('ul', 'archify-model-list archify-decision-list');
|
var decisionList = el('ul', 'archify-model-list archify-decision-list');
|
||||||
model.decisions.forEach(function (row) { decisionList.appendChild(decisionRowItem(row)); });
|
model.decisions.forEach(function (row) { decisionList.appendChild(decisionRowItem(row)); });
|
||||||
@@ -723,7 +724,7 @@ function bridgeScript(options: PageOptions): string {
|
|||||||
if (!info) { holder.hidden = true; return; }
|
if (!info) { holder.hidden = true; return; }
|
||||||
holder.hidden = false;
|
holder.hidden = false;
|
||||||
var others = info.people.filter(function (p) { return !p.me; });
|
var others = info.people.filter(function (p) { return !p.me; });
|
||||||
var livePanel = panel('live', 'archify-model-live is-' + info.state, 'Shared \u00b7 ' + info.name, others.length ? others.length + 1 + ' here' : info.host, true);
|
var livePanel = panel('live', 'archify-model-live is-' + info.state, 'Shared \u00b7 ' + info.name, others.length ? others.length + 1 + ' here' : info.host, false);
|
||||||
var status = el('div', 'archify-live-status');
|
var status = el('div', 'archify-live-status');
|
||||||
status.appendChild(el('span', 'archify-live-dot'));
|
status.appendChild(el('span', 'archify-live-dot'));
|
||||||
status.appendChild(el('span', 'archify-live-state', (LIVE_STATE[info.state] || info.state) + (info.state === 'online' ? ' on ' + info.host : '')));
|
status.appendChild(el('span', 'archify-live-state', (LIVE_STATE[info.state] || info.state) + (info.state === 'online' ? ' on ' + info.host : '')));
|
||||||
@@ -887,6 +888,78 @@ function bridgeScript(options: PageOptions): string {
|
|||||||
onReady(renderDecisionsRail);
|
onReady(renderDecisionsRail);
|
||||||
onReady(describeElements);
|
onReady(describeElements);
|
||||||
onReady(renderModeSwitch);
|
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' }); });
|
||||||
|
// Last in the row that holds the view switch: Archify's toolbar on a
|
||||||
|
// diagram (after Export), the Graph page's header actions (after Export,
|
||||||
|
// built a moment later by graph.js), next to the switch on the other
|
||||||
|
// pages. It floats only while there is no such row.
|
||||||
|
document.body.appendChild(button);
|
||||||
|
function place() {
|
||||||
|
var modeSwitch = document.getElementById('archify-mode-switch');
|
||||||
|
var row = document.querySelector('.ag-actions') || (modeSwitch && modeSwitch.parentElement) || document.querySelector('body > .toolbar');
|
||||||
|
if (!row || row === document.body) return false;
|
||||||
|
button.classList.add('in-toolbar');
|
||||||
|
row.classList.add('archify-sync-row');
|
||||||
|
row.appendChild(button);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!place() && window.MutationObserver) {
|
||||||
|
var watch = new MutationObserver(function () { if (place()) watch.disconnect(); });
|
||||||
|
watch.observe(document.body, { childList: true, subtree: true });
|
||||||
|
}
|
||||||
|
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-model-switch-row.archify-sync-row { display: flex !important; align-items: center !important; gap: 0.5rem !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; }'
|
||||||
|
+ ' .archify-model-switch-row #archify-sync-button.in-toolbar { width: 33px !important; height: 33px !important; }'
|
||||||
|
+ ' .ag-actions #archify-sync-button.in-toolbar { width: 29px !important; height: 29px !important; }'
|
||||||
|
+ ' body.vscode-high-contrast #archify-sync-button:not(.is-shared) { 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) {
|
if (config.source) {
|
||||||
// The host answers with the problem count for the Source badge.
|
// The host answers with the problem count for the Source badge.
|
||||||
post({ type: 'sourceReady' });
|
post({ type: 'sourceReady' });
|
||||||
@@ -1013,10 +1086,13 @@ const MODEL_STYLE = `
|
|||||||
/* The Model | Graph switch: in Archify's toolbar it is the 2D | 3D switch's twin; on the model page it sits top right. */
|
/* The Model | Graph switch: in Archify's toolbar it is the 2D | 3D switch's twin; on the model page it sits top right. */
|
||||||
const SWITCH_STYLE = `
|
const SWITCH_STYLE = `
|
||||||
.archify-mode-switch { display: inline-flex; align-items: center; padding: 0 0.25rem; gap: 0; color: var(--text-muted, var(--vscode-descriptionForeground)); position: relative; }
|
.archify-mode-switch { display: inline-flex; align-items: center; padding: 0 0.25rem; gap: 0; color: var(--text-muted, var(--vscode-descriptionForeground)); position: relative; }
|
||||||
|
.toolbar .archify-mode-switch { height: 2.75rem; box-sizing: border-box; }
|
||||||
.toolbar .archify-mode-switch::before { content: ""; position: absolute; inset: 0.25rem 0; border-radius: 0.6875rem; background: color-mix(in srgb, var(--panel) 78%, transparent); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--panel-border) 85%, transparent), 0 1px 2px rgba(15, 23, 42, 0.04); backdrop-filter: blur(12px); }
|
.toolbar .archify-mode-switch::before { content: ""; position: absolute; inset: 0.25rem 0; border-radius: 0.6875rem; background: color-mix(in srgb, var(--panel) 78%, transparent); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--panel-border) 85%, transparent), 0 1px 2px rgba(15, 23, 42, 0.04); backdrop-filter: blur(12px); }
|
||||||
.toolbar .archify-mode-switch:hover::before { box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--text) 22%, transparent), 0 1px 2px rgba(15, 23, 42, 0.04); }
|
.toolbar .archify-mode-switch:hover::before { box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--text) 22%, transparent), 0 1px 2px rgba(15, 23, 42, 0.04); }
|
||||||
/* Anchored on the id: Archify's .toolbar button rules (one of them with :not(#btn-theme)) would win otherwise. */
|
/* Anchored on the id: Archify's .toolbar button rules (one of them with :not(#btn-theme)) would win otherwise. */
|
||||||
#archify-mode-switch .archify-mode-seg { position: relative; display: inline-flex; align-items: center; height: 1.75rem; padding: 0 0.7rem; border: 0; border-radius: 0.5rem; background: transparent; color: inherit; font: inherit; font-weight: 550; font-size: 0.8125rem; cursor: pointer; transition: background 0.15s, color 0.15s; }
|
#archify-mode-switch .archify-mode-seg { position: relative; display: inline-flex; align-items: center; height: 1.75rem; min-height: 0; margin: 0; padding: 0 0.7rem; border: 0; border-radius: 0.5rem; background: transparent; color: inherit; font: inherit; font-weight: 550; font-size: 0.8125rem; cursor: pointer; transition: background 0.15s, color 0.15s; }
|
||||||
|
/* Archify's toolbar buttons are 2.75rem tall with a painted ::before; the segments are not, like 2D | 3D. */
|
||||||
|
#archify-mode-switch .archify-mode-seg::before, #archify-mode-switch .archify-mode-seg::after { content: none; }
|
||||||
#archify-mode-switch .archify-mode-seg.on[aria-pressed="true"] { color: var(--bg, var(--vscode-editor-background)); background: var(--text, var(--vscode-foreground)); box-shadow: 0 1px 2px rgba(15, 23, 42, 0.12), 0 6px 16px -8px rgba(15, 23, 42, 0.4); }
|
#archify-mode-switch .archify-mode-seg.on[aria-pressed="true"] { color: var(--bg, var(--vscode-editor-background)); background: var(--text, var(--vscode-foreground)); box-shadow: 0 1px 2px rgba(15, 23, 42, 0.12), 0 6px 16px -8px rgba(15, 23, 42, 0.4); }
|
||||||
#archify-mode-switch .archify-mode-seg:hover:not(.on) { color: var(--text, var(--vscode-foreground)); }
|
#archify-mode-switch .archify-mode-seg:hover:not(.on) { color: var(--text, var(--vscode-foreground)); }
|
||||||
#archify-mode-switch .archify-mode-seg:focus-visible { outline: 2px solid var(--frontend-stroke, var(--vscode-focusBorder)); outline-offset: 1px; }
|
#archify-mode-switch .archify-mode-seg:focus-visible { outline: 2px solid var(--frontend-stroke, var(--vscode-focusBorder)); outline-offset: 1px; }
|
||||||
|
|||||||
@@ -20,9 +20,10 @@ type HostMessage =
|
|||||||
|
|
||||||
const STYLE = `
|
const STYLE = `
|
||||||
html.archify-source-on .container > :not(.header):not(#archify-source),
|
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 #btn-3d,
|
||||||
html.archify-source-on .archify-vscode-notice { display: none !important; }
|
html.archify-source-on .archify-vscode-notice { display: none !important; }
|
||||||
|
html:not(.archify-source-on) #archify-source { display: none !important; }
|
||||||
.archify-source { display: flex; flex-direction: column; box-sizing: border-box; min-height: 260px; margin: 0.75rem 0 1rem; }
|
.archify-source { display: flex; flex-direction: column; box-sizing: border-box; min-height: 260px; margin: 0.75rem 0 1rem; }
|
||||||
body > .archify-source { max-width: 1400px; margin: 0.75rem auto 1rem; padding: 0 2rem; }
|
body > .archify-source { max-width: 1400px; margin: 0.75rem auto 1rem; padding: 0 2rem; }
|
||||||
.archify-source-editor { flex: 1; min-height: 0; overflow: hidden; border: 1px solid var(--panel-border, var(--vscode-editorWidget-border, rgba(128,128,128,.35))); border-radius: 0.75rem;
|
.archify-source-editor { flex: 1; min-height: 0; overflow: hidden; border: 1px solid var(--panel-border, var(--vscode-editorWidget-border, rgba(128,128,128,.35))); border-radius: 0.75rem;
|
||||||
|
|||||||
@@ -493,7 +493,6 @@ export async function run(): Promise<void> {
|
|||||||
// No server of anyone's built in: empty until the user sets one.
|
// No server of anyone's built in: empty until the user sets one.
|
||||||
const config = vscode.workspace.getConfiguration('archify.sync');
|
const config = vscode.workspace.getConfiguration('archify.sync');
|
||||||
assert.equal(config.get('server'), '');
|
assert.equal(config.get('server'), '');
|
||||||
assert.equal(config.get('statusBar'), true);
|
|
||||||
// The extension host's module cache: dist/sync.js (Yjs, the WebSocket) is not loaded.
|
// 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`));
|
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');
|
assert.deepEqual(loaded, [], 'the sync bundle is not loaded');
|
||||||
|
|||||||
Reference in new issue
Block a user