@poveste/app 0.13.1 → 0.15.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.
Files changed (132) hide show
  1. package/dist/bundled/App.vue_vue_type_script_setup_true_lang.js +1 -1
  2. package/dist/bundled/app.css +31 -31
  3. package/dist/bundled/components/HomeView.vue_vue_type_script_setup_true_lang.js +1 -1
  4. package/dist/bundled/components/app/AppLogo.vue_vue_type_script_setup_true_lang.js +2 -2
  5. package/dist/bundled/components/app/Breadcrumb.js +1 -1
  6. package/dist/bundled/components/app/Breadcrumb.vue_vue_type_script_setup_true_lang.js +2 -2
  7. package/dist/bundled/components/app/TopBar.vue_vue_type_script_setup_true_lang.js +4 -3
  8. package/dist/bundled/components/app/TopBarChipItem.js +1 -1
  9. package/dist/bundled/components/base/BaseCheckbox.vue_vue_type_script_setup_true_lang.js +1 -1
  10. package/dist/bundled/components/base/BaseEmpty.js +2 -2
  11. package/dist/bundled/components/base/BaseOverflowMenu.vue_vue_type_script_setup_true_lang.js +18 -15
  12. package/dist/bundled/components/base/BaseSelect.vue_vue_type_script_setup_true_lang.js +158 -26
  13. package/dist/bundled/components/base/BaseSplitPane.js +1 -1
  14. package/dist/bundled/components/base/BaseSplitPane.vue_vue_type_script_setup_true_lang.js +6 -4
  15. package/dist/bundled/components/command/CommandPrompts.vue_vue_type_script_setup_true_lang.js +1 -1
  16. package/dist/bundled/components/command/CommandPromptsModal.vue_vue_type_script_setup_true_lang.js +4 -3
  17. package/dist/bundled/components/command/PromptSelect.vue_vue_type_script_setup_true_lang.js +2 -2
  18. package/dist/bundled/components/command/PromptText.vue_vue_type_script_setup_true_lang.js +1 -1
  19. package/dist/bundled/components/panel/ControlsComponentPropItem.vue_vue_type_script_setup_true_lang.js +20 -14
  20. package/dist/bundled/components/panel/PaneTabs.vue_vue_type_script_setup_true_lang.js +6 -6
  21. package/dist/bundled/components/panel/StatePresets.vue_vue_type_script_setup_true_lang.js +94 -55
  22. package/dist/bundled/components/panel/StoryControls.vue_vue_type_script_setup_true_lang.js +1 -1
  23. package/dist/bundled/components/panel/StoryDocs.vue_vue_type_script_setup_true_lang.js +3 -1
  24. package/dist/bundled/components/panel/StoryEvent.vue_vue_type_script_setup_true_lang.js +5 -1
  25. package/dist/bundled/components/panel/StoryEvents.vue_vue_type_script_setup_true_lang.js +2 -1
  26. package/dist/bundled/components/panel/StorySidePanel.vue_vue_type_script_setup_true_lang.js +8 -6
  27. package/dist/bundled/components/panel/StorySourceCode.js +1 -1
  28. package/dist/bundled/components/panel/StorySourceCode.vue_vue_type_script_setup_true_lang.js +8 -6
  29. package/dist/bundled/components/search/SearchItemContent.vue_vue_type_script_setup_true_lang.js +2 -1
  30. package/dist/bundled/components/search/SearchPane.vue_vue_type_script_setup_true_lang.js +16 -9
  31. package/dist/bundled/components/story/GenericMountStory.vue_vue_type_script_setup_true_lang.js +2 -1
  32. package/dist/bundled/components/story/GenericRenderStory.vue_vue_type_script_setup_true_lang.js +2 -1
  33. package/dist/bundled/components/story/StoryResponsivePreview.js +1 -1
  34. package/dist/bundled/components/story/StoryResponsivePreview.vue_vue_type_script_setup_true_lang.js +23 -8
  35. package/dist/bundled/components/story/StoryVariantGrid.js +1 -1
  36. package/dist/bundled/components/story/StoryVariantGrid.vue_vue_type_script_setup_true_lang.js +15 -10
  37. package/dist/bundled/components/story/StoryVariantGridItem.js +1 -1
  38. package/dist/bundled/components/story/StoryVariantGridItem.vue_vue_type_script_setup_true_lang.js +3 -3
  39. package/dist/bundled/components/story/StoryVariantSinglePreviewRemote.vue_vue_type_script_setup_true_lang.js +9 -6
  40. package/dist/bundled/components/story/StoryView.vue_vue_type_script_setup_true_lang.js +7 -3
  41. package/dist/bundled/components/story/StoryViewer.js +1 -1
  42. package/dist/bundled/components/story/StoryViewer.vue_vue_type_script_setup_true_lang.js +3 -3
  43. package/dist/bundled/components/toolbar/ToolbarBackground.vue_vue_type_script_setup_true_lang.js +1 -1
  44. package/dist/bundled/components/toolbar/ToolbarResponsiveSize.vue_vue_type_script_setup_true_lang.js +17 -16
  45. package/dist/bundled/components/toolbar/ToolbarTitle.js +1 -1
  46. package/dist/bundled/components/toolbar/ToolbarTitle.vue_vue_type_script_setup_true_lang.js +1 -1
  47. package/dist/bundled/components/tree/StoryGroup.vue_vue_type_script_setup_true_lang.js +2 -2
  48. package/dist/bundled/components/tree/StoryList.vue_vue_type_script_setup_true_lang.js +3 -3
  49. package/dist/bundled/components/tree/StoryListFolder.js +1 -1
  50. package/dist/bundled/components/tree/StoryListFolder.vue_vue_type_script_setup_true_lang.js +3 -3
  51. package/dist/bundled/components/tree/StoryListItem.js +1 -1
  52. package/dist/bundled/components/tree/StoryListItem.vue_vue_type_script_setup_true_lang.js +3 -3
  53. package/dist/bundled/plugin.js +4 -3
  54. package/dist/bundled/sandbox.js +5 -4
  55. package/dist/bundled/stores/folder.js +1 -1
  56. package/dist/bundled/stores/story.js +2 -2
  57. package/dist/bundled/util/commands.js +21 -10
  58. package/dist/bundled/util/dark.js +1 -1
  59. package/dist/bundled/util/docs.js +2 -1
  60. package/dist/bundled/util/events.js +1 -1
  61. package/dist/bundled/util/grid-slots.js +2 -2
  62. package/dist/bundled/util/icons.generated.js +0 -1
  63. package/dist/bundled/util/keyboard.js +9 -14
  64. package/dist/bundled/util/mapping.js +3 -3
  65. package/dist/bundled/util/preview-settings.js +1 -1
  66. package/dist/bundled/util/render-tracker.js +33 -0
  67. package/dist/bundled/util/variant.js +12 -2
  68. package/dist/style.css +23 -1
  69. package/package.json +14 -18
  70. package/src/__tests__/render-tracker.spec.ts +38 -0
  71. package/src/app/App.vue +7 -4
  72. package/src/app/components/HomeView.vue +1 -1
  73. package/src/app/components/app/AppLogo.vue +6 -2
  74. package/src/app/components/app/Breadcrumb.vue +2 -1
  75. package/src/app/components/app/TopBar.vue +7 -2
  76. package/src/app/components/app/TopBarChipItem.vue +1 -1
  77. package/src/app/components/base/BaseCheckbox.vue +1 -1
  78. package/src/app/components/base/BaseEmpty.vue +1 -1
  79. package/src/app/components/base/BaseListItemLink.vue +4 -2
  80. package/src/app/components/base/BaseOverflowMenu.vue +25 -18
  81. package/src/app/components/base/BaseSelect.vue +171 -15
  82. package/src/app/components/base/BaseSplitPane.vue +11 -6
  83. package/src/app/components/command/CommandPrompts.vue +1 -1
  84. package/src/app/components/command/CommandPromptsModal.vue +1 -0
  85. package/src/app/components/command/PromptSelect.vue +3 -2
  86. package/src/app/components/command/PromptText.vue +7 -3
  87. package/src/app/components/panel/ControlsComponentPropItem.vue +26 -15
  88. package/src/app/components/panel/PaneTabs.vue +6 -6
  89. package/src/app/components/panel/StatePresets.vue +125 -68
  90. package/src/app/components/panel/StoryControls.vue +1 -1
  91. package/src/app/components/panel/StoryDocs.vue +3 -1
  92. package/src/app/components/panel/StoryEvent.vue +7 -2
  93. package/src/app/components/panel/StoryEvents.vue +4 -1
  94. package/src/app/components/panel/StorySidePanel.vue +3 -1
  95. package/src/app/components/panel/StorySourceCode.vue +12 -8
  96. package/src/app/components/search/SearchItemContent.vue +2 -1
  97. package/src/app/components/search/SearchPane.vue +16 -8
  98. package/src/app/components/story/GenericMountStory.vue +2 -1
  99. package/src/app/components/story/GenericRenderStory.vue +2 -1
  100. package/src/app/components/story/StoryResponsivePreview.vue +36 -7
  101. package/src/app/components/story/StoryVariantGrid.vue +33 -16
  102. package/src/app/components/story/StoryVariantGridItem.vue +1 -1
  103. package/src/app/components/story/StoryVariantSinglePreviewRemote.vue +14 -8
  104. package/src/app/components/story/StoryView.vue +10 -4
  105. package/src/app/components/story/StoryViewer.vue +3 -3
  106. package/src/app/components/toolbar/ToolbarBackground.vue +1 -1
  107. package/src/app/components/toolbar/ToolbarResponsiveSize.vue +23 -3
  108. package/src/app/components/toolbar/ToolbarTitle.vue +2 -2
  109. package/src/app/components/tree/StoryGroup.vue +2 -2
  110. package/src/app/components/tree/StoryList.vue +2 -2
  111. package/src/app/components/tree/StoryListFolder.vue +2 -2
  112. package/src/app/components/tree/StoryListItem.vue +1 -1
  113. package/src/app/plugin.ts +7 -3
  114. package/src/app/sandbox.ts +9 -3
  115. package/src/app/stores/folder.ts +1 -1
  116. package/src/app/stores/story.ts +2 -2
  117. package/src/app/types.ts +6 -6
  118. package/src/app/util/commands.ts +27 -17
  119. package/src/app/util/dark.ts +1 -1
  120. package/src/app/util/docs.ts +2 -1
  121. package/src/app/util/env.ts +0 -1
  122. package/src/app/util/events.ts +6 -5
  123. package/src/app/util/grid-slots.ts +2 -2
  124. package/src/app/util/icons.generated.ts +1 -1
  125. package/src/app/util/keyboard.ts +10 -11
  126. package/src/app/util/mapping.ts +3 -3
  127. package/src/app/util/preview-settings.ts +1 -1
  128. package/src/app/util/render-tracker.ts +31 -0
  129. package/src/app/util/scroll.ts +5 -1
  130. package/src/app/util/state.ts +4 -4
  131. package/src/app/util/variant.ts +16 -3
  132. package/src/app/components/base/BaseCopyIcon.vue +0 -34
