@flaier/core 0.1.1 → 0.1.7
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/README.md +57 -10
- package/dist/context.d.ts +1 -0
- package/dist/index.js +118 -9
- package/dist/index.js.map +1 -1
- package/dist/style.css +3 -0
- package/dist/types.d.ts +1 -0
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -1,18 +1,51 @@
|
|
|
1
|
-
# @flaier/core
|
|
1
|
+
# @flaier/core ✨
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@flaier/core)
|
|
4
4
|
[](https://www.npmjs.com/package/@flaier/core)
|
|
5
5
|
[](https://github.com/WeAreRetex/flaier/blob/main/LICENSE)
|
|
6
6
|
|
|
7
|
-
Vue
|
|
7
|
+
**The Vue renderer for explainable flows and architecture walkthroughs.**
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
`@flaier/core` is the main Vue renderer for Flaier.
|
|
10
|
+
|
|
11
|
+
Use it when you want to turn a JSON flow spec or manifest into an interactive walkthrough inside a Vue application, a demo surface, or an internal developer tool.
|
|
12
|
+
|
|
13
|
+
## 🎯 What It Is Used For
|
|
14
|
+
|
|
15
|
+
- Explaining how a request, workflow, or architecture behaves step by step.
|
|
16
|
+
- Embedding interactive system diagrams in Vue apps instead of static screenshots.
|
|
17
|
+
- Rendering AI-generated flow artifacts in a polished UI people can actually inspect.
|
|
18
|
+
- Exporting diagrams for docs, decks, tickets, and async review threads.
|
|
19
|
+
|
|
20
|
+
## 🌟 Features
|
|
21
|
+
|
|
22
|
+
- 🎬 Narrative playback with active-step focus, autoplay, and timeline controls.
|
|
23
|
+
- 🏗 Architecture rendering with zones, inspector panels, and topology-first layouts.
|
|
24
|
+
- 🗂 Manifest support for loading many related flows behind one entry point.
|
|
25
|
+
- 📤 PNG and PDF export for the full diagram, not just the visible viewport.
|
|
26
|
+
- 🎨 Bundled CSS and built-in node renderers so the default experience looks production-ready fast.
|
|
27
|
+
|
|
28
|
+
## 🧭 Common Use Cases
|
|
29
|
+
|
|
30
|
+
**Vue Apps**
|
|
31
|
+
|
|
32
|
+
Embed flows directly inside internal tools, product surfaces, or engineering portals without building a renderer from scratch.
|
|
33
|
+
|
|
34
|
+
**AI-Generated Specs**
|
|
35
|
+
|
|
36
|
+
Point the component at checked-in JSON, generated artifacts, or remote spec URLs and render them in a UI people can step through.
|
|
37
|
+
|
|
38
|
+
**Architecture Reviews**
|
|
39
|
+
|
|
40
|
+
Switch to architecture mode when you need a cleaner system view for discussing boundaries, dependencies, and transitions.
|
|
41
|
+
|
|
42
|
+
## 📦 Install
|
|
10
43
|
|
|
11
44
|
```bash
|
|
12
45
|
npm i @flaier/core
|
|
13
46
|
```
|
|
14
47
|
|
|
15
|
-
## Usage
|
|
48
|
+
## 🚀 Basic Usage
|
|
16
49
|
|
|
17
50
|
```vue
|
|
18
51
|
<script setup lang="ts">
|
|
@@ -25,14 +58,28 @@ import "@flaier/core/style.css";
|
|
|
25
58
|
</template>
|
|
26
59
|
```
|
|
27
60
|
|
|
28
|
-
##
|
|
61
|
+
## 🧠 Accepted Inputs
|
|
62
|
+
|
|
63
|
+
`Flaier` accepts:
|
|
64
|
+
|
|
65
|
+
- a single flow spec object,
|
|
66
|
+
- a single flow spec JSON path or URL,
|
|
67
|
+
- a multi-flow manifest object,
|
|
68
|
+
- or a multi-flow manifest JSON path or URL.
|
|
69
|
+
|
|
70
|
+
That makes `@flaier/core` a good fit whether your specs come from checked-in files, generated artifacts, or remote APIs.
|
|
71
|
+
|
|
72
|
+
## 🧩 When To Use `@flaier/nuxt` Instead
|
|
73
|
+
|
|
74
|
+
Stay with `@flaier/core` when you are in plain Vue.
|
|
75
|
+
|
|
76
|
+
Reach for [`@flaier/nuxt`](https://www.npmjs.com/package/@flaier/nuxt) when you want:
|
|
29
77
|
|
|
30
|
-
-
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
- PNG and PDF export for the full diagram, not just the visible viewport.
|
|
78
|
+
- global Nuxt wrapper components,
|
|
79
|
+
- easy embedding in Nuxt Content or Docus markdown,
|
|
80
|
+
- or docs-site-friendly fullscreen and client-only behavior out of the box.
|
|
34
81
|
|
|
35
|
-
## Links
|
|
82
|
+
## 🔗 Links
|
|
36
83
|
|
|
37
84
|
- Repository: https://github.com/WeAreRetex/flaier
|
|
38
85
|
- Package source: https://github.com/WeAreRetex/flaier/tree/main/packages/core
|
package/dist/context.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export interface FlaierRuntimeContext {
|
|
|
6
6
|
nodes: Ref<FlaierCustomNodeDefinitions>;
|
|
7
7
|
flowOptions: Ref<FlaierFlowOption[]>;
|
|
8
8
|
activeFlowId: Ref<string | null>;
|
|
9
|
+
viewportResetToken: Ref<number>;
|
|
9
10
|
setActiveFlow: (flowId: string) => void;
|
|
10
11
|
}
|
|
11
12
|
export declare const flaierRuntimeKey: InjectionKey<FlaierRuntimeContext>;
|
package/dist/index.js
CHANGED
|
@@ -2406,6 +2406,10 @@ const DEFAULT_DAGRE_NODE_SEP_VERTICAL = 120;
|
|
|
2406
2406
|
const DEFAULT_DAGRE_EDGE_SEP = 30;
|
|
2407
2407
|
const OVERVIEW_ENTER_ZOOM = .52;
|
|
2408
2408
|
const OVERVIEW_EXIT_ZOOM = .62;
|
|
2409
|
+
const NARRATIVE_FOCUS_HORIZONTAL_CONTEXT = 420;
|
|
2410
|
+
const NARRATIVE_FOCUS_VERTICAL_CONTEXT = 320;
|
|
2411
|
+
const NARRATIVE_FOCUS_MIN_ZOOM = .58;
|
|
2412
|
+
const NARRATIVE_FOCUS_MAX_ZOOM = 1.35;
|
|
2409
2413
|
const FLAIER_THEME_STORAGE_KEY = "flaier-ui-theme";
|
|
2410
2414
|
const ARCHITECTURE_ZONE_MIN_CONTENT_PADDING = 44;
|
|
2411
2415
|
const ARCHITECTURE_ZONE_MIN_BOTTOM_PADDING = 88;
|
|
@@ -3627,8 +3631,12 @@ var FlowTimelineRenderer_default = /* @__PURE__ */ defineComponent({
|
|
|
3627
3631
|
timer = null;
|
|
3628
3632
|
}
|
|
3629
3633
|
}
|
|
3630
|
-
function
|
|
3634
|
+
function pauseNarrativePlayback() {
|
|
3635
|
+
if (!isArchitectureMode.value) playing.value = false;
|
|
3636
|
+
}
|
|
3637
|
+
function next(manual = true) {
|
|
3631
3638
|
if (isArchitectureMode.value) return false;
|
|
3639
|
+
if (manual) pauseNarrativePlayback();
|
|
3632
3640
|
if (currentStep.value >= totalSteps.value - 1) return false;
|
|
3633
3641
|
currentStep.value += 1;
|
|
3634
3642
|
return true;
|
|
@@ -3641,15 +3649,17 @@ var FlowTimelineRenderer_default = /* @__PURE__ */ defineComponent({
|
|
|
3641
3649
|
clearTimer();
|
|
3642
3650
|
if (!isPlaying || steps <= 1) return;
|
|
3643
3651
|
timer = setInterval(() => {
|
|
3644
|
-
if (!next()) playing.value = false;
|
|
3652
|
+
if (!next(false)) playing.value = false;
|
|
3645
3653
|
}, interval);
|
|
3646
3654
|
}, { immediate: true });
|
|
3647
|
-
function prev() {
|
|
3655
|
+
function prev(manual = true) {
|
|
3648
3656
|
if (isArchitectureMode.value) return;
|
|
3657
|
+
if (manual) pauseNarrativePlayback();
|
|
3649
3658
|
if (currentStep.value > 0) currentStep.value -= 1;
|
|
3650
3659
|
}
|
|
3651
|
-
function goTo(step) {
|
|
3660
|
+
function goTo(step, manual = true) {
|
|
3652
3661
|
if (isArchitectureMode.value) return;
|
|
3662
|
+
if (manual) pauseNarrativePlayback();
|
|
3653
3663
|
currentStep.value = clampStep(step);
|
|
3654
3664
|
}
|
|
3655
3665
|
function togglePlay() {
|
|
@@ -4364,6 +4374,7 @@ var FlowTimelineRenderer_default = /* @__PURE__ */ defineComponent({
|
|
|
4364
4374
|
function chooseChoice(choiceId) {
|
|
4365
4375
|
const node = activeNode.value;
|
|
4366
4376
|
if (!node) return;
|
|
4377
|
+
pauseNarrativePlayback();
|
|
4367
4378
|
if (!(outgoingNodeKeys.value[node.key] ?? []).includes(choiceId)) return;
|
|
4368
4379
|
selectedBranchByNode.value = {
|
|
4369
4380
|
...selectedBranchByNode.value,
|
|
@@ -4474,6 +4485,33 @@ var FlowTimelineRenderer_default = /* @__PURE__ */ defineComponent({
|
|
|
4474
4485
|
containerHeight.value = element?.clientHeight ?? 0;
|
|
4475
4486
|
containerReady.value = containerWidth.value > 0 && containerHeight.value > 0;
|
|
4476
4487
|
}
|
|
4488
|
+
function waitForAnimationFrame() {
|
|
4489
|
+
return new Promise((resolve) => {
|
|
4490
|
+
if (typeof window === "undefined" || typeof window.requestAnimationFrame !== "function") {
|
|
4491
|
+
setTimeout(resolve, 16);
|
|
4492
|
+
return;
|
|
4493
|
+
}
|
|
4494
|
+
window.requestAnimationFrame(() => {
|
|
4495
|
+
resolve();
|
|
4496
|
+
});
|
|
4497
|
+
});
|
|
4498
|
+
}
|
|
4499
|
+
async function waitForViewportLayoutStability() {
|
|
4500
|
+
await nextTick();
|
|
4501
|
+
await waitForAnimationFrame();
|
|
4502
|
+
await waitForAnimationFrame();
|
|
4503
|
+
updateContainerReady();
|
|
4504
|
+
await nextTick();
|
|
4505
|
+
}
|
|
4506
|
+
function getNarrativeFocusZoom(size) {
|
|
4507
|
+
const width = Math.max(1, containerWidth.value);
|
|
4508
|
+
const height = Math.max(1, containerHeight.value);
|
|
4509
|
+
const focusWidth = Math.max(size.width * 1.35, size.width + NARRATIVE_FOCUS_HORIZONTAL_CONTEXT);
|
|
4510
|
+
const focusHeight = Math.max(size.height * 1.45, size.height + NARRATIVE_FOCUS_VERTICAL_CONTEXT);
|
|
4511
|
+
const zoom = Math.min(width / focusWidth, height / focusHeight);
|
|
4512
|
+
if (!Number.isFinite(zoom)) return 1;
|
|
4513
|
+
return Math.max(NARRATIVE_FOCUS_MIN_ZOOM, Math.min(NARRATIVE_FOCUS_MAX_ZOOM, zoom));
|
|
4514
|
+
}
|
|
4477
4515
|
const sceneStyle = computed(() => ({ height: `${Math.max(containerHeight.value, containerMinHeight.value)}px` }));
|
|
4478
4516
|
onMounted(() => {
|
|
4479
4517
|
if (typeof document !== "undefined") {
|
|
@@ -4496,6 +4534,33 @@ var FlowTimelineRenderer_default = /* @__PURE__ */ defineComponent({
|
|
|
4496
4534
|
const canExportDiagram = computed(() => {
|
|
4497
4535
|
return Boolean(viewportReady.value && diagramBounds.value);
|
|
4498
4536
|
});
|
|
4537
|
+
async function refitViewportAfterContainerChange() {
|
|
4538
|
+
if (!nodes.value.length) return;
|
|
4539
|
+
await waitForViewportLayoutStability();
|
|
4540
|
+
if (!viewportReady.value || nodes.value.length === 0) return;
|
|
4541
|
+
if (isArchitectureMode.value || overviewMode.value) {
|
|
4542
|
+
await Promise.resolve(fitView({
|
|
4543
|
+
duration: isArchitectureMode.value ? 260 : 280,
|
|
4544
|
+
padding: isArchitectureMode.value ? .18 : .3,
|
|
4545
|
+
maxZoom: isArchitectureMode.value ? 1.15 : .95
|
|
4546
|
+
}));
|
|
4547
|
+
return;
|
|
4548
|
+
}
|
|
4549
|
+
const target = narrativeFocusTarget.value;
|
|
4550
|
+
if (!target) {
|
|
4551
|
+
await Promise.resolve(fitView({
|
|
4552
|
+
duration: 280,
|
|
4553
|
+
padding: .3,
|
|
4554
|
+
maxZoom: .95
|
|
4555
|
+
}));
|
|
4556
|
+
return;
|
|
4557
|
+
}
|
|
4558
|
+
await nextTick();
|
|
4559
|
+
await Promise.resolve(setCenter(target.x, target.y, {
|
|
4560
|
+
duration: 280,
|
|
4561
|
+
zoom: target.zoom
|
|
4562
|
+
}));
|
|
4563
|
+
}
|
|
4499
4564
|
watch(canExportDiagram, (canExport) => {
|
|
4500
4565
|
if (!canExport) closeExportMenu();
|
|
4501
4566
|
});
|
|
@@ -4511,6 +4576,7 @@ var FlowTimelineRenderer_default = /* @__PURE__ */ defineComponent({
|
|
|
4511
4576
|
};
|
|
4512
4577
|
return {
|
|
4513
4578
|
signature: [
|
|
4579
|
+
currentStep.value,
|
|
4514
4580
|
node.id,
|
|
4515
4581
|
Math.round(node.position.x),
|
|
4516
4582
|
Math.round(node.position.y),
|
|
@@ -4520,10 +4586,12 @@ var FlowTimelineRenderer_default = /* @__PURE__ */ defineComponent({
|
|
|
4520
4586
|
Math.round(containerHeight.value)
|
|
4521
4587
|
].join(":"),
|
|
4522
4588
|
x: node.position.x + size.width / 2,
|
|
4523
|
-
y: node.position.y + size.height / 2
|
|
4589
|
+
y: node.position.y + size.height / 2,
|
|
4590
|
+
zoom: getNarrativeFocusZoom(size)
|
|
4524
4591
|
};
|
|
4525
4592
|
});
|
|
4526
4593
|
const narrativeFitSignature = ref("");
|
|
4594
|
+
const suppressNarrativeResizeFit = ref(false);
|
|
4527
4595
|
watch([
|
|
4528
4596
|
viewportReady,
|
|
4529
4597
|
isArchitectureMode,
|
|
@@ -4534,6 +4602,10 @@ var FlowTimelineRenderer_default = /* @__PURE__ */ defineComponent({
|
|
|
4534
4602
|
if (!ready || architectureMode || currentNodes.length === 0) return;
|
|
4535
4603
|
const signature = [`${Math.round(width)}x${Math.round(height)}`, ...currentNodes.map((node) => `${node.id}:${Math.round(node.position.x)}:${Math.round(node.position.y)}`)].join("|");
|
|
4536
4604
|
if (signature === narrativeFitSignature.value) return;
|
|
4605
|
+
if (suppressNarrativeResizeFit.value && narrativeFocusTarget.value) {
|
|
4606
|
+
narrativeFitSignature.value = signature;
|
|
4607
|
+
return;
|
|
4608
|
+
}
|
|
4537
4609
|
narrativeFitSignature.value = signature;
|
|
4538
4610
|
nextTick(() => {
|
|
4539
4611
|
fitView({
|
|
@@ -4546,15 +4618,15 @@ var FlowTimelineRenderer_default = /* @__PURE__ */ defineComponent({
|
|
|
4546
4618
|
watch(() => narrativeFocusTarget.value?.signature ?? "", () => {
|
|
4547
4619
|
const target = narrativeFocusTarget.value;
|
|
4548
4620
|
if (!target) return;
|
|
4549
|
-
const zoom = Number.isFinite(viewport.value.zoom) ? viewport.value.zoom : 1;
|
|
4550
4621
|
nextTick(() => {
|
|
4551
4622
|
setCenter(target.x, target.y, {
|
|
4552
4623
|
duration: 280,
|
|
4553
|
-
zoom
|
|
4624
|
+
zoom: target.zoom
|
|
4554
4625
|
});
|
|
4555
4626
|
});
|
|
4556
4627
|
}, { immediate: true });
|
|
4557
4628
|
const architectureFitSignature = ref("");
|
|
4629
|
+
const lastViewportResetToken = ref(0);
|
|
4558
4630
|
watch([
|
|
4559
4631
|
viewportReady,
|
|
4560
4632
|
isArchitectureMode,
|
|
@@ -4574,6 +4646,25 @@ var FlowTimelineRenderer_default = /* @__PURE__ */ defineComponent({
|
|
|
4574
4646
|
});
|
|
4575
4647
|
});
|
|
4576
4648
|
}, { immediate: true });
|
|
4649
|
+
watch([() => runtime.viewportResetToken.value, viewportReady], ([token, ready]) => {
|
|
4650
|
+
if (!ready || token <= lastViewportResetToken.value) return;
|
|
4651
|
+
lastViewportResetToken.value = token;
|
|
4652
|
+
narrativeFitSignature.value = "";
|
|
4653
|
+
architectureFitSignature.value = "";
|
|
4654
|
+
const shouldSuppressNarrativeFit = Boolean(narrativeFocusTarget.value);
|
|
4655
|
+
if (shouldSuppressNarrativeFit) suppressNarrativeResizeFit.value = true;
|
|
4656
|
+
(async () => {
|
|
4657
|
+
try {
|
|
4658
|
+
await refitViewportAfterContainerChange();
|
|
4659
|
+
} finally {
|
|
4660
|
+
if (shouldSuppressNarrativeFit) {
|
|
4661
|
+
await waitForAnimationFrame();
|
|
4662
|
+
await waitForAnimationFrame();
|
|
4663
|
+
suppressNarrativeResizeFit.value = false;
|
|
4664
|
+
}
|
|
4665
|
+
}
|
|
4666
|
+
})();
|
|
4667
|
+
}, { immediate: true });
|
|
4577
4668
|
watch(isArchitectureMode, (architectureMode) => {
|
|
4578
4669
|
if (architectureMode) {
|
|
4579
4670
|
playing.value = false;
|
|
@@ -5106,6 +5197,10 @@ var Flaier_default = /* @__PURE__ */ defineComponent({
|
|
|
5106
5197
|
nodes: {
|
|
5107
5198
|
type: Object,
|
|
5108
5199
|
required: false
|
|
5200
|
+
},
|
|
5201
|
+
viewportResetToken: {
|
|
5202
|
+
type: Number,
|
|
5203
|
+
required: false
|
|
5109
5204
|
}
|
|
5110
5205
|
},
|
|
5111
5206
|
emits: [
|
|
@@ -5126,6 +5221,9 @@ var Flaier_default = /* @__PURE__ */ defineComponent({
|
|
|
5126
5221
|
let sourceRequestId = 0;
|
|
5127
5222
|
let flowRequestId = 0;
|
|
5128
5223
|
const customNodes = computed(() => normalizeFlaierCustomNodes(props.nodes));
|
|
5224
|
+
const viewportResetToken = computed(() => {
|
|
5225
|
+
return typeof props.viewportResetToken === "number" && Number.isFinite(props.viewportResetToken) ? Math.max(0, Math.floor(props.viewportResetToken)) : 0;
|
|
5226
|
+
});
|
|
5129
5227
|
const rendererRegistry = computed(() => createFlaierRendererRegistry({ nodes: customNodes.value }));
|
|
5130
5228
|
provide(flaierRuntimeKey, {
|
|
5131
5229
|
spec: resolvedSpec,
|
|
@@ -5133,6 +5231,7 @@ var Flaier_default = /* @__PURE__ */ defineComponent({
|
|
|
5133
5231
|
nodes: customNodes,
|
|
5134
5232
|
flowOptions,
|
|
5135
5233
|
activeFlowId,
|
|
5234
|
+
viewportResetToken,
|
|
5136
5235
|
setActiveFlow
|
|
5137
5236
|
});
|
|
5138
5237
|
watch([() => props.src, () => props.themeMode], () => {
|
|
@@ -5556,12 +5655,17 @@ var FlaierPanel_default = /* @__PURE__ */ _plugin_vue_export_helper_default(/* @
|
|
|
5556
5655
|
nodes: {
|
|
5557
5656
|
type: Object,
|
|
5558
5657
|
required: false
|
|
5658
|
+
},
|
|
5659
|
+
viewportResetToken: {
|
|
5660
|
+
type: Number,
|
|
5661
|
+
required: false
|
|
5559
5662
|
}
|
|
5560
5663
|
},
|
|
5561
5664
|
setup(__props) {
|
|
5562
5665
|
const props = __props;
|
|
5563
5666
|
const { fullscreen, closeFullscreen, toggleFullscreen } = useFlaierFullscreen();
|
|
5564
5667
|
const fullscreenActive = computed(() => props.fullscreenEnabled && fullscreen.value);
|
|
5668
|
+
const viewportResetToken = ref(0);
|
|
5565
5669
|
const containerStyle = computed(() => {
|
|
5566
5670
|
const minHeight = Number.isFinite(props.minHeight) ? Math.max(280, Math.floor(props.minHeight)) : 420;
|
|
5567
5671
|
return {
|
|
@@ -5573,6 +5677,9 @@ var FlaierPanel_default = /* @__PURE__ */ _plugin_vue_export_helper_default(/* @
|
|
|
5573
5677
|
watch(() => props.fullscreenEnabled, (enabled) => {
|
|
5574
5678
|
if (!enabled && fullscreen.value) closeFullscreen();
|
|
5575
5679
|
});
|
|
5680
|
+
watch(fullscreenActive, () => {
|
|
5681
|
+
viewportResetToken.value += 1;
|
|
5682
|
+
}, { flush: "post" });
|
|
5576
5683
|
return (_ctx, _cache) => {
|
|
5577
5684
|
return openBlock(), createBlock(Teleport, {
|
|
5578
5685
|
to: "body",
|
|
@@ -5592,13 +5699,15 @@ var FlaierPanel_default = /* @__PURE__ */ _plugin_vue_export_helper_default(/* @
|
|
|
5592
5699
|
"auto-play": __props.autoPlay,
|
|
5593
5700
|
interval: __props.interval,
|
|
5594
5701
|
"theme-mode": __props.themeMode,
|
|
5595
|
-
nodes: __props.nodes
|
|
5702
|
+
nodes: __props.nodes,
|
|
5703
|
+
"viewport-reset-token": viewportResetToken.value
|
|
5596
5704
|
}, null, 8, [
|
|
5597
5705
|
"src",
|
|
5598
5706
|
"auto-play",
|
|
5599
5707
|
"interval",
|
|
5600
5708
|
"theme-mode",
|
|
5601
|
-
"nodes"
|
|
5709
|
+
"nodes",
|
|
5710
|
+
"viewport-reset-token"
|
|
5602
5711
|
]), __props.fullscreenEnabled ? (openBlock(), createElementBlock("button", {
|
|
5603
5712
|
key: 0,
|
|
5604
5713
|
type: "button",
|