@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 CHANGED
@@ -1,18 +1,51 @@
1
- # @flaier/core
1
+ # @flaier/core
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/%40flaier%2Fcore?style=flat-square)](https://www.npmjs.com/package/@flaier/core)
4
4
  [![npm downloads](https://img.shields.io/npm/dm/%40flaier%2Fcore?style=flat-square)](https://www.npmjs.com/package/@flaier/core)
5
5
  [![License](https://img.shields.io/github/license/WeAreRetex/flaier?style=flat-square)](https://github.com/WeAreRetex/flaier/blob/main/LICENSE)
6
6
 
7
- Vue components, catalog helpers, and styles for rendering Flaier flow specs in narrative and architecture modes.
7
+ **The Vue renderer for explainable flows and architecture walkthroughs.**
8
8
 
9
- ## Install
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
- ## Features
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
- - Narrative playback with active-step focus, autoplay, and timeline controls.
31
- - Architecture rendering with zones, inspector panels, and export controls.
32
- - Manifest support for loading many related flows behind one entry point.
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 next() {
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",