@@ -2,27 +2,38 @@ import { router } from "../router.js";
2
2
  import { useStoryStore } from "../stores/story.js";
3
3
  import { openInEditor } from "./open-in-editor.js";
4
4
  //#region src/app/util/commands.ts
5
+ /**
6
+ * The path "Open file in editor" should open, or undefined when there is none.
7
+ *
8
+ * `Story.file` is optional, and `let file: string` used to be assigned
9
+ * `story.file?.filePath` in both branches — so a story without a file produced
10
+ * `file: undefined`, and `openInEditor` sent
11
+ * `__open-in-editor?file=undefined` to the dev server: a request to open a file
12
+ * literally named "undefined", with nothing said in the UI (#667).
13
+ *
14
+ * A docs-only story prefers its markdown, falling back to the component file
15
+ * when the story has both.
16
+ */
17
+ function editableFilePath(story) {
18
+ const file = story?.file;
19
+ if (!file) return void 0;
20
+ return story?.docsOnly ? file.docsFilePath ?? file.filePath : file.filePath;
21
+ }
5
22
  var builtinCommands = [{
6
23
  id: "builtin:open-in-editor",
7
24
  label: "Open file in editor",
8
25
  icon: "carbon:script-reference",
9
- showIf: ({ route }) => route.name === "story" && !!useStoryStore().currentStory,
10
- getParams: () => {
11
- const story = useStoryStore().currentStory;
12
- let file;
13
- if (story.docsOnly) file = story.file?.docsFilePath ?? story.file?.filePath;
14
- else file = story.file?.filePath;
15
- return { file };
16
- },
26
+ showIf: ({ route }) => route.name === "story" && !!editableFilePath(useStoryStore().currentStory),
27
+ getParams: () => ({ file: editableFilePath(useStoryStore().currentStory) }),
17
28
  clientAction: ({ file }) => {
18
- openInEditor(file);
29
+ if (file) openInEditor(file);
19
30
  }
20
31
  }, {
21
32
  id: "builtin:poveste-docs",
22
33
  label: "Open Poveste Documentation",
23
34
  icon: "carbon:help",
24
35
  clientAction: () => {
25
- window.open("https://poveste.dev/guide/getting-started.html", "_blank");
36
+ window.open("https://poveste.dev/guide/getting-started", "_blank");
26
37
  }
27
38
  }];
