@isothrum/editor 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +8 -0
- package/PUBLISHING.md +93 -0
- package/README.md +106 -2
- package/desktop-editor/editor.css +11 -0
- package/desktop-editor/index.html +14 -0
- package/desktop-editor/main.tsx +26 -0
- package/dist/desktop/editor.css +1 -0
- package/dist/desktop/editor.js +181 -0
- package/dist/desktop/fonts/geist-cyrillic-ext-wght-normal-RO72NYA2.woff2 +0 -0
- package/dist/desktop/fonts/geist-cyrillic-wght-normal-CK2XY27K.woff2 +0 -0
- package/dist/desktop/fonts/geist-latin-ext-wght-normal-XEQ56JUF.woff2 +0 -0
- package/dist/desktop/fonts/geist-latin-wght-normal-T72VVCUW.woff2 +0 -0
- package/dist/desktop/fonts/geist-mono-cyrillic-ext-wght-normal-QXKVIKFD.woff2 +0 -0
- package/dist/desktop/fonts/geist-mono-cyrillic-wght-normal-GOIAQAGU.woff2 +0 -0
- package/dist/desktop/fonts/geist-mono-latin-ext-wght-normal-HU3ZAWN6.woff2 +0 -0
- package/dist/desktop/fonts/geist-mono-latin-wght-normal-CJNQKXUG.woff2 +0 -0
- package/dist/desktop/fonts/geist-mono-symbols2-wght-normal-WZJJVLQO.woff2 +0 -0
- package/dist/desktop/fonts/geist-mono-vietnamese-wght-normal-KW5NM6YV.woff2 +0 -0
- package/dist/desktop/fonts/geist-vietnamese-wght-normal-Y2R7JZKV.woff2 +0 -0
- package/dist/desktop/fonts/shantell-sans-cyrillic-ext-wght-normal-ILPQWRJJ.woff2 +0 -0
- package/dist/desktop/fonts/shantell-sans-cyrillic-wght-normal-RKC6NDPM.woff2 +0 -0
- package/dist/desktop/fonts/shantell-sans-latin-ext-wght-normal-SNKHG3LM.woff2 +0 -0
- package/dist/desktop/fonts/shantell-sans-latin-wght-normal-7CHHGC3N.woff2 +0 -0
- package/dist/desktop/fonts/shantell-sans-vietnamese-wght-normal-IPFAQSBA.woff2 +0 -0
- package/dist/desktop/index.html +14 -0
- package/dist/desktop/styles.css +2 -0
- package/dist/desktop/wasm/isothrum_wasm.d.ts +60 -0
- package/dist/desktop/wasm/isothrum_wasm.js +312 -0
- package/dist/desktop/wasm/isothrum_wasm_bg.wasm +0 -0
- package/dist/index.js +7059 -0
- package/dist/index.js.map +7 -0
- package/dist/model.js +745 -0
- package/dist/model.js.map +7 -0
- package/dist/types/components/tool/canvas/cable-edge.d.ts +4 -0
- package/dist/types/components/tool/canvas/contexts.d.ts +12 -0
- package/dist/types/components/tool/canvas/device-icon.d.ts +17 -0
- package/dist/types/components/tool/canvas/inline-text.d.ts +15 -0
- package/dist/types/components/tool/canvas/lan-node.d.ts +8 -0
- package/dist/types/components/tool/canvas/machine-addresses.d.ts +14 -0
- package/dist/types/components/tool/canvas/machine-node.d.ts +4 -0
- package/dist/types/components/tool/canvas/note-node.d.ts +7 -0
- package/dist/types/components/tool/canvas/port-led.d.ts +10 -0
- package/dist/types/components/tool/canvas/router-node.d.ts +8 -0
- package/dist/types/components/tool/canvas/sketch.d.ts +50 -0
- package/dist/types/components/tool/canvas/status-dot.d.ts +5 -0
- package/dist/types/components/tool/canvas/switch-node.d.ts +8 -0
- package/dist/types/components/tool/canvas/use-measured-ports.d.ts +7 -0
- package/dist/types/components/tool/context-menus.d.ts +5 -0
- package/dist/types/components/tool/device-block.d.ts +12 -0
- package/dist/types/components/tool/editor-menu.d.ts +28 -0
- package/dist/types/components/tool/export/copy-button.d.ts +9 -0
- package/dist/types/components/tool/export/export-panel.d.ts +17 -0
- package/dist/types/components/tool/export/target-list.d.ts +15 -0
- package/dist/types/components/tool/floating-window.d.ts +13 -0
- package/dist/types/components/tool/flow-canvas.d.ts +6 -0
- package/dist/types/components/tool/hooks/use-cabling.d.ts +23 -0
- package/dist/types/components/tool/hooks/use-captures.d.ts +9 -0
- package/dist/types/components/tool/hooks/use-clipboard.d.ts +12 -0
- package/dist/types/components/tool/hooks/use-context-menus.d.ts +25 -0
- package/dist/types/components/tool/hooks/use-design.d.ts +10 -0
- package/dist/types/components/tool/hooks/use-desktop.d.ts +50 -0
- package/dist/types/components/tool/hooks/use-devices.d.ts +17 -0
- package/dist/types/components/tool/hooks/use-downloads.d.ts +9 -0
- package/dist/types/components/tool/hooks/use-editor.d.ts +609 -0
- package/dist/types/components/tool/hooks/use-engine.d.ts +9 -0
- package/dist/types/components/tool/hooks/use-history.d.ts +9 -0
- package/dist/types/components/tool/hooks/use-lab-actions.d.ts +25 -0
- package/dist/types/components/tool/hooks/use-lab-run.d.ts +25 -0
- package/dist/types/components/tool/hooks/use-lab.d.ts +18 -0
- package/dist/types/components/tool/hooks/use-lan-carry.d.ts +8 -0
- package/dist/types/components/tool/hooks/use-machine-actions.d.ts +26 -0
- package/dist/types/components/tool/hooks/use-placement.d.ts +27 -0
- package/dist/types/components/tool/hooks/use-project.d.ts +25 -0
- package/dist/types/components/tool/hooks/use-projects.d.ts +34 -0
- package/dist/types/components/tool/hooks/use-shared-link.d.ts +23 -0
- package/dist/types/components/tool/hooks/use-toast.d.ts +2 -0
- package/dist/types/components/tool/hooks/use-tools.d.ts +17 -0
- package/dist/types/components/tool/lab-canvas.d.ts +18 -0
- package/dist/types/components/tool/overlays.d.ts +5 -0
- package/dist/types/components/tool/projects-list.d.ts +9 -0
- package/dist/types/components/tool/text.d.ts +418 -0
- package/dist/types/components/tool/toolbar.d.ts +5 -0
- package/dist/types/components/tool/top-bars.d.ts +10 -0
- package/dist/types/components/tool/windows/conditions-fields.d.ts +6 -0
- package/dist/types/components/tool/windows/editor-windows.d.ts +5 -0
- package/dist/types/components/tool/windows/fields.d.ts +4 -0
- package/dist/types/components/tool/windows/lab-status.d.ts +6 -0
- package/dist/types/components/tool/windows/lab-windows.d.ts +13 -0
- package/dist/types/components/tool/windows/link-controls.d.ts +21 -0
- package/dist/types/components/tool/windows/machine-controls.d.ts +16 -0
- package/dist/types/components/tool/windows/machine-inspector.d.ts +11 -0
- package/dist/types/components/tool/windows/runs-fields.d.ts +18 -0
- package/dist/types/components/tool/windows/selected-windows.d.ts +21 -0
- package/dist/types/components/ui/button.d.ts +8 -0
- package/dist/types/components/ui/dropdown-menu.d.ts +21 -0
- package/dist/types/index.d.ts +7 -0
- package/dist/types/lib/auth.d.ts +38 -0
- package/dist/types/lib/brand.d.ts +19 -0
- package/dist/types/lib/i18n.d.ts +19 -0
- package/dist/types/lib/tool/blocks.d.ts +35 -0
- package/dist/types/lib/tool/cables.d.ts +48 -0
- package/dist/types/lib/tool/clone.d.ts +12 -0
- package/dist/types/lib/tool/conditions.d.ts +31 -0
- package/dist/types/lib/tool/create.d.ts +22 -0
- package/dist/types/lib/tool/desktop.d.ts +63 -0
- package/dist/types/lib/tool/draft.d.ts +20 -0
- package/dist/types/lib/tool/engine.d.ts +33 -0
- package/dist/types/lib/tool/examples.d.ts +11 -0
- package/dist/types/lib/tool/export-targets.d.ts +21 -0
- package/dist/types/lib/tool/facing.d.ts +9 -0
- package/dist/types/lib/tool/geometry.d.ts +36 -0
- package/dist/types/lib/tool/gns3-cables.d.ts +17 -0
- package/dist/types/lib/tool/gns3-import.d.ts +15 -0
- package/dist/types/lib/tool/gns3-parts.d.ts +25 -0
- package/dist/types/lib/tool/issues.d.ts +12 -0
- package/dist/types/lib/tool/layout.d.ts +52 -0
- package/dist/types/lib/tool/live.d.ts +21 -0
- package/dist/types/lib/tool/logos.d.ts +7 -0
- package/dist/types/lib/tool/model.d.ts +173 -0
- package/dist/types/lib/tool/os.d.ts +20 -0
- package/dist/types/lib/tool/pins.d.ts +5 -0
- package/dist/types/lib/tool/placement.d.ts +21 -0
- package/dist/types/lib/tool/ports.d.ts +21 -0
- package/dist/types/lib/tool/presets.d.ts +21 -0
- package/dist/types/lib/tool/projects-api.d.ts +37 -0
- package/dist/types/lib/tool/router-ports.d.ts +22 -0
- package/dist/types/lib/tool/selection.d.ts +19 -0
- package/dist/types/lib/tool/storage.d.ts +25 -0
- package/dist/types/lib/tool/switch-edit.d.ts +17 -0
- package/dist/types/lib/tool/text/de.d.ts +2 -0
- package/dist/types/lib/tool/text/en.d.ts +429 -0
- package/dist/types/lib/tool/text/es.d.ts +2 -0
- package/dist/types/lib/tool/text/fr.d.ts +2 -0
- package/dist/types/lib/tool/text/index.d.ts +419 -0
- package/dist/types/lib/tool/topology.d.ts +2 -0
- package/dist/types/lib/tool/vlans.d.ts +24 -0
- package/dist/types/lib/utils.d.ts +2 -0
- package/dist/types/model.d.ts +13 -0
- package/package.json +88 -4
- package/scripts/build-desktop-editor.mjs +43 -0
- package/scripts/build-lib.mjs +38 -0
- package/src/components/tool/canvas/cable-edge.tsx +112 -0
- package/src/components/tool/canvas/contexts.ts +18 -0
- package/src/components/tool/canvas/device-icon.tsx +105 -0
- package/src/components/tool/canvas/inline-text.tsx +69 -0
- package/src/components/tool/canvas/lan-node.tsx +148 -0
- package/src/components/tool/canvas/machine-addresses.tsx +117 -0
- package/src/components/tool/canvas/machine-node.tsx +114 -0
- package/src/components/tool/canvas/note-node.tsx +109 -0
- package/src/components/tool/canvas/port-led.tsx +21 -0
- package/src/components/tool/canvas/router-node.tsx +114 -0
- package/src/components/tool/canvas/sketch.tsx +130 -0
- package/src/components/tool/canvas/status-dot.tsx +29 -0
- package/src/components/tool/canvas/switch-node.tsx +143 -0
- package/src/components/tool/canvas/use-measured-ports.ts +25 -0
- package/src/components/tool/context-menus.tsx +142 -0
- package/src/components/tool/device-block.tsx +107 -0
- package/src/components/tool/editor-menu.tsx +191 -0
- package/src/components/tool/export/copy-button.tsx +45 -0
- package/src/components/tool/export/export-panel.tsx +127 -0
- package/src/components/tool/export/target-list.tsx +87 -0
- package/src/components/tool/floating-window.tsx +69 -0
- package/src/components/tool/flow-canvas.tsx +138 -0
- package/src/components/tool/hooks/use-cabling.ts +66 -0
- package/src/components/tool/hooks/use-captures.ts +24 -0
- package/src/components/tool/hooks/use-clipboard.ts +54 -0
- package/src/components/tool/hooks/use-context-menus.ts +37 -0
- package/src/components/tool/hooks/use-design.ts +24 -0
- package/src/components/tool/hooks/use-desktop.ts +100 -0
- package/src/components/tool/hooks/use-devices.ts +58 -0
- package/src/components/tool/hooks/use-downloads.ts +42 -0
- package/src/components/tool/hooks/use-editor.ts +114 -0
- package/src/components/tool/hooks/use-engine.ts +29 -0
- package/src/components/tool/hooks/use-history.ts +107 -0
- package/src/components/tool/hooks/use-lab-actions.ts +57 -0
- package/src/components/tool/hooks/use-lab-run.ts +61 -0
- package/src/components/tool/hooks/use-lab.ts +32 -0
- package/src/components/tool/hooks/use-lan-carry.ts +43 -0
- package/src/components/tool/hooks/use-machine-actions.ts +57 -0
- package/src/components/tool/hooks/use-placement.ts +80 -0
- package/src/components/tool/hooks/use-project.ts +112 -0
- package/src/components/tool/hooks/use-projects.ts +85 -0
- package/src/components/tool/hooks/use-shared-link.ts +64 -0
- package/src/components/tool/hooks/use-toast.ts +16 -0
- package/src/components/tool/hooks/use-tools.ts +60 -0
- package/src/components/tool/lab-canvas.tsx +96 -0
- package/src/components/tool/overlays.tsx +104 -0
- package/src/components/tool/projects-list.tsx +96 -0
- package/src/components/tool/text.ts +9 -0
- package/src/components/tool/toolbar.tsx +106 -0
- package/src/components/tool/top-bars.tsx +90 -0
- package/src/components/tool/windows/conditions-fields.tsx +43 -0
- package/src/components/tool/windows/editor-windows.tsx +73 -0
- package/src/components/tool/windows/fields.ts +6 -0
- package/src/components/tool/windows/lab-status.tsx +68 -0
- package/src/components/tool/windows/lab-windows.tsx +130 -0
- package/src/components/tool/windows/link-controls.tsx +69 -0
- package/src/components/tool/windows/machine-controls.tsx +116 -0
- package/src/components/tool/windows/machine-inspector.tsx +83 -0
- package/src/components/tool/windows/runs-fields.tsx +127 -0
- package/src/components/tool/windows/selected-windows.tsx +147 -0
- package/src/components/ui/button.tsx +59 -0
- package/src/components/ui/dropdown-menu.tsx +185 -0
- package/src/index.ts +21 -0
- package/src/lib/auth.tsx +43 -0
- package/src/lib/brand.tsx +46 -0
- package/src/lib/i18n.ts +53 -0
- package/src/lib/tool/blocks.test.ts +64 -0
- package/src/lib/tool/blocks.ts +78 -0
- package/src/lib/tool/cable-wiring.test.ts +74 -0
- package/src/lib/tool/cables.test.ts +75 -0
- package/src/lib/tool/cables.ts +215 -0
- package/src/lib/tool/clone.test.ts +54 -0
- package/src/lib/tool/clone.ts +48 -0
- package/src/lib/tool/conditions.test.ts +170 -0
- package/src/lib/tool/conditions.ts +132 -0
- package/src/lib/tool/create.test.ts +67 -0
- package/src/lib/tool/create.ts +83 -0
- package/src/lib/tool/desktop.test.ts +59 -0
- package/src/lib/tool/desktop.ts +99 -0
- package/src/lib/tool/draft.test.ts +150 -0
- package/src/lib/tool/draft.ts +109 -0
- package/src/lib/tool/engine.test.ts +39 -0
- package/src/lib/tool/engine.ts +58 -0
- package/src/lib/tool/examples.test.ts +35 -0
- package/src/lib/tool/examples.ts +71 -0
- package/src/lib/tool/export-targets.test.ts +47 -0
- package/src/lib/tool/export-targets.ts +84 -0
- package/src/lib/tool/facing.test.ts +72 -0
- package/src/lib/tool/facing.ts +100 -0
- package/src/lib/tool/geometry.test.ts +80 -0
- package/src/lib/tool/geometry.ts +109 -0
- package/src/lib/tool/gns3-cables.test.ts +47 -0
- package/src/lib/tool/gns3-cables.ts +81 -0
- package/src/lib/tool/gns3-import.test.ts +102 -0
- package/src/lib/tool/gns3-import.ts +198 -0
- package/src/lib/tool/gns3-layout.test.ts +62 -0
- package/src/lib/tool/gns3-parts.test.ts +47 -0
- package/src/lib/tool/gns3-parts.ts +68 -0
- package/src/lib/tool/issues.test.ts +35 -0
- package/src/lib/tool/issues.ts +60 -0
- package/src/lib/tool/layout.test.ts +128 -0
- package/src/lib/tool/layout.ts +229 -0
- package/src/lib/tool/live.test.ts +46 -0
- package/src/lib/tool/live.ts +49 -0
- package/src/lib/tool/logos.test.ts +30 -0
- package/src/lib/tool/logos.ts +99 -0
- package/src/lib/tool/model.test.ts +128 -0
- package/src/lib/tool/model.ts +238 -0
- package/src/lib/tool/os.test.ts +45 -0
- package/src/lib/tool/os.ts +57 -0
- package/src/lib/tool/parity.test.ts +145 -0
- package/src/lib/tool/pins.test.ts +24 -0
- package/src/lib/tool/pins.ts +11 -0
- package/src/lib/tool/placement.test.ts +79 -0
- package/src/lib/tool/placement.ts +60 -0
- package/src/lib/tool/ports.test.ts +39 -0
- package/src/lib/tool/ports.ts +48 -0
- package/src/lib/tool/presets.test.ts +50 -0
- package/src/lib/tool/presets.ts +77 -0
- package/src/lib/tool/projects-api.test.ts +59 -0
- package/src/lib/tool/projects-api.ts +47 -0
- package/src/lib/tool/router-ports.test.ts +63 -0
- package/src/lib/tool/router-ports.ts +88 -0
- package/src/lib/tool/selection.test.ts +44 -0
- package/src/lib/tool/selection.ts +34 -0
- package/src/lib/tool/storage.test.ts +123 -0
- package/src/lib/tool/storage.ts +127 -0
- package/src/lib/tool/switch-edit.test.ts +49 -0
- package/src/lib/tool/switch-edit.ts +38 -0
- package/src/lib/tool/text/de.ts +358 -0
- package/src/lib/tool/text/en.ts +371 -0
- package/src/lib/tool/text/es.ts +358 -0
- package/src/lib/tool/text/fr.ts +358 -0
- package/src/lib/tool/text/index.ts +21 -0
- package/src/lib/tool/text/text.test.ts +33 -0
- package/src/lib/tool/topology.test.ts +108 -0
- package/src/lib/tool/topology.ts +116 -0
- package/src/lib/tool/vlans.test.ts +64 -0
- package/src/lib/tool/vlans.ts +79 -0
- package/src/lib/utils.ts +6 -0
- package/src/model.ts +13 -0
- package/src/test/arbitrary.ts +41 -0
- package/src/test/engine.ts +48 -0
- package/src/test/lab.ts +101 -0
- package/styles/globals.css +258 -0
- package/styles/theme.css +192 -0
- package/tsconfig.build.json +12 -0
- package/tsconfig.json +22 -0
- package/wasm/isothrum_wasm.d.ts +60 -0
- package/wasm/isothrum_wasm.js +312 -0
- package/wasm/isothrum_wasm_bg.wasm +0 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { accessVlan, isTrunkPort, switchLans, switchPortLabel, trunkInterface, trunkStripes, vlanColor, vlanName, vlanOf } from "./vlans";
|
|
3
|
+
import { cable, switchNode } from "@/test/lab";
|
|
4
|
+
|
|
5
|
+
describe("VLAN names and colors", () => {
|
|
6
|
+
test("names a VLAN vlan<id>, and reads the number back from a LAN's name", () => {
|
|
7
|
+
expect(vlanName(10)).toBe("vlan10");
|
|
8
|
+
expect(["vlan10", "vlan0", "vlan", "vlan12345", "xvlan1"].map(vlanOf)).toEqual([10, 0, null, null, null]);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test("colors a VLAN by its tens, eight colors around", () => {
|
|
12
|
+
expect(vlanColor(10)).toBe("#2f9e44");
|
|
13
|
+
expect(vlanColor(14)).toBe(vlanColor(10));
|
|
14
|
+
expect(vlanColor(90)).toBe(vlanColor(10));
|
|
15
|
+
expect(vlanColor(-10)).toBe(vlanColor(10));
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("stripes a trunk with the colors of its VLANs, muted when it carries none", () => {
|
|
19
|
+
expect(trunkStripes([{ id: 10 }, { id: 20 }])).toBe("repeating-linear-gradient(135deg, #2f9e44 0px 5px, #1971c2 5px 10px)");
|
|
20
|
+
expect(trunkStripes([])).toBe("var(--muted)");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("names a trunk's interface after its LAN, within Linux's limit", () => {
|
|
24
|
+
expect(["sw1", "a-very-long-lan-name", "abcdefghi-x"].map(trunkInterface)).toEqual(["sw1", "a-very-lon", "abcdefghi"]);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe("switch ports", () => {
|
|
29
|
+
const sw = switchNode("sw1");
|
|
30
|
+
|
|
31
|
+
test("the T port and any port set to trunk are trunks", () => {
|
|
32
|
+
expect(["trunk", "p4"].map((h) => isTrunkPort(sw, h))).toEqual([true, true]);
|
|
33
|
+
expect(["p1", null, "eth0"].map((h) => isTrunkPort(sw, h))).toEqual([false, false, false]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("an access port is in its VLAN; a trunk, a free port or no port is in none", () => {
|
|
37
|
+
expect(["p1", "p3"].map((h) => accessVlan(sw, h))).toEqual([10, 20]);
|
|
38
|
+
expect(["p4", "p5", "trunk", "eth0", undefined].map((h) => accessVlan(sw, h))).toEqual([null, null, null, null, null]);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("labels a port 1, 2… or T", () => {
|
|
42
|
+
expect(["p1", "p12", "trunk", "", null, undefined, "x"].map(switchPortLabel)).toEqual(["1", "12", "T", "", "", "", ""]);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe("switchLans", () => {
|
|
47
|
+
const s1 = switchNode("s1", { name: "sw1", vlans: [{ id: 10, cidr: "10.10.10.0/24" }] });
|
|
48
|
+
const s2 = switchNode("s2", { name: "sw2", vlans: [{ id: 30, cidr: "10.10.30.0/24" }] });
|
|
49
|
+
const s3 = switchNode("s3", { name: "sw3", vlans: [{ id: 10, cidr: "10.10.10.0/24" }] });
|
|
50
|
+
|
|
51
|
+
test("switches cabled together are one LAN, named after the first", () => {
|
|
52
|
+
const lanOf = switchLans([s1, s2], [cable("c", "s2", "s1")]);
|
|
53
|
+
expect([lanOf("s1"), lanOf("s2")]).toEqual(["sw1", "sw1"]);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("switches sharing a VLAN number are one LAN", () => {
|
|
57
|
+
expect(switchLans([s1, s3], [])("s3")).toBe("sw1");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("a switch alone is its own LAN, and an unknown id has none", () => {
|
|
61
|
+
const lanOf = switchLans([s1, s2], []);
|
|
62
|
+
expect([lanOf("s2"), lanOf("nope")]).toEqual(["sw2", ""]);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { TRUNK, isSwitch, type CableEdge, type LabNode, type SwitchNode } from "./model";
|
|
2
|
+
|
|
3
|
+
/** VLANs and switch ports. VLANs are lab-wide: `vlan10` is the same network on every switch. */
|
|
4
|
+
|
|
5
|
+
export const vlanName = (id: number) => `vlan${String(id)}`;
|
|
6
|
+
|
|
7
|
+
/** A VLAN's color, from its number, so `vlan10` has the same color on every switch and area. */
|
|
8
|
+
const VLAN_COLORS = ["#7048e8", "#2f9e44", "#1971c2", "#e8590c", "#e03131", "#0c8599", "#c2255c", "#5c940d"];
|
|
9
|
+
export function vlanColor(id: number): string {
|
|
10
|
+
return VLAN_COLORS[Math.abs(Math.round(id / 10)) % VLAN_COLORS.length];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** A trunk port's look: striped with the colors of the VLANs it carries (muted when none). */
|
|
14
|
+
export function trunkStripes(vlans: { id: number }[]): string {
|
|
15
|
+
if (!vlans.length) return "var(--muted)";
|
|
16
|
+
const stripes = vlans.map((v, i) => `${vlanColor(v.id)} ${String(i * 5)}px ${String((i + 1) * 5)}px`).join(", ");
|
|
17
|
+
return `repeating-linear-gradient(135deg, ${stripes})`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** The VLAN number in a LAN's name (`vlan10` -> 10), when the LAN is a VLAN. */
|
|
21
|
+
export function vlanOf(lanName: string): number | null {
|
|
22
|
+
const m = /^vlan(\d{1,4})$/.exec(lanName);
|
|
23
|
+
return m ? Number(m[1]) : null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The port a switch handle stands for: `p3` -> its index, `trunk` -> the trunk. */
|
|
27
|
+
function switchPort(handle: string | null | undefined): number | typeof TRUNK | null {
|
|
28
|
+
if (handle === TRUNK) return TRUNK;
|
|
29
|
+
const m = handle ? /^p(\d+)$/.exec(handle) : null;
|
|
30
|
+
return m ? Number(m[1]) - 1 : null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Whether a switch handle is a trunk: the T port, or any port set to "trunk". */
|
|
34
|
+
export function isTrunkPort(sw: SwitchNode, handle: string | null | undefined): boolean {
|
|
35
|
+
const port = switchPort(handle);
|
|
36
|
+
return port === TRUNK || (typeof port === "number" && sw.data.ports[port] === TRUNK);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The VLAN of an access port, or null (a trunk, an unassigned port, not a port). */
|
|
40
|
+
export function accessVlan(sw: SwitchNode, handle: string | null | undefined): number | null {
|
|
41
|
+
const port = switchPort(handle);
|
|
42
|
+
const mode = typeof port === "number" ? sw.data.ports[port] : null;
|
|
43
|
+
return typeof mode === "number" ? mode : null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** A port's label next to its cable: 1, 2… or T. */
|
|
47
|
+
export function switchPortLabel(handle: string | null | undefined): string {
|
|
48
|
+
const p = switchPort(handle);
|
|
49
|
+
if (p === TRUNK) return "T";
|
|
50
|
+
return p === null ? "" : String(p + 1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The LAN each switch's VLANs belong to in isoloom.yml: switches joined by a cable, or sharing a
|
|
55
|
+
* VLAN number (VLANs are lab-wide here), are one LAN, named after the first. Returns a switch's
|
|
56
|
+
* LAN name by its id.
|
|
57
|
+
*/
|
|
58
|
+
export function switchLans(nodes: LabNode[], edges: CableEdge[]): (switchId: string) => string {
|
|
59
|
+
const switches = nodes.filter(isSwitch);
|
|
60
|
+
const group = new Map(switches.map((s) => [s.id, s.id]));
|
|
61
|
+
const root = (id: string): string => {
|
|
62
|
+
const up = group.get(id);
|
|
63
|
+
return up === undefined || up === id ? id : root(up);
|
|
64
|
+
};
|
|
65
|
+
const unite = (a: string, b: string) => group.set(root(a), root(b));
|
|
66
|
+
for (const e of edges) if (group.has(e.source) && group.has(e.target)) unite(e.source, e.target);
|
|
67
|
+
const firstWith = new Map<number, string>();
|
|
68
|
+
for (const sw of switches) {
|
|
69
|
+
for (const v of sw.data.vlans) {
|
|
70
|
+
const other = firstWith.get(v.id);
|
|
71
|
+
if (other) unite(sw.id, other);
|
|
72
|
+
else firstWith.set(v.id, sw.id);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return (id: string) => switches.find((s) => root(s.id) === root(id))?.data.name ?? "";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The name Isoloom gives a trunk's interface: its LAN's, within Linux's limit (`sw1`, then `sw1.10`). */
|
|
79
|
+
export const trunkInterface = (lan: string) => lan.slice(0, 10).replace(/-+$/, "");
|
package/src/lib/utils.ts
ADDED
package/src/model.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The editor's lab model and conversions, with no React: a lab's nodes and cables, the topology the
|
|
3
|
+
* engine reads, and helpers to lay a topology out again. Used by hosts that work with labs outside
|
|
4
|
+
* the canvas (the website's MCP server builds and reads topologies with these).
|
|
5
|
+
*/
|
|
6
|
+
export * from "@/lib/tool/model";
|
|
7
|
+
export { type Topology, type LaidOut, draftFromTopology } from "@/lib/tool/layout";
|
|
8
|
+
export { toTopology } from "@/lib/tool/topology";
|
|
9
|
+
export { PORT_PROTOCOLS, readPorts, portsText, type Service } from "@/lib/tool/ports";
|
|
10
|
+
export { exampleLab, vlanExample } from "@/lib/tool/examples";
|
|
11
|
+
export { planOf, ownFiles, type Engine, type Plan, type GeneratedFile } from "@/lib/tool/engine";
|
|
12
|
+
export { groupByTarget, type Target, type TargetText } from "@/lib/tool/export-targets";
|
|
13
|
+
export { canvasIssues, type IssueText } from "@/lib/tool/issues";
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import fc from "fast-check";
|
|
2
|
+
import { DESIGNS, type Draft, type LinkConditions } from "@/lib/tool/model";
|
|
3
|
+
import { cable, lan, machine } from "./lab";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* fast-check generators for the editor model's property tests: the values the editor itself
|
|
7
|
+
* writes, so a property holds for every lab someone can draw.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** A positive number up to `max`, as a field edit keeps one (see `withCondition`). */
|
|
11
|
+
const positive = (max: number) => fc.double({ min: 0, max, minExcluded: true, noNaN: true });
|
|
12
|
+
|
|
13
|
+
/** Conditions as the editor writes them: a delay (a jitter only varies one), a loss up to 100%, a rate in whole kbit/s. */
|
|
14
|
+
export const arbitraryConditions: fc.Arbitrary<LinkConditions> = fc.record(
|
|
15
|
+
{ delay: positive(1e6), jitter: positive(1e6), loss: positive(100), rate: fc.integer({ min: 1, max: 10_000_000 }).map((kbit) => kbit / 1000) },
|
|
16
|
+
{ requiredKeys: ["delay"] },
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
/** A node to draw: a LAN (with its conditions) or a machine. */
|
|
20
|
+
const arbitraryNode = fc.record({ id: fc.stringMatching(/^[a-z][a-z0-9-]{0,7}$/), lan: fc.boolean(), tc: fc.option(arbitraryConditions, { nil: undefined }) });
|
|
21
|
+
|
|
22
|
+
/** A cable between the `from`th and the `to`th node (modulo their count), with its ports and maybe conditions. */
|
|
23
|
+
const arbitraryCable = fc.record({ from: fc.nat(), to: fc.nat(), aPort: fc.string(), bPort: fc.string(), tc: fc.option(arbitraryConditions, { nil: undefined }) });
|
|
24
|
+
|
|
25
|
+
/** A lab as the editor draws one: LANs and machines, and cables between them, each colored for the canvas. */
|
|
26
|
+
export const arbitraryLab: fc.Arbitrary<Draft> = fc
|
|
27
|
+
.record({
|
|
28
|
+
name: fc.string(),
|
|
29
|
+
design: fc.constantFrom(undefined, ...DESIGNS),
|
|
30
|
+
nodes: fc.uniqueArray(arbitraryNode, { selector: (n) => n.id, minLength: 1, maxLength: 6 }),
|
|
31
|
+
cables: fc.array(arbitraryCable, { maxLength: 8 }),
|
|
32
|
+
})
|
|
33
|
+
.map(({ name, design, nodes, cables }) => ({
|
|
34
|
+
name,
|
|
35
|
+
design,
|
|
36
|
+
nodes: nodes.map((n) => (n.lan ? lan(n.id, { tc: n.tc }) : machine(n.id))),
|
|
37
|
+
edges: cables.map((c, i) => {
|
|
38
|
+
const [source, target] = [nodes[c.from % nodes.length].id, nodes[c.to % nodes.length].id];
|
|
39
|
+
return cable(`c${String(i)}`, source, target, { aPort: c.aPort, bPort: c.bPort, tc: c.tc, color: "#000" });
|
|
40
|
+
}),
|
|
41
|
+
}));
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The engine the package ships (`wasm/`), loaded in Node for the tests: Isoloom itself, so the
|
|
7
|
+
* editor's model is checked against what Isoloom accepts, not against a copy of its rules.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** The engine's functions the tests call. */
|
|
11
|
+
export interface TestEngine {
|
|
12
|
+
plan_json(topology: string): string;
|
|
13
|
+
choices_json(): string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type EngineModule = TestEngine & { initSync(options: { module: Buffer }): unknown };
|
|
17
|
+
|
|
18
|
+
function isEngineModule(mod: unknown): mod is EngineModule {
|
|
19
|
+
if (typeof mod !== "object" || mod === null) return false;
|
|
20
|
+
return ["initSync", "plan_json", "choices_json"].every((name) => typeof (mod as Record<string, unknown>)[name] === "function");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let loading: Promise<TestEngine> | null = null;
|
|
24
|
+
|
|
25
|
+
/** The shipped engine, started once for the test file. */
|
|
26
|
+
export function shippedEngine(): Promise<TestEngine> {
|
|
27
|
+
loading ??= (async () => {
|
|
28
|
+
const dir = join(process.cwd(), "wasm");
|
|
29
|
+
const mod: unknown = await import(pathToFileURL(join(dir, "isothrum_wasm.js")).href);
|
|
30
|
+
if (!isEngineModule(mod)) throw new Error("wasm/isothrum_wasm.js is not the Isothrum engine with choices_json");
|
|
31
|
+
mod.initSync({ module: readFileSync(join(dir, "isothrum_wasm_bg.wasm")) });
|
|
32
|
+
return mod;
|
|
33
|
+
})();
|
|
34
|
+
return loading;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** What Isoloom's engine says of a topology: its problems, or the error that stopped it. */
|
|
38
|
+
export interface Verdict {
|
|
39
|
+
ok: boolean;
|
|
40
|
+
error: string | null;
|
|
41
|
+
problems: string[];
|
|
42
|
+
isoloom_yml: string | null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The engine's verdict on `topology`. */
|
|
46
|
+
export async function verdict(topology: unknown): Promise<Verdict> {
|
|
47
|
+
return JSON.parse((await shippedEngine()).plan_json(JSON.stringify(topology))) as Verdict;
|
|
48
|
+
}
|
package/src/test/lab.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { CableData, CableEdge, LabNode, LanData, LanNode, MachineData, MachineNode, NoteNode, SwitchNode } from "@/lib/tool/model";
|
|
2
|
+
import type { IssueText } from "@/lib/tool/issues";
|
|
3
|
+
import { toTopology } from "@/lib/tool/topology";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Builders for the editor model's tests: small labs, each node given only what the test is about.
|
|
7
|
+
* A node's name defaults to its id.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
type At = { x: number; y: number };
|
|
11
|
+
|
|
12
|
+
/** A container machine (nginx), unless `data` says otherwise. */
|
|
13
|
+
export function machine(id: string, data: Partial<MachineData> = {}, position: At = { x: 0, y: 0 }): MachineNode {
|
|
14
|
+
return {
|
|
15
|
+
id,
|
|
16
|
+
type: "machine",
|
|
17
|
+
position,
|
|
18
|
+
data: { name: id, kind: "container", icon: "container", image: "nginx:1.27-alpine", os: "", provision: "", ports: "", access: false, ...data },
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** An automatic router (the kind that is no machine). */
|
|
23
|
+
export const autoRouter = (id: string, position: At = { x: 0, y: 0 }): MachineNode => machine(id, { kind: "router", icon: "router", image: "" }, position);
|
|
24
|
+
|
|
25
|
+
/** A router you configure (FRR in a container): drawn with ports. */
|
|
26
|
+
export const realRouter = (id: string, position: At = { x: 0, y: 0 }, data: Partial<MachineData> = {}): MachineNode =>
|
|
27
|
+
machine(id, { icon: "router", image: "frrouting/frr:v8.5.2", ...data }, position);
|
|
28
|
+
|
|
29
|
+
/** A LAN area at `area` (400 by 300 at the origin by default). */
|
|
30
|
+
export function lan(id: string, data: Partial<LanData> = {}, area: Partial<{ x: number; y: number; w: number; h: number }> = {}): LanNode {
|
|
31
|
+
const { x = 0, y = 0, w = 400, h = 300 } = area;
|
|
32
|
+
return { id, type: "lan", position: { x, y }, width: w, height: h, data: { name: id, cidr: "10.10.1.0/24", internet: true, ...data } };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** A switch: vlan10 on ports 1 and 2, vlan20 on port 3, port 4 a trunk, the rest unassigned. */
|
|
36
|
+
export function switchNode(id: string, data: Partial<SwitchNode["data"]> = {}, position: At = { x: 0, y: 0 }): SwitchNode {
|
|
37
|
+
return {
|
|
38
|
+
id,
|
|
39
|
+
type: "switch",
|
|
40
|
+
position,
|
|
41
|
+
data: {
|
|
42
|
+
name: id,
|
|
43
|
+
vlans: [
|
|
44
|
+
{ id: 10, cidr: "10.10.10.0/24" },
|
|
45
|
+
{ id: 20, cidr: "10.10.20.0/24" },
|
|
46
|
+
],
|
|
47
|
+
ports: [10, 10, 20, "trunk", null, null, null, null],
|
|
48
|
+
...data,
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const note = (id: string, position: At = { x: 0, y: 0 }): NoteNode => ({
|
|
54
|
+
id,
|
|
55
|
+
type: "note",
|
|
56
|
+
position,
|
|
57
|
+
width: 200,
|
|
58
|
+
height: 90,
|
|
59
|
+
data: { text: "remember", shape: "rect", color: "yellow" },
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
/** A cable's ends and labels: handles default to none, ports to eth0 on both ends. */
|
|
63
|
+
type CableOptions = { sourceHandle?: string | null; targetHandle?: string | null } & Partial<CableData>;
|
|
64
|
+
|
|
65
|
+
export function cable(id: string, source: string, target: string, options: CableOptions = {}): CableEdge {
|
|
66
|
+
const { sourceHandle = null, targetHandle = null, ...data } = options;
|
|
67
|
+
return { id, type: "cable", source, sourceHandle, target, targetHandle, data: { aPort: "eth0", bPort: "eth0", ...data } };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The node of `nodes` with id `id` (a test failure when there is none). */
|
|
71
|
+
export function byId<T extends LabNode>(nodes: T[], id: string): T {
|
|
72
|
+
const found = nodes.find((n) => n.id === id);
|
|
73
|
+
if (!found) throw new Error(`no node ${id}`);
|
|
74
|
+
return found;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The canvas's findings in plain words, so tests can read them. */
|
|
78
|
+
export const issueText: IssueText = {
|
|
79
|
+
vlanBlocks: (vlan, block, other, sw) => `${vlan}: ${block} vs ${other} on ${sw}`,
|
|
80
|
+
areaBlock: (lan, block, switches) => `area ${lan}: ${block} vs ${switches}`,
|
|
81
|
+
noName: { machine: "unnamed machine", lan: "unnamed lan", switch: "unnamed switch" },
|
|
82
|
+
sameName: { machine: (n) => `two machines ${n}`, lan: (n) => `two lans ${n}`, switch: (n) => `two switches ${n}` },
|
|
83
|
+
badPort: (m, entry) => `${m}: port ${entry}`,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/** A compiled lab: `toTopology`'s JSON, parsed. */
|
|
87
|
+
export interface Compiled {
|
|
88
|
+
name: string;
|
|
89
|
+
nodes: Record<string, { kind: Record<string, unknown>; access: boolean; services: { port: number; protocol?: string }[]; networks: Record<string, number | null> }>;
|
|
90
|
+
networks: Record<string, { cidr: string; internet: boolean; tc?: unknown; switch?: string; vlan?: number }>;
|
|
91
|
+
links: { a: { node: string; port: string }; b: { node: string; port: string }; tc?: unknown }[];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** The lab as the engine reads it. */
|
|
95
|
+
export const compile = (nodes: LabNode[], edges: CableEdge[] = [], name = "lab"): Compiled => JSON.parse(toTopology(name, nodes, edges)) as Compiled;
|
|
96
|
+
|
|
97
|
+
/** The networks the VLANs of a switch named sw1 become: `switchNode("sw1")`'s, and the VLAN example's. */
|
|
98
|
+
export const sw1Networks: Compiled["networks"] = {
|
|
99
|
+
vlan10: { cidr: "10.10.10.0/24", internet: true, switch: "sw1", vlan: 10 },
|
|
100
|
+
vlan20: { cidr: "10.10.20.0/24", internet: true, switch: "sw1", vlan: 20 },
|
|
101
|
+
};
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
@import "tw-animate-css";
|
|
3
|
+
@import "shadcn/tailwind.css";
|
|
4
|
+
@import "./theme.css";
|
|
5
|
+
@plugin "@tailwindcss/typography";
|
|
6
|
+
|
|
7
|
+
/* The unit tests and their helpers are never rendered: their strings aren't classes. */
|
|
8
|
+
@source not "../src/**/*.test.ts";
|
|
9
|
+
@source not "../src/test";
|
|
10
|
+
|
|
11
|
+
@custom-variant dark (&:is(.dark *));
|
|
12
|
+
|
|
13
|
+
@theme inline {
|
|
14
|
+
/* Typography — font files are loaded in lib/fonts.ts */
|
|
15
|
+
--font-sans: var(--font-brand-sans), ui-sans-serif, system-ui, sans-serif;
|
|
16
|
+
--font-mono: var(--font-brand-mono), ui-monospace, SFMono-Regular, monospace;
|
|
17
|
+
--font-heading: var(--font-brand-heading, var(--font-brand-sans)), ui-sans-serif, system-ui, sans-serif;
|
|
18
|
+
|
|
19
|
+
/* Semantic colors — values live in theme.css */
|
|
20
|
+
--color-background: var(--background);
|
|
21
|
+
--color-foreground: var(--foreground);
|
|
22
|
+
--color-surface: var(--surface);
|
|
23
|
+
--color-card: var(--card);
|
|
24
|
+
--color-card-foreground: var(--card-foreground);
|
|
25
|
+
--color-popover: var(--popover);
|
|
26
|
+
--color-popover-foreground: var(--popover-foreground);
|
|
27
|
+
--color-primary: var(--primary);
|
|
28
|
+
--color-signal: var(--signal);
|
|
29
|
+
--color-signal-foreground: var(--signal-foreground);
|
|
30
|
+
--color-pastel-blue: var(--pastel-blue);
|
|
31
|
+
--color-pastel-green: var(--pastel-green);
|
|
32
|
+
--color-pastel-yellow: var(--pastel-yellow);
|
|
33
|
+
--color-pastel-violet: var(--pastel-violet);
|
|
34
|
+
--color-pastel-red: var(--pastel-red);
|
|
35
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
36
|
+
--color-secondary: var(--secondary);
|
|
37
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
|
38
|
+
--color-muted: var(--muted);
|
|
39
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
40
|
+
--color-accent: var(--accent);
|
|
41
|
+
--color-accent-foreground: var(--accent-foreground);
|
|
42
|
+
--color-link: var(--link);
|
|
43
|
+
--color-destructive: var(--destructive);
|
|
44
|
+
--color-success: var(--success);
|
|
45
|
+
--color-warning: var(--warning);
|
|
46
|
+
--color-info: var(--info);
|
|
47
|
+
--color-border: var(--border);
|
|
48
|
+
--color-input: var(--input);
|
|
49
|
+
--color-ring: var(--ring);
|
|
50
|
+
--color-chart-1: var(--chart-1);
|
|
51
|
+
--color-chart-2: var(--chart-2);
|
|
52
|
+
--color-chart-3: var(--chart-3);
|
|
53
|
+
--color-chart-4: var(--chart-4);
|
|
54
|
+
--color-chart-5: var(--chart-5);
|
|
55
|
+
--color-sidebar: var(--sidebar);
|
|
56
|
+
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
57
|
+
--color-sidebar-primary: var(--sidebar-primary);
|
|
58
|
+
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
59
|
+
--color-sidebar-accent: var(--sidebar-accent);
|
|
60
|
+
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
61
|
+
--color-sidebar-border: var(--sidebar-border);
|
|
62
|
+
--color-sidebar-ring: var(--sidebar-ring);
|
|
63
|
+
|
|
64
|
+
/* Brand scale as utilities: bg-brand-50, text-brand-700, … */
|
|
65
|
+
--color-brand-50: var(--brand-50);
|
|
66
|
+
--color-brand-100: var(--brand-100);
|
|
67
|
+
--color-brand-200: var(--brand-200);
|
|
68
|
+
--color-brand-300: var(--brand-300);
|
|
69
|
+
--color-brand-400: var(--brand-400);
|
|
70
|
+
--color-brand-500: var(--brand-500);
|
|
71
|
+
--color-brand-600: var(--brand-600);
|
|
72
|
+
--color-brand-700: var(--brand-700);
|
|
73
|
+
--color-brand-800: var(--brand-800);
|
|
74
|
+
--color-brand-900: var(--brand-900);
|
|
75
|
+
--color-brand-950: var(--brand-950);
|
|
76
|
+
|
|
77
|
+
/* Radius scale, derived from --radius */
|
|
78
|
+
--radius-sm: calc(var(--radius) * 0.6);
|
|
79
|
+
--radius-md: calc(var(--radius) * 0.8);
|
|
80
|
+
--radius-lg: var(--radius);
|
|
81
|
+
--radius-xl: calc(var(--radius) * 1.4);
|
|
82
|
+
--radius-2xl: calc(var(--radius) * 1.8);
|
|
83
|
+
--radius-3xl: calc(var(--radius) * 2.2);
|
|
84
|
+
--radius-4xl: calc(var(--radius) * 2.6);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
@layer base {
|
|
88
|
+
* {
|
|
89
|
+
@apply border-border outline-ring/50;
|
|
90
|
+
}
|
|
91
|
+
body {
|
|
92
|
+
@apply bg-background text-foreground;
|
|
93
|
+
}
|
|
94
|
+
html {
|
|
95
|
+
@apply font-sans;
|
|
96
|
+
font-feature-settings: var(--font-features);
|
|
97
|
+
text-rendering: optimizeLegibility;
|
|
98
|
+
}
|
|
99
|
+
h1,
|
|
100
|
+
h2,
|
|
101
|
+
h3,
|
|
102
|
+
h4 {
|
|
103
|
+
font-family: var(--font-heading);
|
|
104
|
+
letter-spacing: -0.02em;
|
|
105
|
+
text-wrap: balance;
|
|
106
|
+
}
|
|
107
|
+
::selection {
|
|
108
|
+
background-color: color-mix(in oklab, var(--primary) 20%, transparent);
|
|
109
|
+
}
|
|
110
|
+
/* Stable scrollbar gutter to avoid CLS when content overflows */
|
|
111
|
+
html {
|
|
112
|
+
scrollbar-gutter: stable;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/*
|
|
117
|
+
* Map Tailwind Typography's palette onto the theme tokens so article text,
|
|
118
|
+
* links and quotes follow theme.css in both light and dark mode (no need for
|
|
119
|
+
* `dark:prose-invert`).
|
|
120
|
+
*/
|
|
121
|
+
.prose {
|
|
122
|
+
--tw-prose-body: color-mix(in oklab, var(--foreground) 85%, transparent);
|
|
123
|
+
--tw-prose-headings: var(--foreground);
|
|
124
|
+
--tw-prose-lead: var(--muted-foreground);
|
|
125
|
+
--tw-prose-links: var(--link);
|
|
126
|
+
--tw-prose-bold: var(--foreground);
|
|
127
|
+
--tw-prose-counters: var(--muted-foreground);
|
|
128
|
+
--tw-prose-bullets: var(--border);
|
|
129
|
+
--tw-prose-hr: var(--border);
|
|
130
|
+
--tw-prose-quotes: var(--foreground);
|
|
131
|
+
--tw-prose-quote-borders: var(--primary);
|
|
132
|
+
--tw-prose-captions: var(--muted-foreground);
|
|
133
|
+
--tw-prose-kbd: var(--foreground);
|
|
134
|
+
--tw-prose-code: var(--foreground);
|
|
135
|
+
--tw-prose-th-borders: var(--border);
|
|
136
|
+
--tw-prose-td-borders: var(--border);
|
|
137
|
+
--tw-prose-invert-body: var(--tw-prose-body);
|
|
138
|
+
--tw-prose-invert-headings: var(--tw-prose-headings);
|
|
139
|
+
--tw-prose-invert-lead: var(--tw-prose-lead);
|
|
140
|
+
--tw-prose-invert-links: var(--tw-prose-links);
|
|
141
|
+
--tw-prose-invert-bold: var(--tw-prose-bold);
|
|
142
|
+
--tw-prose-invert-counters: var(--tw-prose-counters);
|
|
143
|
+
--tw-prose-invert-bullets: var(--tw-prose-bullets);
|
|
144
|
+
--tw-prose-invert-hr: var(--tw-prose-hr);
|
|
145
|
+
--tw-prose-invert-quotes: var(--tw-prose-quotes);
|
|
146
|
+
--tw-prose-invert-quote-borders: var(--tw-prose-quote-borders);
|
|
147
|
+
--tw-prose-invert-captions: var(--tw-prose-captions);
|
|
148
|
+
--tw-prose-invert-code: var(--tw-prose-code);
|
|
149
|
+
--tw-prose-invert-th-borders: var(--tw-prose-th-borders);
|
|
150
|
+
--tw-prose-invert-td-borders: var(--tw-prose-td-borders);
|
|
151
|
+
}
|
|
152
|
+
.prose :where(a):not(:where([class~="not-prose"] *)) {
|
|
153
|
+
text-underline-offset: 3px;
|
|
154
|
+
text-decoration-thickness: 1px;
|
|
155
|
+
text-decoration-color: color-mix(in oklab, var(--link) 40%, transparent);
|
|
156
|
+
transition: text-decoration-color 150ms;
|
|
157
|
+
}
|
|
158
|
+
.prose :where(a):not(:where([class~="not-prose"] *)):hover {
|
|
159
|
+
text-decoration-color: var(--link);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/*
|
|
163
|
+
* Inline code styling. Tailwind Typography wraps inline `<code>` with
|
|
164
|
+
* literal backticks via ::before/::after — we remove those and replace
|
|
165
|
+
* the default look with a quiet background pill so file paths and command
|
|
166
|
+
* names visually pop from prose without competing with editorial links.
|
|
167
|
+
*
|
|
168
|
+
* `:not(pre code)` keeps fenced code blocks (`<pre><code>`) untouched so
|
|
169
|
+
* any syntax highlighter (e.g. Shiki via rehype-pretty-code) keeps its
|
|
170
|
+
* token colors and background.
|
|
171
|
+
*/
|
|
172
|
+
.prose :where(code):not(:where([class~="not-prose"] *, pre code)) {
|
|
173
|
+
background-color: var(--muted);
|
|
174
|
+
color: var(--foreground);
|
|
175
|
+
padding: 0.125rem 0.375rem;
|
|
176
|
+
border-radius: 0.25rem;
|
|
177
|
+
font-weight: 500;
|
|
178
|
+
font-size: 0.875em;
|
|
179
|
+
word-break: break-word;
|
|
180
|
+
}
|
|
181
|
+
.prose :where(code):not(:where([class~="not-prose"] *))::before,
|
|
182
|
+
.prose :where(code):not(:where([class~="not-prose"] *))::after {
|
|
183
|
+
content: none;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/*
|
|
187
|
+
* Respect prefers-reduced-motion (WCAG 2.3.3): near-instantly resolve any
|
|
188
|
+
* animation/transition and disable smooth scrolling for users who opt out of
|
|
189
|
+
* motion. Applies site-wide to the theme-toggle icon swap, the reading-progress
|
|
190
|
+
* bar, tw-animate-css, and any future motion.
|
|
191
|
+
*/
|
|
192
|
+
@media (prefers-reduced-motion: reduce) {
|
|
193
|
+
*,
|
|
194
|
+
::before,
|
|
195
|
+
::after {
|
|
196
|
+
animation-duration: 0.01ms !important;
|
|
197
|
+
animation-iteration-count: 1 !important;
|
|
198
|
+
transition-duration: 0.01ms !important;
|
|
199
|
+
scroll-behavior: auto !important;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/* Long paths, URLs and inline code must wrap on phones (no horizontal page scroll). */
|
|
204
|
+
.prose {
|
|
205
|
+
overflow-wrap: break-word;
|
|
206
|
+
}
|
|
207
|
+
.prose :where(code):not(:where(pre code)),
|
|
208
|
+
.prose :where(a) {
|
|
209
|
+
overflow-wrap: anywhere;
|
|
210
|
+
}
|
|
211
|
+
.prose :where(table) {
|
|
212
|
+
display: block;
|
|
213
|
+
max-width: 100%;
|
|
214
|
+
overflow-x: auto;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/*
|
|
218
|
+
* Mobile overflow guards. Grid items default to min-width:auto, so one long
|
|
219
|
+
* title, badge row or table can push a column past the viewport; long
|
|
220
|
+
* unbreakable strings (paths, URLs, registry keys) spill out of their box.
|
|
221
|
+
* Code blocks keep white-space:pre and still scroll inside their own box.
|
|
222
|
+
*/
|
|
223
|
+
@layer base {
|
|
224
|
+
body {
|
|
225
|
+
overflow-wrap: break-word;
|
|
226
|
+
}
|
|
227
|
+
.grid > * {
|
|
228
|
+
min-width: 0;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/* Cap the layout width with a token instead of the per-breakpoint default. */
|
|
233
|
+
@utility container {
|
|
234
|
+
max-width: min(100%, var(--content-width)) !important;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/*
|
|
238
|
+
* Decorative backgrounds for marketing sections. Built only from tokens, so
|
|
239
|
+
* they follow theme.css and dark mode. Usage: <div className="bg-grid" />.
|
|
240
|
+
*/
|
|
241
|
+
/* The editor's canvas: a dot grid, so the site and the product share one surface. */
|
|
242
|
+
@utility bg-grid {
|
|
243
|
+
background-image: radial-gradient(
|
|
244
|
+
circle at 1px 1px,
|
|
245
|
+
color-mix(in oklab, var(--foreground) 16%, transparent) 1px,
|
|
246
|
+
transparent 0
|
|
247
|
+
);
|
|
248
|
+
background-size: 22px 22px;
|
|
249
|
+
mask-image: radial-gradient(ellipse 70% 60% at 50% 0%, #000 40%, transparent 100%);
|
|
250
|
+
}
|
|
251
|
+
/* A soft white wash from the top, so the hero reads as light rather than flat. */
|
|
252
|
+
@utility bg-glow {
|
|
253
|
+
background-image: radial-gradient(
|
|
254
|
+
ellipse 60% 50% at 50% 0%,
|
|
255
|
+
color-mix(in oklab, var(--background) 90%, transparent),
|
|
256
|
+
transparent
|
|
257
|
+
);
|
|
258
|
+
}
|