28
39
  function executeCommand(command, params) {
@@ -4,7 +4,7 @@ import { watch } from "@poveste/vendors/vue";
4
4
  //#region src/app/util/dark.ts
5
5
  var isDark = useDark({
6
6
  valueDark: "ptw-dark",
7
- initialValue: povesteConfig.theme.defaultColorScheme,
7
+ ...povesteConfig.theme.defaultColorScheme ? { initialValue: povesteConfig.theme.defaultColorScheme } : {},
8
8
  storageKey: "poveste-color-scheme",
9
9
  storage: povesteConfig.theme.storeColorScheme ? localStorage : sessionStorage
10
10
  });
@@ -7,7 +7,8 @@ async function getSourceCode(story, variant) {
7
7
  const source = variant.slots?.().source()[0].children;
8
8
  if (source) return unindent(source);
9
9
  } else {
10
- const clientPlugin = clientSupportPlugins[story.file?.supportPluginId];
10
+ const supportPluginId = story.file?.supportPluginId;
11
+ const clientPlugin = supportPluginId ? clientSupportPlugins[supportPluginId] : void 0;
11
12
  if (clientPlugin) return (await clientPlugin()).generateSourceCode(variant);
12
13
  }
13
14
  const sourceLoader = story.file?.source;
@@ -19,7 +19,7 @@ async function logEvent(name, argument) {
19
19
  }
20
20
  function stringifyEvent(e) {
21
21
  const obj = {};
22
- for (const k in e) obj[k] = e[k];
22
+ for (const k in e) obj[k] = Reflect.get(e, k);
23
23
  return JSON.stringify(obj, (k, v) => {
24
24
  if (v instanceof Node) return "Node";
25
25
  if (v instanceof Window) return "Window";
@@ -11,7 +11,7 @@ function assignSlots(slots, storyId, visible) {
11
11
  const next = slots.map((slot) => slot.storyId === storyId && position.has(slot.variant.id) ? {
12
12
  ...slot,
13
13
  visible: true,
14
- order: position.get(slot.variant.id)
14
+ order: position.get(slot.variant.id) ?? 0
15
15
  } : {
16
16
  ...slot,
17
17
  visible: false,
@@ -57,7 +57,7 @@ function trimSlots(slots) {
57
57
  next = kept;
58
58
  }
59
59
  let end = next.length;
60
- while (end > 0 && !next[end - 1].visible) end--;
60
+ while (end > 0 && !next[end - 1]?.visible) end--;
61
61
  return end === next.length ? next : next.slice(0, end);
62
62
  }
63
63
  //#endregion
@@ -6,7 +6,6 @@ var iconCollections = [
6
6
  "add-alt": { "body": "<path fill=\"currentColor\" d=\"M16 4c6.6 0 12 5.4 12 12s-5.4 12-12 12S4 22.6 4 16S9.4 4 16 4m0-2C8.3 2 2 8.3 2 16s6.3 14 14 14s14-6.3 14-14S23.7 2 16 2\"/><path fill=\"currentColor\" d=\"M24 15h-7V8h-2v7H8v2h7v7h2v-7h7z\"/>" },
7
7
  "area": { "body": "<path fill=\"currentColor\" d=\"M27 22.142V9.858A3.992 3.992 0 1 0 22.142 5H9.858A3.992 3.992 0 1 0 5 9.858v12.284A3.992 3.992 0 1 0 9.858 27h12.284A3.992 3.992 0 1 0 27 22.142M26 4a2 2 0 1 1-2 2a2 2 0 0 1 2-2M4 6a2 2 0 1 1 2 2a2 2 0 0 1-2-2m2 22a2 2 0 1 1 2-2a2 2 0 0 1-2 2m16.142-3H9.858A4 4 0 0 0 7 22.142V9.858A4 4 0 0 0 9.858 7h12.284A4 4 0 0 0 25 9.858v12.284A3.99 3.99 0 0 0 22.142 25M26 28a2 2 0 1 1 2-2a2.003 2.003 0 0 1-2 2\"/>" },
8
8
  "audio-console": { "body": "<path fill=\"currentColor\" d=\"M28 14.005h-1v-12h-2v12h-1a2 2 0 0 0-2 2v2a2 2 0 0 0 2 2h1v10h2v-10h1a2.003 2.003 0 0 0 2-2v-2a2 2 0 0 0-2-2m0 4h-4v-2h4Zm-10-12h-1v-4h-2v4h-1a2 2 0 0 0-2 2v2a2 2 0 0 0 2 2h1v18h2v-18h1a2 2 0 0 0 2-2v-2a2 2 0 0 0-2-2m0 4h-4v-2h4Zm-10 10H7v-18H5v18H4a2 2 0 0 0-2 2v2a2 2 0 0 0 2 2h1v4h2v-4h1a2 2 0 0 0 2-2v-2a2 2 0 0 0-2-2m0 4H4v-2h4Z\"/>" },
9
- "bring-forward": { "body": "<path fill=\"currentColor\" d=\"M12 15h-2v-3a2 2 0 0 1 2-2h3v2h-3Zm3 15h-3a2 2 0 0 1-2-2v-3h2v3h3Zm3-2h4v2h-4zm10 2h-3v-2h3v-3h2v3a2 2 0 0 1-2 2M10 18h2v4h-2zm18 0h2v4h-2zm2-3h-2v-3h-3v-2h3a2 2 0 0 1 2 2Zm-12-5h4v2h-4z\"/><path fill=\"currentColor\" d=\"M8 22H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v4h-2V4H4v16h4Z\"/>" },
10
9
  "caret-down": { "body": "<path fill=\"currentColor\" d=\"m24 12l-8 10l-8-10z\"/>" },
11
10
  "caret-right": { "body": "<path fill=\"currentColor\" d=\"m12 8l10 8l-10 8z\"/>" },
12
11
  "checkbox": { "body": "<path fill=\"currentColor\" d=\"M26 4H6a2 2 0 0 0-2 2v20a2 2 0 0 0 2 2h20a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2M6 26V6h20v20Z\"/>" },
@@ -24,30 +24,24 @@ var pressedKeys = /* @__PURE__ */ new Set();
24
24
  var trackedWindows = shallowRef([]);
25
25
  function bindTracking(target) {
26
26
  const onKeydown = (event) => {
27
- for (const i in modifiers) {
28
- const mod = modifiers[i];
29
- if (mod.key === event.key) {
30
- mod.pressed = true;
31
- return;
32
- }
27
+ for (const mod of Object.values(modifiers)) if (mod.key === event.key) {
28
+ mod.pressed = true;
29
+ return;
33
30
  }
34
31
  pressedKeys.add(event.key.toLocaleLowerCase());
35
32
  };
36
33
  const onKeyup = (event) => {
37
34
  requestAnimationFrame(() => {
38
35
  pressedKeys.clear();
39
- for (const i in modifiers) {
40
- const mod = modifiers[i];
41
- if (mod.key === event.key) {
42
- mod.pressed = false;
43
- break;
44
- }
36
+ for (const mod of Object.values(modifiers)) if (mod.key === event.key) {
37
+ mod.pressed = false;
38
+ break;
45
39
  }
46
40
  });
47
41
  };
48
42
  const onBlur = () => {
49
43
  pressedKeys.clear();
50
- for (const i in modifiers) modifiers[i].pressed = false;
44
+ for (const mod of Object.values(modifiers)) mod.pressed = false;
51
45
  };
52
46
  target.addEventListener("keydown", onKeydown);
53
47
  target.addEventListener("keyup", onKeyup);
@@ -88,7 +82,8 @@ function isMatchingShortcut(shortcut) {
88
82
  function isMatchingCombination(combination) {
89
83
  const splitted = combination.split("+").map((key) => key.trim());
90
84
  const targetKey = splitted.pop();
91
- for (const mod in modifiers) if (splitted.includes(mod) !== modifiers[mod].pressed) return false;
85
+ if (!targetKey) return false;
86
+ for (const [name, mod] of Object.entries(modifiers)) if (splitted.includes(name) !== mod.pressed) return false;
92
87
  return pressedKeys.has(targetKey);
93
88
  }
94
89
  function formatKey(key) {
@@ -20,7 +20,7 @@ function mapFile(file, existingFile) {
20
20
  file: markRaw(result),
21
21
  variants: file.story.variants.map((v) => mapVariant(v, existingFile.story.variants.find((item) => item.id === v.id)))
22
22
  };
23
- else if (key !== "component") result[key] = file[key];
23
+ else if (key !== "component") Reflect.set(result, key, Reflect.get(file, key));
24
24
  } else result = {
25
25
  ...file,
26
26
  component: markRaw(file.component),
@@ -38,14 +38,14 @@ function mapVariant(variant, existingVariant) {
38
38
  let result;
39
39
  if (existingVariant) {
40
40
  result = existingVariant;
41
- for (const key in variant) if (!copiedFromExistingVariant.includes(key)) result[key] = variant[key];
41
+ for (const key in variant) if (!copiedFromExistingVariant.includes(key)) Reflect.set(result, key, Reflect.get(variant, key));
42
42
  } else result = {
43
43
  ...variant,
44
44
  state: reactive({
45
45
  _hPropState: {},
46
46
  _hPropDefs: {}
47
47
  }),
48
- setupApp: null,
48
+ setupApp: void 0,
49
49
  slots: () => ({
50
50
  default: void 0,
51
51
  controls: void 0,
@@ -24,7 +24,7 @@ function applyPreviewSettings(settings) {
24
24
  if (povesteConfig.autoApplyContrastColor) document.documentElement.style.color = contrastColor;
25
25
  }
26
26
  function getContrastColor(setting) {
27
- return povesteConfig.backgroundPresets.find((preset) => preset.color === setting.backgroundColor)?.contrastColor ?? "unset";
27
+ return povesteConfig.backgroundPresets?.find((preset) => preset.color === setting.backgroundColor)?.contrastColor ?? "unset";
28
28
  }
29
29
  //#endregion
30
30
  export { PREVIEW_SETTINGS_STORAGE_KEY, applyPreviewSettings, getContrastColor, loadStoredPreviewSettings, receivedSettings };
@@ -0,0 +1,33 @@
1
+ //#region src/app/util/render-tracker.ts
2
+ /**
3
+ * Whether the render now under way inside a realm reported a throw.
4
+ *
5
+ * The host cannot see inside the realm, so "did this render succeed" has to be
6
+ * read off the two named messages it does see. A throw reaches
7
+ * `app.config.errorHandler` during mount and `SANDBOX_READY` is emitted after
8
+ * `await host.mount()` returns, so the order is always error-then-ready — which
9
+ * makes a ready with nothing recorded since the render began a clean render.
10
+ *
11
+ * Clearing on the story and variant ids changing instead cleared the pair being
12
+ * navigated *to*, so arriving at a broken story wiped its marker, and the fixed
13
+ * story the comment was about was never reached at all: a file edited in place
14
+ * keeps its ids, so that watcher did not fire on the one path it named (#597).
15
+ */
16
+ function createRenderTracker() {
17
+ let threw = false;
18
+ return {
19
+ /** A new render is starting: what the previous one reported no longer holds. */
20
+ begin() {
21
+ threw = false;
22
+ },
23
+ threw() {
24
+ threw = true;
25
+ },
26
+ /** True when the render that has just reported ready said nothing before it. */
27
+ wasClean() {
28
+ return !threw;
29
+ }
30
+ };
31
+ }
32
+ //#endregion
33
+ export { createRenderTracker };
@@ -8,14 +8,24 @@ import { useRoute } from "@poveste/vendors/vue-router";
8
8
  * has to tell "a variant is on its way" from "this story is waiting for the
9
9
  * reader to pick one" needs the same answer it will give — hence one predicate
10
10
  * rather than two copies of the condition (#328).
11
+ *
12
+ * Any story with variants, deliberately. It used to require a remembered choice
13
+ * or exactly one variant, which made `StoryView`'s `variants[0]` fallback
14
+ * unreachable: a story with several variants and no history satisfied neither
15
+ * clause, so the first visit of a session showed an empty panel and every visit
16
+ * after it worked, because the first one recorded the history (#724).
17
+ *
18
+ * No layout clause. Grid stories already auto-selected on revisit through the
19
+ * remembered choice, so excluding them here would auto-open them on the second
20
+ * visit and never the first — this defect, made permanent for one layout.
11
21
  */
12
22
  function autoSelectsVariant(story) {
13
- return !!story && (!!story.lastSelectedVariant || story.variants.length === 1);
23
+ return !!story && story.variants.length > 0;
14
24
  }
15
25
  function useCurrentVariantRoute(variant) {
16
26
  const route = useRoute();
17
27
  return {
18
- isActive: computed(() => route.query.variantId === variant.value.id),
28
+ isActive: computed(() => route.query["variantId"] === variant.value.id),
19
29
  targetRoute: computed(() => ({
20
30
  ...route,
21
31
  query: {
package/dist/style.css CHANGED
@@ -274,7 +274,7 @@
274
274
  inherits: false;
275
275
  initial-value: solid;
276
276
  }
277
- .__poveste-json-code[data-v-e4c89a1b] .cm-editor {
277
+ .__poveste-json-code[data-v-68225145] .cm-editor {
278
278
  min-width: 280px;
279
279
  height: 100%;
280
280
  }
@@ -1012,6 +1012,9 @@
1012
1012
  .mb-8 {
1013
1013
  margin-bottom: calc(var(--spacing) * 8);
1014
1014
  }
1015
+ .mb-\[10px\] {
1016
+ margin-bottom: 10px;
1017
+ }
1015
1018
  .ml-4 {
1016
1019
  margin-left: calc(var(--spacing) * 4);
1017
1020
  }
@@ -1670,6 +1673,9 @@
1670
1673
  .stroke-2 {
1671
1674
  stroke-width: 2;
1672
1675
  }
1676
+ .p-0 {
1677
+ padding: 0px;
1678
+ }
1673
1679
  .p-1 {
1674
1680
  padding: var(--spacing);
1675
1681
  }
@@ -1927,6 +1933,16 @@
1927
1933
  outline-style: var(--tw-outline-style);
1928
1934
  outline-width: 1px;
1929
1935
  }
1936
+ .outline-2 {
1937
+ outline-style: var(--tw-outline-style);
1938
+ outline-width: 2px;
1939
+ }
1940
+ .-outline-offset-2 {
1941
+ outline-offset: calc(2px * -1);
1942
+ }
1943
+ .outline-primary-500 {
1944
+ outline-color: var(--color-primary-500);
1945
+ }
1930
1946
  .blur {
1931
1947
  --tw-blur: blur(8px);
1932
1948
  filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
@@ -2022,6 +2038,9 @@
2022
2038
  .group-focus-within\:visible:is(:where(.group):focus-within *) {
2023
2039
  visibility: visible;
2024
2040
  }
2041
+ .group-focus-within\:flex:is(:where(.group):focus-within *) {
2042
+ display: flex;
2043
+ }
2025
2044
  @media (hover: hover) {
2026
2045
  .group-hover\:flex:is(:where(.group):hover *) {
2027
2046
  display: flex;
@@ -2166,6 +2185,9 @@
2166
2185
  .focus-visible\:border-primary-500:focus-visible {
2167
2186
  border-color: var(--color-primary-500);
2168
2187
  }
2188
+ .focus-visible\:opacity-100:focus-visible {
2189
+ opacity: 100%;
2190
+ }
2169
2191
  .active\:bg-gray-600\/50:active {
2170
2192
  background-color: color-mix(in srgb, rgb(var(--_poveste-color-gray-600)) 50%, transparent);
2171
2193
  @supports (color: color-mix(in lab, red, red)) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@poveste/app",
3
3
  "type": "module",
4
- "version": "0.13.1",
4
+ "version": "0.15.0",
5
5
  "description": "Pre-bundled UI for Poveste",
6
6
  "author": {
7
7
  "name": "Sorin Gitlan",
@@ -23,13 +23,9 @@
23
23
  "access": "public"
24
24
  },
25
25
  "exports": {
26
- ".": {
27
- "types": "./dist/index.d.ts",
28
- "default": "./dist/bundle-index.js"
29
- },
26
+ ".": "./dist/bundle-index.js",
30
27
  "./*": "./*"
31
28
  },
32
- "types": "./src/index.ts",
33
29
  "files": [
34
30
  "dist",
35
31
  "src"
@@ -38,11 +34,11 @@
38
34
  "node": "^22.22.2 || ^24.15.0 || >=26.0.0"
39
35
  },
40
36
  "dependencies": {
41
- "@poveste/controls": "^0.13.1",
42
- "@poveste/shared": "^0.13.1",
43
- "@poveste/vendors": "^0.13.1",
37
+ "@poveste/controls": "^0.15.0",
38
+ "@poveste/shared": "^0.15.0",
39
+ "@poveste/vendors": "^0.15.0",
44
40
  "fuse.js": "^7.0.0",
45
- "shiki": "^3.0.0"
41
+ "shiki": "^4.4.3"
46
42
  },
47
43
  "devDependencies": {
48
44
  "@fontsource/noto-sans-display": "^5.3.0",
@@ -56,19 +52,19 @@
56
52
  "@types/fs-extra": "^11.0.4",
57
53
  "@types/node": "^22.10.1",
58
54
  "@vitejs/plugin-vue": "^6.0.0",
59
- "chokidar": "^4.0.1",
60
- "concurrently": "^9.1.0",
55
+ "chokidar": "^5.0.0",
56
+ "concurrently": "^10.0.5",
61
57
  "fs-extra": "^11.2.0",
62
- "globby": "^14.0.2",
63
- "pathe": "^1.1.2",
58
+ "globby": "^16.2.4",
59
+ "pathe": "^2.0.3",
64
60
  "postcss": "^8.5.23",
65
- "postcss-cli": "^11.0.1",
61
+ "postcss-cli": "^12.0.0",
66
62
  "tailwindcss": "^4.3.3",
67
- "typescript": "5.6.3",
63
+ "typescript": "6.0.3",
68
64
  "vite": "^8.2.0",
69
- "vitest": "^4.1.10",
65
+ "vitest": "^5.0.0",
70
66
  "vue": "^3.5.26",
71
- "vue-tsc": "^2.1.10"
67
+ "vue-tsc": "^3.3.11"
72
68
  },
73
69
  "scripts": {
74
70
  "build": "pnpm run icons && rimraf dist && vite build && pnpm run build:css && node ./scripts/copy.mjs",
@@ -0,0 +1,38 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { createRenderTracker } from '../app/util/render-tracker.js'
3
+
4
+ describe('createRenderTracker', () => {
5
+ it('calls a render clean when nothing was reported during it', () => {
6
+ const render = createRenderTracker()
7
+
8
+ render.begin()
9
+
10
+ expect(render.wasClean()).toBe(true)
11
+ })
12
+
13
+ it('does not call a render clean when a throw was reported during it', () => {
14
+ const render = createRenderTracker()
15
+ render.begin()
16
+
17
+ render.threw()
18
+
19
+ expect(render.wasClean()).toBe(false)
20
+ })
21
+
22
+ // The half #597 is about. Without it a story that threw once stays marked for
23
+ // the life of the tab: every later render inherits the first one's verdict,
24
+ // so the marker the old comment promised to lift on a fix never lifts.
25
+ it('forgets the previous render when a new one begins', () => {
26
+ const render = createRenderTracker()
27
+ render.begin()
28
+ render.threw()
29
+
30
+ render.begin()
31
+
32
+ expect(render.wasClean()).toBe(true)
33
+ })
34
+
35
+ it('treats a render that has reported nothing yet as clean', () => {
36
+ expect(createRenderTracker().wasClean()).toBe(true)
37
+ })
38
+ })
package/src/app/App.vue CHANGED
@@ -5,7 +5,7 @@ export default {
5
5
  </script>
6
6
 
7
7
  <script lang="ts" setup>
8
- import type { StoryFile, Tree } from './types'
8
+ import type { Story, StoryFile, Tree } from './types'
9
9
  import { useTitle } from '@vueuse/core'
10
10
  import { onUpdate, files as rawFiles, tree as rawTree } from 'virtual:$poveste-stories'
11
11
  import { computed, onMounted, ref, watch } from 'vue'
@@ -29,7 +29,7 @@ import { onKeyboardShortcut } from './util/keyboard'
29
29
  import { mapFile } from './util/mapping'
30
30
  import { isMobile } from './util/responsive'
31
31
 
32
- const files = ref<StoryFile[]>(rawFiles.map(file => mapFile(file)))
32
+ const files = ref<StoryFile[]>(rawFiles.map((file: StoryFile) => mapFile(file)))
33
33
  const tree = ref<Tree>(rawTree)
34
34
 
35
35
  onUpdate((newFiles: StoryFile[], newTree: Tree) => {
@@ -41,8 +41,11 @@ onUpdate((newFiles: StoryFile[], newTree: Tree) => {
41
41
  tree.value = newTree
42
42
  })
43
43
 
44
- const stories = computed(() => files.value.reduce((acc, file) => {
45
- acc.push(file.story)
44
+ // The accumulator needs a type: `[]` alone infers `never[]`, so the push was an
45
+ // error about the seed rather than about the story. A file whose story has not
46
+ // been collected yet contributes nothing.
47
+ const stories = computed(() => files.value.reduce<Story[]>((acc, file) => {
48
+ if (file.story) acc.push(file.story)
46
49
  return acc
47
50
  }, []))
48
51
 
@@ -5,7 +5,7 @@ import { useStoryStore } from '../stores/story'
5
5
  import { customLogos, povesteConfig } from '../util/config'
6
6
  import HomeCounter from './app/HomeCounter.vue'
7
7
 
8
- const logoUrl = computed(() => povesteConfig.theme?.logo?.square ? customLogos.square : PovesteLogo)
8
+ const logoUrl = computed(() => povesteConfig.theme?.logo?.square ? customLogos?.square ?? PovesteLogo : PovesteLogo)
9
9
  const storyStore = useStoryStore()
10
10
 
11
11
  const stats = computed(() => {
@@ -6,11 +6,15 @@ import PovesteLogoLight from '../../assets/poveste-text.svg'
6
6
  import { customLogos, povesteConfig } from '../../util/config'
7
7
  import { isDark } from '../../util/dark'
8
8
 
9
+ // `theme.logo` is what the config asked for; `customLogos` is the resolved URL
10
+ // map from the virtual module. Two objects, so a check on one narrows nothing
11
+ // about the other — and falling back to the bundled logo beats rendering an
12
+ // `<img>` with no `src` if they ever disagree.
9
13
  const logoUrl = computed(() => {
10
14
  if (isDark.value) {
11
- return povesteConfig.theme.logo?.dark ? customLogos.dark : PovesteLogoDark
15
+ return povesteConfig.theme.logo?.dark ? customLogos?.dark ?? PovesteLogoDark : PovesteLogoDark
12
16
  }
13
- return povesteConfig.theme.logo?.light ? customLogos.light : PovesteLogoLight
17
+ return povesteConfig.theme.logo?.light ? customLogos?.light ?? PovesteLogoLight : PovesteLogoLight
14
18
  })
15
19
 
16
20
  const altText = computed(() => `${povesteConfig.theme.title} logo`)
@@ -16,7 +16,8 @@ const storyStore = useStoryStore()
16
16
  const story = computed(() => storyStore.currentStory)
17
17
 
18
18
  const folders = computed(() => {
19
- return story.value.file.path.slice(0, -1)
19
+ // No story, or a story with no file behind it, means no trail to draw.
20
+ return story.value?.file?.path.slice(0, -1) ?? []
20
21
  })
21
22
 
22
23
  const isMenuOpened = ref(false)
@@ -1,4 +1,5 @@
1
1
  <script lang="ts" setup>
2
+ import { computed } from 'vue'
2
3
  import { useStoryStore } from '../../stores/story'
3
4
  import DevOnlyToolbarOpenInEditor from '../toolbar/DevOnlyToolbarOpenInEditor.vue'
4
5
  import ToolbarBackground from '../toolbar/ToolbarBackground.vue'
@@ -15,6 +16,10 @@ defineEmits({
15
16
  })
16
17
 
17
18
  const storyStore = useStoryStore()
19
+
20
+ // Named once so the `v-if` and the binding below read the same value; a story
21
+ // without a file has nothing to open.
22
+ const editableFile = computed(() => storyStore.currentStory?.file?.filePath)
18
23
  </script>
19
24
 
20
25
  <template>
@@ -51,8 +56,8 @@ const storyStore = useStoryStore()
51
56
  :story="storyStore.currentStory"
52
57
  />
53
58
  <DevOnlyToolbarOpenInEditor
54
- v-if="__POVESTE_DEV__"
55
- :file="storyStore.currentStory.file?.filePath"
59
+ v-if="__POVESTE_DEV__ && editableFile"
60
+ :file="editableFile"
56
61
  tooltip="Edit story in editor"
57
62
  />
58
63
  </TopBarChip>
@@ -2,7 +2,7 @@
2
2
  <!-- Without an href this is not a link, so `aria-label` on it is prohibited and
3
3
  it has no accessible role. Chip items are used both ways (#310). -->
4
4
  <a
5
- :role="$attrs.href ? undefined : 'button'"
5
+ :role="$attrs['href'] ? undefined : 'button'"
6
6
  class="poveste-top-bar-chip-item flex items-center gap-1 px-2.5 py-1.5 hover:bg-white/50 active:bg-white/70 dark:hover:bg-white/10 dark:active:bg-white/15 hover:text-primary-500 dark:hover:text-primary-400 cursor-pointer text-gray-900 dark:text-gray-100 transition-colors"
7
7
  >
8
8
  <slot />
@@ -28,7 +28,7 @@ const progress = computed(() => props.modelValue ? 1 : 0)
28
28
  const dashoffset = computed(() => (1 - progress.value) * dasharray.value)
29
29
 
30
30
  watch(path, () => {
31
- dasharray.value = path.value.getTotalLength?.() ?? 21.21
31
+ dasharray.value = path.value?.getTotalLength?.() ?? 21.21
32
32
  })
33
33
  </script>
34
34
 
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div class="poveste-base-empty base-empty flex flex-col items-center justify-center space-y-4 py-12 h-full text-center text-gray-400 text-lg">
2
+ <div class="poveste-base-empty base-empty flex flex-col items-center justify-center space-y-4 py-12 h-full text-center text-gray-500 dark:text-gray-400 text-lg">
3
3
  <slot />
4
4
  </div>
5
5
  </template>
@@ -26,9 +26,11 @@ export default defineComponent({
26
26
  },
27
27
 
28
28
  setup(props, { emit }) {
29
- function handleNavigate(event, navigate: (event) => unknown) {
29
+ function handleNavigate(event: MouseEvent | KeyboardEvent, navigate: (event?: MouseEvent) => unknown) {
30
30
  emit('navigate')
31
- navigate(event)
31
+ // vue-router types this as a click, but its guard reads only the modifier
32
+ // keys and `defaultPrevented`, which a key press carries too.
33
+ navigate(event as MouseEvent)
32
34
  }
33
35
 
34
36
  return {