@poveste/app 0.6.1 → 0.8.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/dist/bundled/app.css +7 -7
- package/dist/bundled/components/story/StoryVariantGrid.js +1 -1
- package/dist/bundled/components/story/StoryVariantGrid.vue_vue_type_script_setup_true_lang.js +22 -2
- package/dist/bundled/components/story/StoryVariantListItem.js +1 -1
- package/dist/bundled/components/story/StoryVariantListItem.vue_vue_type_script_setup_true_lang.js +29 -10
- package/dist/bundled/components/story/StoryVariantSinglePreviewRemote.vue_vue_type_script_setup_true_lang.js +40 -2
- package/dist/bundled/components/tree/StoryListItem.js +1 -1
- package/dist/bundled/components/tree/StoryListItem.vue_vue_type_script_setup_true_lang.js +24 -11
- package/dist/bundled/sandbox.js +9 -0
- package/dist/bundled/stores/story-errors.js +26 -0
- package/dist/style.css +28 -0
- package/package.json +13 -4
- package/src/app/components/story/StoryVariantGrid.vue +41 -2
- package/src/app/components/story/StoryVariantListItem.vue +18 -1
- package/src/app/components/story/StoryVariantSinglePreviewRemote.vue +44 -0
- package/src/app/components/tree/StoryListItem.vue +15 -0
- package/src/app/sandbox.ts +12 -0
- package/src/app/stores/story-errors.ts +48 -0
package/dist/bundled/app.css
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
@scope (:root) to (.__poveste-render-story:not(.__poveste-render-custom-controls)) {
|
|
2
|
-
.bind-tree-margin[data-v-
|
|
3
|
-
margin-left: var(--
|
|
2
|
+
.bind-tree-margin[data-v-a603aebf] {
|
|
3
|
+
margin-left: var(--v18724f1f);
|
|
4
4
|
}
|
|
5
|
-
.bind-icon-color[data-v-
|
|
6
|
-
color: var(--
|
|
5
|
+
.bind-icon-color[data-v-a603aebf] {
|
|
6
|
+
color: var(--v45b473db);
|
|
7
7
|
}
|
|
8
8
|
}
|
|
9
9
|
@scope (:root) to (.__poveste-render-story:not(.__poveste-render-custom-controls)) {
|
|
@@ -124,13 +124,13 @@
|
|
|
124
124
|
/* Cells are placed with `order`, so without this Tab and a screen reader walk
|
|
125
125
|
them in slot order — the order they were recycled in, not the screen's.
|
|
126
126
|
Chromium only for now; elsewhere the rule is ignored. */
|
|
127
|
-
.poveste-story-variant-grid-cells[data-v-
|
|
127
|
+
.poveste-story-variant-grid-cells[data-v-5f7bc1e1] {
|
|
128
128
|
reading-flow: grid-order;
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
131
|
@scope (:root) to (.__poveste-render-story:not(.__poveste-render-custom-controls)) {
|
|
132
|
-
.bind-icon-color[data-v-
|
|
133
|
-
color: var(--
|
|
132
|
+
.bind-icon-color[data-v-8112241f] {
|
|
133
|
+
color: var(--v29a7233a);
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
136
|
@scope (:root) to (.__poveste-render-story:not(.__poveste-render-custom-controls)) {
|
|
@@ -2,6 +2,6 @@ import _plugin_vue_export_helper_default from "../../_virtual/_plugin-vue_export
|
|
|
2
2
|
import StoryVariantGrid_vue_vue_type_script_setup_true_lang_default from "./StoryVariantGrid.vue_vue_type_script_setup_true_lang.js";
|
|
3
3
|
/* empty css */
|
|
4
4
|
//#region src/app/components/story/StoryVariantGrid.vue
|
|
5
|
-
var StoryVariantGrid_default = /*#__PURE__*/ _plugin_vue_export_helper_default(StoryVariantGrid_vue_vue_type_script_setup_true_lang_default, [["__scopeId", "data-v-
|
|
5
|
+
var StoryVariantGrid_default = /*#__PURE__*/ _plugin_vue_export_helper_default(StoryVariantGrid_vue_vue_type_script_setup_true_lang_default, [["__scopeId", "data-v-5f7bc1e1"]]);
|
|
6
6
|
//#endregion
|
|
7
7
|
export { StoryVariantGrid_default as default };
|
package/dist/bundled/components/story/StoryVariantGrid.vue_vue_type_script_setup_true_lang.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useStoryStore } from "../../stores/story.js";
|
|
2
2
|
import { assignSlots, trimSlots } from "../../util/grid-slots.js";
|
|
3
3
|
import StoryVariantGridItem_default from "./StoryVariantGridItem.js";
|
|
4
|
-
import { useResizeObserver } from "@poveste/vendors/vue-use";
|
|
4
|
+
import { useDebounceFn, useResizeObserver } from "@poveste/vendors/vue-use";
|
|
5
5
|
import { Fragment, computed, createBlock, createElementBlock, createElementVNode, defineComponent, normalizeStyle, onBeforeUnmount, onMounted, openBlock, ref, renderList, unref, watch } from "@poveste/vendors/vue";
|
|
6
6
|
//#region src/app/components/story/StoryVariantGrid.vue?vue&type=script&setup=true&lang.ts
|
|
7
7
|
var _hoisted_1 = { class: "poveste-story-variant-grid flex flex-col items-stretch h-full __poveste-pane-shadow-from-right" };
|
|
@@ -9,6 +9,8 @@ var _hoisted_2 = { class: "flex w-0 flex-1 mx-4" };
|
|
|
9
9
|
var gap = 16;
|
|
10
10
|
var overscanRows = 2;
|
|
11
11
|
var initialCount = 10;
|
|
12
|
+
var FAST_PX_PER_MS = 8;
|
|
13
|
+
var FAST_REFRESH_MS = 150;
|
|
12
14
|
var TRIM_DELAY = 2e3;
|
|
13
15
|
var StoryVariantGrid_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineComponent({
|
|
14
16
|
__name: "StoryVariantGrid",
|
|
@@ -35,6 +37,23 @@ var StoryVariantGrid_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/
|
|
|
35
37
|
scrollTop.value = el.value.scrollTop;
|
|
36
38
|
viewportHeight.value = el.value.clientHeight;
|
|
37
39
|
}
|
|
40
|
+
let lastScrollTop = 0;
|
|
41
|
+
let lastScrollTime = 0;
|
|
42
|
+
let lastWindowUpdate = 0;
|
|
43
|
+
const settleWindow = useDebounceFn(updateWindow, 120);
|
|
44
|
+
function onScroll() {
|
|
45
|
+
const now = performance.now();
|
|
46
|
+
const top = el.value?.scrollTop ?? 0;
|
|
47
|
+
const dt = now - lastScrollTime;
|
|
48
|
+
const velocity = dt > 0 ? Math.abs(top - lastScrollTop) / dt : 0;
|
|
49
|
+
lastScrollTop = top;
|
|
50
|
+
lastScrollTime = now;
|
|
51
|
+
if (velocity <= FAST_PX_PER_MS || now - lastWindowUpdate >= FAST_REFRESH_MS) {
|
|
52
|
+
updateWindow();
|
|
53
|
+
lastWindowUpdate = now;
|
|
54
|
+
}
|
|
55
|
+
settleWindow();
|
|
56
|
+
}
|
|
38
57
|
function onItemResize(_w, h) {
|
|
39
58
|
if (maxItemHeight.value < h) {
|
|
40
59
|
maxItemHeight.value = h;
|
|
@@ -91,6 +110,7 @@ var StoryVariantGrid_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/
|
|
|
91
110
|
}, { immediate: true });
|
|
92
111
|
onBeforeUnmount(() => {
|
|
93
112
|
clearTimeout(trimTimer);
|
|
113
|
+
settleWindow.cancel();
|
|
94
114
|
});
|
|
95
115
|
const totalHeight = computed(() => measured.value ? rowCount.value * rowHeight.value - gap : void 0);
|
|
96
116
|
const offsetY = computed(() => measured.value ? startIndex.value / columnCount.value * rowHeight.value : 0);
|
|
@@ -111,7 +131,7 @@ var StoryVariantGrid_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/
|
|
|
111
131
|
ref_key: "el",
|
|
112
132
|
ref: el,
|
|
113
133
|
class: "overflow-y-auto flex flex-1",
|
|
114
|
-
onScroll: _cache[0] || (_cache[0] = ($event) =>
|
|
134
|
+
onScroll: _cache[0] || (_cache[0] = ($event) => onScroll())
|
|
115
135
|
}, [createElementVNode("div", _hoisted_2, [createElementVNode("div", {
|
|
116
136
|
class: "m-auto",
|
|
117
137
|
style: normalizeStyle({ minHeight: totalHeight.value != null ? `${totalHeight.value}px` : void 0 })
|
|
@@ -2,6 +2,6 @@ import _plugin_vue_export_helper_default from "../../_virtual/_plugin-vue_export
|
|
|
2
2
|
import StoryVariantListItem_vue_vue_type_script_setup_true_lang_default from "./StoryVariantListItem.vue_vue_type_script_setup_true_lang.js";
|
|
3
3
|
/* empty css */
|
|
4
4
|
//#region src/app/components/story/StoryVariantListItem.vue
|
|
5
|
-
var StoryVariantListItem_default = /*#__PURE__*/ _plugin_vue_export_helper_default(StoryVariantListItem_vue_vue_type_script_setup_true_lang_default, [["__scopeId", "data-v-
|
|
5
|
+
var StoryVariantListItem_default = /*#__PURE__*/ _plugin_vue_export_helper_default(StoryVariantListItem_vue_vue_type_script_setup_true_lang_default, [["__scopeId", "data-v-8112241f"]]);
|
|
6
6
|
//#endregion
|
|
7
7
|
export { StoryVariantListItem_default as default };
|
package/dist/bundled/components/story/StoryVariantListItem.vue_vue_type_script_setup_true_lang.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { useStoryStore } from "../../stores/story.js";
|
|
2
|
+
import { useStoryErrorStore } from "../../stores/story-errors.js";
|
|
1
3
|
import { useScrollOnActive } from "../../util/scroll.js";
|
|
2
4
|
import BaseListItemLink_default from "../base/BaseListItemLink.js";
|
|
3
5
|
import { useCurrentVariantRoute } from "../../util/variant.js";
|
|
4
|
-
import { createElementBlock, createElementVNode, createVNode, defineComponent, normalizeClass, openBlock, ref, toDisplayString, toRefs, unref, useCssVars, withCtx } from "@poveste/vendors/vue";
|
|
6
|
+
import { computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createVNode, defineComponent, normalizeClass, openBlock, ref, toDisplayString, toRefs, unref, useCssVars, withCtx } from "@poveste/vendors/vue";
|
|
5
7
|
import { Icon } from "@poveste/vendors/iconify";
|
|
6
8
|
//#region src/app/components/story/StoryVariantListItem.vue?vue&type=script&setup=true&lang.ts
|
|
7
9
|
var _hoisted_1 = { class: "truncate" };
|
|
@@ -12,11 +14,18 @@ var StoryVariantListItem_vue_vue_type_script_setup_true_lang_default = /*@__PURE
|
|
|
12
14
|
required: true
|
|
13
15
|
} },
|
|
14
16
|
setup(__props) {
|
|
15
|
-
useCssVars((_ctx) => ({ "
|
|
16
|
-
const
|
|
17
|
+
useCssVars((_ctx) => ({ "v29a7233a": unref(variant).iconColor }));
|
|
18
|
+
const props = __props;
|
|
19
|
+
const { variant } = toRefs(props);
|
|
17
20
|
const { isActive, targetRoute } = useCurrentVariantRoute(variant);
|
|
18
21
|
const el = ref();
|
|
19
22
|
useScrollOnActive(isActive, el);
|
|
23
|
+
const storyStore = useStoryStore();
|
|
24
|
+
const errorStore = useStoryErrorStore();
|
|
25
|
+
const error = computed(() => {
|
|
26
|
+
const storyId = storyStore.currentStory?.id;
|
|
27
|
+
return storyId ? errorStore.forVariant(storyId, props.variant.id) : void 0;
|
|
28
|
+
});
|
|
20
29
|
return (_ctx, _cache) => {
|
|
21
30
|
return openBlock(), createElementBlock("div", {
|
|
22
31
|
ref_key: "el",
|
|
@@ -28,13 +37,23 @@ var StoryVariantListItem_vue_vue_type_script_setup_true_lang_default = /*@__PURE
|
|
|
28
37
|
"is-active": unref(isActive),
|
|
29
38
|
class: "px-2 py-2 md:py-1.5 m-1 rounded-sm flex items-center gap-2"
|
|
30
39
|
}, {
|
|
31
|
-
default: withCtx(({ active }) => [
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
"
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
40
|
+
default: withCtx(({ active }) => [
|
|
41
|
+
createVNode(unref(Icon), {
|
|
42
|
+
icon: unref(variant).icon ?? "carbon:cube",
|
|
43
|
+
class: normalizeClass(["w-5 h-5 sm:w-4 sm:h-4 flex-none", {
|
|
44
|
+
"text-gray-500": !active && !unref(variant).iconColor,
|
|
45
|
+
"bind-icon-color": !active && unref(variant).iconColor
|
|
46
|
+
}])
|
|
47
|
+
}, null, 8, ["icon", "class"]),
|
|
48
|
+
createElementVNode("span", _hoisted_1, toDisplayString(unref(variant).title), 1),
|
|
49
|
+
error.value ? (openBlock(), createBlock(unref(Icon), {
|
|
50
|
+
key: 0,
|
|
51
|
+
icon: "carbon:warning-alt",
|
|
52
|
+
class: "w-4 h-4 flex-none ml-auto text-red-500",
|
|
53
|
+
"data-testid": "variant-error-marker",
|
|
54
|
+
"aria-label": `Threw while rendering: ${error.value.message}`
|
|
55
|
+
}, null, 8, ["aria-label"])) : createCommentVNode("", true)
|
|
56
|
+
]),
|
|
38
57
|
_: 1
|
|
39
58
|
}, 8, ["to", "is-active"])], 512);
|
|
40
59
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { EVENT_SEND, PREVIEW_SETTINGS_REQUEST, PREVIEW_SETTINGS_SYNC, SANDBOX_HEIGHT, SANDBOX_READY, SANDBOX_RETARGET, STATE_SYNC } from "../../util/const.js";
|
|
2
2
|
import { trackWindow } from "../../util/keyboard.js";
|
|
3
3
|
import { useStoryStore } from "../../stores/story.js";
|
|
4
|
+
import { useStoryErrorStore } from "../../stores/story-errors.js";
|
|
4
5
|
import { usePreviewSettingsStore } from "../../stores/preview-settings.js";
|
|
5
6
|
import { getSandboxUrl } from "../../util/sandbox.js";
|
|
6
7
|
import { createStateBridge } from "../../util/state-bridge.js";
|
|
@@ -9,9 +10,25 @@ import { useEventsStore } from "../../stores/events.js";
|
|
|
9
10
|
import { firstReportedHeight } from "../../util/grid-cell-height.js";
|
|
10
11
|
import StoryResponsivePreview_default from "./StoryResponsivePreview.js";
|
|
11
12
|
import { useEventListener } from "@poveste/vendors/vue-use";
|
|
12
|
-
import { computed, createBlock, createElementVNode, defineComponent, normalizeClass, normalizeStyle, onBeforeUnmount, openBlock, ref, toRaw, watch, withCtx } from "@poveste/vendors/vue";
|
|
13
|
+
import { computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createVNode, defineComponent, normalizeClass, normalizeStyle, onBeforeUnmount, openBlock, ref, toDisplayString, toRaw, unref, watch, withCtx } from "@poveste/vendors/vue";
|
|
14
|
+
import { Icon } from "@poveste/vendors/iconify";
|
|
15
|
+
import { SANDBOX_ERROR } from "@poveste/shared";
|
|
13
16
|
//#region src/app/components/story/StoryVariantSinglePreviewRemote.vue?vue&type=script&setup=true&lang.ts
|
|
14
17
|
var _hoisted_1 = ["src"];
|
|
18
|
+
var _hoisted_2 = {
|
|
19
|
+
key: 0,
|
|
20
|
+
class: "poveste-story-error absolute inset-0 overflow-auto p-6 bg-white dark:bg-gray-900",
|
|
21
|
+
"data-testid": "story-error"
|
|
22
|
+
};
|
|
23
|
+
var _hoisted_3 = { class: "flex items-center gap-2 text-red-600 dark:text-red-400 font-medium" };
|
|
24
|
+
var _hoisted_4 = {
|
|
25
|
+
class: "mt-2 font-mono text-sm text-red-700 dark:text-red-300",
|
|
26
|
+
"data-testid": "story-error-message"
|
|
27
|
+
};
|
|
28
|
+
var _hoisted_5 = {
|
|
29
|
+
key: 0,
|
|
30
|
+
class: "mt-4 text-xs whitespace-pre-wrap text-gray-600 dark:text-gray-400"
|
|
31
|
+
};
|
|
15
32
|
var MIN_HEIGHT = 40;
|
|
16
33
|
var MAX_AUTO_HEIGHT = 400;
|
|
17
34
|
var REUSE_LIMIT = 30;
|
|
@@ -30,6 +47,11 @@ var StoryVariantSinglePreviewRemote_vue_vue_type_script_setup_true_lang_default
|
|
|
30
47
|
setup(__props) {
|
|
31
48
|
const props = __props;
|
|
32
49
|
const settings = usePreviewSettingsStore().currentSettings;
|
|
50
|
+
const errorStore = useStoryErrorStore();
|
|
51
|
+
const storyError = computed(() => errorStore.forVariant(props.story.id, props.variant.id));
|
|
52
|
+
watch(() => [props.story.id, props.variant.id], () => {
|
|
53
|
+
errorStore.clear(props.story.id, props.variant.id);
|
|
54
|
+
});
|
|
33
55
|
const iframe = ref();
|
|
34
56
|
function postState(changes) {
|
|
35
57
|
iframe.value?.contentWindow?.postMessage({
|
|
@@ -67,6 +89,15 @@ var StoryVariantSinglePreviewRemote_vue_vue_type_script_setup_true_lang_default
|
|
|
67
89
|
if (!fromCurrentOccupant(event.data)) break;
|
|
68
90
|
setPreviewReady();
|
|
69
91
|
break;
|
|
92
|
+
case SANDBOX_ERROR:
|
|
93
|
+
if (!fromCurrentOccupant(event.data)) break;
|
|
94
|
+
errorStore.report({
|
|
95
|
+
message: event.data.message,
|
|
96
|
+
stack: event.data.stack,
|
|
97
|
+
storyId: props.story.id,
|
|
98
|
+
variantId: props.variant.id
|
|
99
|
+
});
|
|
100
|
+
break;
|
|
70
101
|
case PREVIEW_SETTINGS_REQUEST:
|
|
71
102
|
syncSettings();
|
|
72
103
|
break;
|
|
@@ -192,7 +223,14 @@ var StoryVariantSinglePreviewRemote_vue_vue_type_script_setup_true_lang_default
|
|
|
192
223
|
style: normalizeStyle(previewStyle(isResponsiveEnabled, finalWidth, finalHeight)),
|
|
193
224
|
"data-testid": "preview-iframe",
|
|
194
225
|
onLoad: _cache[0] || (_cache[0] = ($event) => onIframeLoad())
|
|
195
|
-
}, null, 46, _hoisted_1)
|
|
226
|
+
}, null, 46, _hoisted_1), storyError.value ? (openBlock(), createElementBlock("div", _hoisted_2, [
|
|
227
|
+
createElementVNode("div", _hoisted_3, [createVNode(unref(Icon), {
|
|
228
|
+
icon: "carbon:warning-alt",
|
|
229
|
+
class: "w-5 h-5 flex-none"
|
|
230
|
+
}), _cache[1] || (_cache[1] = createElementVNode("span", null, "This component threw while rendering", -1))]),
|
|
231
|
+
createElementVNode("p", _hoisted_4, toDisplayString(storyError.value.message), 1),
|
|
232
|
+
storyError.value.stack ? (openBlock(), createElementBlock("pre", _hoisted_5, toDisplayString(storyError.value.stack), 1)) : createCommentVNode("", true)
|
|
233
|
+
])) : createCommentVNode("", true)]),
|
|
196
234
|
_: 1
|
|
197
235
|
}, 8, ["variant", "responsive"]);
|
|
198
236
|
};
|
|
@@ -2,6 +2,6 @@ import _plugin_vue_export_helper_default from "../../_virtual/_plugin-vue_export
|
|
|
2
2
|
import StoryListItem_vue_vue_type_script_setup_true_lang_default from "./StoryListItem.vue_vue_type_script_setup_true_lang.js";
|
|
3
3
|
/* empty css */
|
|
4
4
|
//#region src/app/components/tree/StoryListItem.vue
|
|
5
|
-
var StoryListItem_default = /*#__PURE__*/ _plugin_vue_export_helper_default(StoryListItem_vue_vue_type_script_setup_true_lang_default, [["__scopeId", "data-v-
|
|
5
|
+
var StoryListItem_default = /*#__PURE__*/ _plugin_vue_export_helper_default(StoryListItem_vue_vue_type_script_setup_true_lang_default, [["__scopeId", "data-v-a603aebf"]]);
|
|
6
6
|
//#endregion
|
|
7
7
|
export { StoryListItem_default as default };
|
|
@@ -1,13 +1,14 @@
|
|
|
1
|
+
import { useStoryErrorStore } from "../../stores/story-errors.js";
|
|
1
2
|
import { useScrollOnActive } from "../../util/scroll.js";
|
|
2
3
|
import BaseListItemLink_default from "../base/BaseListItemLink.js";
|
|
3
|
-
import { computed, createCommentVNode, createElementBlock, createElementVNode, createVNode, defineComponent, normalizeClass, openBlock, ref, toDisplayString, unref, useCssVars, withCtx } from "@poveste/vendors/vue";
|
|
4
|
+
import { computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createVNode, defineComponent, normalizeClass, openBlock, ref, toDisplayString, unref, useCssVars, withCtx } from "@poveste/vendors/vue";
|
|
4
5
|
import { Icon } from "@poveste/vendors/iconify";
|
|
5
6
|
import { useRoute } from "@poveste/vendors/vue-router";
|
|
6
7
|
//#region src/app/components/tree/StoryListItem.vue?vue&type=script&setup=true&lang.ts
|
|
7
8
|
var _hoisted_1 = { class: "bind-tree-margin flex items-center gap-2 pl-4 min-w-0" };
|
|
8
9
|
var _hoisted_2 = { class: "truncate" };
|
|
9
10
|
var _hoisted_3 = {
|
|
10
|
-
key:
|
|
11
|
+
key: 1,
|
|
11
12
|
class: "opacity-40 text-sm"
|
|
12
13
|
};
|
|
13
14
|
var StoryListItem_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineComponent({
|
|
@@ -18,10 +19,12 @@ var StoryListItem_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ de
|
|
|
18
19
|
},
|
|
19
20
|
setup(__props) {
|
|
20
21
|
useCssVars((_ctx) => ({
|
|
21
|
-
"
|
|
22
|
-
"
|
|
22
|
+
"v18724f1f": filePadding.value,
|
|
23
|
+
"v45b473db": _ctx.story.iconColor
|
|
23
24
|
}));
|
|
24
25
|
const props = __props;
|
|
26
|
+
const errorStore = useStoryErrorStore();
|
|
27
|
+
const hasError = computed(() => errorStore.storyHasError(props.story.id));
|
|
25
28
|
const filePadding = computed(() => {
|
|
26
29
|
return `${props.depth * 12}px`;
|
|
27
30
|
});
|
|
@@ -42,13 +45,23 @@ var StoryListItem_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ de
|
|
|
42
45
|
},
|
|
43
46
|
class: "pl-0.5 pr-2 py-2 md:py-1.5 mx-1 rounded-sm"
|
|
44
47
|
}, {
|
|
45
|
-
default: withCtx(({ active }) => [
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
"
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
default: withCtx(({ active }) => [
|
|
49
|
+
createElementVNode("span", _hoisted_1, [createVNode(unref(Icon), {
|
|
50
|
+
icon: __props.story.icon ?? "carbon:cube",
|
|
51
|
+
class: normalizeClass(["w-5 h-5 sm:w-4 sm:h-4 flex-none", {
|
|
52
|
+
"text-primary-500": !active && !__props.story.iconColor,
|
|
53
|
+
"bind-icon-color": !active && __props.story.iconColor
|
|
54
|
+
}])
|
|
55
|
+
}, null, 8, ["icon", "class"]), createElementVNode("span", _hoisted_2, toDisplayString(__props.story.title), 1)]),
|
|
56
|
+
hasError.value ? (openBlock(), createBlock(unref(Icon), {
|
|
57
|
+
key: 0,
|
|
58
|
+
icon: "carbon:warning-alt",
|
|
59
|
+
class: "w-4 h-4 flex-none ml-auto text-red-500",
|
|
60
|
+
"data-testid": "story-error-marker",
|
|
61
|
+
"aria-label": "A variant of this story threw while rendering"
|
|
62
|
+
})) : createCommentVNode("", true),
|
|
63
|
+
!__props.story.docsOnly ? (openBlock(), createElementBlock("span", _hoisted_3, toDisplayString(__props.story.variants.length), 1)) : createCommentVNode("", true)
|
|
64
|
+
]),
|
|
52
65
|
_: 1
|
|
53
66
|
}, 8, ["to"])], 512);
|
|
54
67
|
};
|
package/dist/bundled/sandbox.js
CHANGED
|
@@ -12,6 +12,7 @@ import { computed, createApp, h, onMounted, ref, watch } from "@poveste/vendors/
|
|
|
12
12
|
import { createPinia } from "@poveste/vendors/pinia";
|
|
13
13
|
import { files, onUpdate } from "virtual:$poveste-stories";
|
|
14
14
|
import { parseQuery } from "@poveste/vendors/vue-router";
|
|
15
|
+
import { reportStoryError } from "@poveste/shared";
|
|
15
16
|
//#region src/app/sandbox.ts
|
|
16
17
|
var query = parseQuery(window.location.search);
|
|
17
18
|
var storyId = ref(typeof query.storyId === "string" ? query.storyId : null);
|
|
@@ -29,6 +30,14 @@ onUpdate((newFiles) => {
|
|
|
29
30
|
var storedSettings = loadStoredPreviewSettings();
|
|
30
31
|
if (storedSettings) applyPreviewSettings(storedSettings);
|
|
31
32
|
if (window.parent && window.parent !== window) window.parent.postMessage({ type: PREVIEW_SETTINGS_REQUEST }, window.location.origin);
|
|
33
|
+
function reportUncaught(error) {
|
|
34
|
+
reportStoryError(error, {
|
|
35
|
+
storyId: storyId.value,
|
|
36
|
+
variantId: variantId.value
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
window.addEventListener("error", (event) => reportUncaught(event.error ?? event.message));
|
|
40
|
+
window.addEventListener("unhandledrejection", (event) => reportUncaught(event.reason));
|
|
32
41
|
var prefersDark = usePreferredDark();
|
|
33
42
|
var previewDark = computed(() => resolvePreviewDark(receivedSettings.colorScheme, prefersDark.value, isDark.value));
|
|
34
43
|
var darkClassTargets = [document.documentElement, document.querySelector("#app")];
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { computed, ref } from "@poveste/vendors/vue";
|
|
2
|
+
import { defineStore } from "@poveste/vendors/pinia";
|
|
3
|
+
//#region src/app/stores/story-errors.ts
|
|
4
|
+
var useStoryErrorStore = defineStore("story-errors", () => {
|
|
5
|
+
const errors = ref(/* @__PURE__ */ new Map());
|
|
6
|
+
const key = (storyId, variantId) => `${storyId}::${variantId ?? ""}`;
|
|
7
|
+
function report(error) {
|
|
8
|
+
if (!error.storyId) return;
|
|
9
|
+
errors.value.set(key(error.storyId, error.variantId), error);
|
|
10
|
+
}
|
|
11
|
+
function clear(storyId, variantId) {
|
|
12
|
+
errors.value.delete(key(storyId, variantId));
|
|
13
|
+
}
|
|
14
|
+
return {
|
|
15
|
+
errors,
|
|
16
|
+
report,
|
|
17
|
+
clear,
|
|
18
|
+
forVariant: computed(() => (storyId, variantId) => errors.value.get(key(storyId, variantId))),
|
|
19
|
+
storyHasError: computed(() => (storyId) => {
|
|
20
|
+
for (const [entry] of errors.value) if (entry.startsWith(`${storyId}::`)) return true;
|
|
21
|
+
return false;
|
|
22
|
+
})
|
|
23
|
+
};
|
|
24
|
+
});
|
|
25
|
+
//#endregion
|
|
26
|
+
export { useStoryErrorStore };
|
package/dist/style.css
CHANGED
|
@@ -335,7 +335,11 @@
|
|
|
335
335
|
:scope, :host {
|
|
336
336
|
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
|
|
337
337
|
monospace;
|
|
338
|
+
--color-red-300: oklch(80.8% 0.114 19.571);
|
|
339
|
+
--color-red-400: oklch(70.4% 0.191 22.216);
|
|
338
340
|
--color-red-500: oklch(63.7% 0.237 25.331);
|
|
341
|
+
--color-red-600: oklch(57.7% 0.245 27.325);
|
|
342
|
+
--color-red-700: oklch(50.5% 0.213 27.518);
|
|
339
343
|
--color-orange-500: oklch(70.5% 0.213 47.604);
|
|
340
344
|
--color-emerald-50: oklch(97.9% 0.021 166.113);
|
|
341
345
|
--color-emerald-100: oklch(95% 0.052 163.051);
|
|
@@ -990,6 +994,9 @@
|
|
|
990
994
|
.mt-2 {
|
|
991
995
|
margin-top: calc(var(--spacing) * 2);
|
|
992
996
|
}
|
|
997
|
+
.mt-4 {
|
|
998
|
+
margin-top: calc(var(--spacing) * 4);
|
|
999
|
+
}
|
|
993
1000
|
.mr-2 {
|
|
994
1001
|
margin-right: calc(var(--spacing) * 2);
|
|
995
1002
|
}
|
|
@@ -1669,6 +1676,9 @@
|
|
|
1669
1676
|
.p-4 {
|
|
1670
1677
|
padding: calc(var(--spacing) * 4);
|
|
1671
1678
|
}
|
|
1679
|
+
.p-6 {
|
|
1680
|
+
padding: calc(var(--spacing) * 6);
|
|
1681
|
+
}
|
|
1672
1682
|
.p-8 {
|
|
1673
1683
|
padding: calc(var(--spacing) * 8);
|
|
1674
1684
|
}
|
|
@@ -1812,6 +1822,9 @@
|
|
|
1812
1822
|
.whitespace-nowrap {
|
|
1813
1823
|
white-space: nowrap;
|
|
1814
1824
|
}
|
|
1825
|
+
.whitespace-pre-wrap {
|
|
1826
|
+
white-space: pre-wrap;
|
|
1827
|
+
}
|
|
1815
1828
|
.text-black\/\[1\%\] {
|
|
1816
1829
|
color: color-mix(in srgb, #000 1%, transparent);
|
|
1817
1830
|
@supports (color: color-mix(in lab, red, red)) {
|
|
@@ -1830,6 +1843,9 @@
|
|
|
1830
1843
|
color: color-mix(in oklab, var(--color-gray-500) 20%, transparent);
|
|
1831
1844
|
}
|
|
1832
1845
|
}
|
|
1846
|
+
.text-gray-600 {
|
|
1847
|
+
color: var(--color-gray-600);
|
|
1848
|
+
}
|
|
1833
1849
|
.text-gray-700 {
|
|
1834
1850
|
color: var(--color-gray-700);
|
|
1835
1851
|
}
|
|
@@ -1854,6 +1870,12 @@
|
|
|
1854
1870
|
.text-red-500 {
|
|
1855
1871
|
color: var(--color-red-500);
|
|
1856
1872
|
}
|
|
1873
|
+
.text-red-600 {
|
|
1874
|
+
color: var(--color-red-600);
|
|
1875
|
+
}
|
|
1876
|
+
.text-red-700 {
|
|
1877
|
+
color: var(--color-red-700);
|
|
1878
|
+
}
|
|
1857
1879
|
.text-white {
|
|
1858
1880
|
color: var(--color-white);
|
|
1859
1881
|
}
|
|
@@ -2307,6 +2329,12 @@
|
|
|
2307
2329
|
.dark\:text-primary-400:where(.ptw-dark, .ptw-dark *) {
|
|
2308
2330
|
color: var(--color-primary-400);
|
|
2309
2331
|
}
|
|
2332
|
+
.dark\:text-red-300:where(.ptw-dark, .ptw-dark *) {
|
|
2333
|
+
color: var(--color-red-300);
|
|
2334
|
+
}
|
|
2335
|
+
.dark\:text-red-400:where(.ptw-dark, .ptw-dark *) {
|
|
2336
|
+
color: var(--color-red-400);
|
|
2337
|
+
}
|
|
2310
2338
|
.dark\:text-white\/\[1\%\]:where(.ptw-dark, .ptw-dark *) {
|
|
2311
2339
|
color: color-mix(in srgb, #fff 1%, transparent);
|
|
2312
2340
|
@supports (color: color-mix(in lab, red, red)) {
|
package/package.json
CHANGED
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@poveste/app",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.8.0",
|
|
5
5
|
"description": "Pre-bundled UI for Poveste",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Sorin Gitlan"
|
|
8
8
|
},
|
|
9
9
|
"license": "MIT",
|
|
10
|
+
"homepage": "https://poveste.dev",
|
|
10
11
|
"repository": {
|
|
11
12
|
"url": "https://github.com/poveste-dev/poveste.git",
|
|
12
13
|
"type": "git",
|
|
13
14
|
"directory": "packages/poveste-app"
|
|
14
15
|
},
|
|
16
|
+
"bugs": "https://github.com/poveste-dev/poveste/issues",
|
|
17
|
+
"keywords": [
|
|
18
|
+
"poveste",
|
|
19
|
+
"ui"
|
|
20
|
+
],
|
|
15
21
|
"publishConfig": {
|
|
16
22
|
"access": "public"
|
|
17
23
|
},
|
|
@@ -27,13 +33,16 @@
|
|
|
27
33
|
"dist",
|
|
28
34
|
"src"
|
|
29
35
|
],
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": "^22.22.2 || ^24.15.0 || >=26.0.0"
|
|
38
|
+
},
|
|
30
39
|
"dependencies": {
|
|
31
40
|
"@fontsource/noto-sans-display": "^5.3.0",
|
|
32
41
|
"fuse.js": "^7.0.0",
|
|
33
42
|
"shiki": "^3.0.0",
|
|
34
|
-
"@poveste/controls": "^0.
|
|
35
|
-
"@poveste/shared": "^0.
|
|
36
|
-
"@poveste/vendors": "^0.
|
|
43
|
+
"@poveste/controls": "^0.8.0",
|
|
44
|
+
"@poveste/shared": "^0.8.0",
|
|
45
|
+
"@poveste/vendors": "^0.8.0"
|
|
37
46
|
},
|
|
38
47
|
"devDependencies": {
|
|
39
48
|
"@iconify-json/carbon": "^1.2.25",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts" setup>
|
|
2
2
|
import type { Slot } from '../../util/grid-slots'
|
|
3
|
-
import { useResizeObserver } from '@vueuse/core'
|
|
3
|
+
import { useDebounceFn, useResizeObserver } from '@vueuse/core'
|
|
4
4
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
|
5
5
|
import { useStoryStore } from '../../stores/story'
|
|
6
6
|
import { assignSlots, trimSlots } from '../../util/grid-slots'
|
|
@@ -55,6 +55,43 @@ function updateWindow() {
|
|
|
55
55
|
viewportHeight.value = el.value.clientHeight
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
// A window shift retargets a row of sandbox iframes, and each retarget re-renders
|
|
59
|
+
// a story — fine while reading, ruinous while flicking a 1000-cell grid, which
|
|
60
|
+
// would render every row it flies past (#283). So the window follows a slow
|
|
61
|
+
// scroll promptly but skips most events during a fast flick. It is not frozen
|
|
62
|
+
// solid though: a bounded refresh keeps it tracking the scroller so a sustained
|
|
63
|
+
// fast drag still shows cells instead of a blank band, and the final window
|
|
64
|
+
// lands on settle. Resize/mount call `updateWindow` directly — not the churn path.
|
|
65
|
+
//
|
|
66
|
+
// The threshold sits above an ordinary mouse-wheel notch (a few px/ms) so plain
|
|
67
|
+
// reading takes the prompt path; only a genuine fling defers.
|
|
68
|
+
const FAST_PX_PER_MS = 8
|
|
69
|
+
// Cap on how long the window may lag the scroller during a fast flick: at most
|
|
70
|
+
// one retarget per this interval, versus one per row on the prompt path.
|
|
71
|
+
const FAST_REFRESH_MS = 150
|
|
72
|
+
let lastScrollTop = 0
|
|
73
|
+
let lastScrollTime = 0
|
|
74
|
+
let lastWindowUpdate = 0
|
|
75
|
+
|
|
76
|
+
// Lands the final window shortly after scrolling stops, whatever the speed was —
|
|
77
|
+
// this is what fills the tail of a fast scroll back in.
|
|
78
|
+
const settleWindow = useDebounceFn(updateWindow, 120)
|
|
79
|
+
|
|
80
|
+
function onScroll() {
|
|
81
|
+
const now = performance.now()
|
|
82
|
+
const top = el.value?.scrollTop ?? 0
|
|
83
|
+
const dt = now - lastScrollTime
|
|
84
|
+
const velocity = dt > 0 ? Math.abs(top - lastScrollTop) / dt : 0
|
|
85
|
+
lastScrollTop = top
|
|
86
|
+
lastScrollTime = now
|
|
87
|
+
|
|
88
|
+
if (velocity <= FAST_PX_PER_MS || now - lastWindowUpdate >= FAST_REFRESH_MS) {
|
|
89
|
+
updateWindow()
|
|
90
|
+
lastWindowUpdate = now
|
|
91
|
+
}
|
|
92
|
+
settleWindow()
|
|
93
|
+
}
|
|
94
|
+
|
|
58
95
|
// The reported width is ignored: `columnCount` is what CSS lays out, so it is
|
|
59
96
|
// the authority on how many items sit in a row.
|
|
60
97
|
function onItemResize(_w: number, h: number) {
|
|
@@ -153,6 +190,8 @@ watch(visibleVariants, (visible) => {
|
|
|
153
190
|
|
|
154
191
|
onBeforeUnmount(() => {
|
|
155
192
|
clearTimeout(trimTimer)
|
|
193
|
+
// Drop any settle still pending, so it cannot fire updateWindow after teardown.
|
|
194
|
+
settleWindow.cancel()
|
|
156
195
|
})
|
|
157
196
|
|
|
158
197
|
// The spacer holds the full scroll extent; the grid is translated into place
|
|
@@ -185,7 +224,7 @@ watch(() => storyStore.currentVariant, (variant) => {
|
|
|
185
224
|
<div
|
|
186
225
|
ref="el"
|
|
187
226
|
class="overflow-y-auto flex flex-1"
|
|
188
|
-
@scroll="
|
|
227
|
+
@scroll="onScroll()"
|
|
189
228
|
>
|
|
190
229
|
<div class="flex w-0 flex-1 mx-4">
|
|
191
230
|
<div
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
import type { PropType } from 'vue'
|
|
3
3
|
import type { Variant } from '../../types'
|
|
4
4
|
import { Icon } from '@iconify/vue'
|
|
5
|
-
import { ref, toRefs } from 'vue'
|
|
5
|
+
import { computed, ref, toRefs } from 'vue'
|
|
6
|
+
import { useStoryStore } from '../../stores/story'
|
|
7
|
+
import { useStoryErrorStore } from '../../stores/story-errors'
|
|
6
8
|
import { useScrollOnActive } from '../../util/scroll'
|
|
7
9
|
import { useCurrentVariantRoute } from '../../util/variant'
|
|
8
10
|
import BaseListItemLink from '../base/BaseListItemLink.vue'
|
|
@@ -18,6 +20,14 @@ const { variant } = toRefs(props)
|
|
|
18
20
|
const { isActive, targetRoute } = useCurrentVariantRoute(variant)
|
|
19
21
|
const el = ref<HTMLDivElement>()
|
|
20
22
|
useScrollOnActive(isActive, el)
|
|
23
|
+
|
|
24
|
+
// This list only ever shows the current story's variants.
|
|
25
|
+
const storyStore = useStoryStore()
|
|
26
|
+
const errorStore = useStoryErrorStore()
|
|
27
|
+
const error = computed(() => {
|
|
28
|
+
const storyId = storyStore.currentStory?.id
|
|
29
|
+
return storyId ? errorStore.forVariant(storyId, props.variant.id) : undefined
|
|
30
|
+
})
|
|
21
31
|
</script>
|
|
22
32
|
|
|
23
33
|
<template>
|
|
@@ -41,6 +51,13 @@ useScrollOnActive(isActive, el)
|
|
|
41
51
|
}"
|
|
42
52
|
/>
|
|
43
53
|
<span class="truncate">{{ variant.title }}</span>
|
|
54
|
+
<Icon
|
|
55
|
+
v-if="error"
|
|
56
|
+
icon="carbon:warning-alt"
|
|
57
|
+
class="w-4 h-4 flex-none ml-auto text-red-500"
|
|
58
|
+
data-testid="variant-error-marker"
|
|
59
|
+
:aria-label="`Threw while rendering: ${error.message}`"
|
|
60
|
+
/>
|
|
44
61
|
</BaseListItemLink>
|
|
45
62
|
</div>
|
|
46
63
|
</template>
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
<script lang="ts" setup>
|
|
2
2
|
import type { HstEvent } from '../../stores/events'
|
|
3
3
|
import type { Story, Variant } from '../../types'
|
|
4
|
+
import { Icon } from '@iconify/vue'
|
|
5
|
+
import { SANDBOX_ERROR } from '@poveste/shared'
|
|
4
6
|
import { useEventListener } from '@vueuse/core'
|
|
5
7
|
import { computed, onBeforeUnmount, ref, toRaw, watch } from 'vue'
|
|
6
8
|
import { useEventsStore } from '../../stores/events'
|
|
7
9
|
import { usePreviewSettingsStore } from '../../stores/preview-settings'
|
|
8
10
|
import { useStoryStore } from '../../stores/story'
|
|
11
|
+
import { useStoryErrorStore } from '../../stores/story-errors'
|
|
9
12
|
import { EVENT_SEND, PREVIEW_SETTINGS_REQUEST, PREVIEW_SETTINGS_SYNC, SANDBOX_HEIGHT, SANDBOX_READY, SANDBOX_RETARGET, STATE_SYNC } from '../../util/const'
|
|
10
13
|
import { firstReportedHeight } from '../../util/grid-cell-height'
|
|
11
14
|
import { trackWindow } from '../../util/keyboard'
|
|
@@ -24,6 +27,15 @@ const props = withDefaults(defineProps<{
|
|
|
24
27
|
})
|
|
25
28
|
|
|
26
29
|
const settings = usePreviewSettingsStore().currentSettings
|
|
30
|
+
const errorStore = useStoryErrorStore()
|
|
31
|
+
|
|
32
|
+
const storyError = computed(() => errorStore.forVariant(props.story.id, props.variant.id))
|
|
33
|
+
|
|
34
|
+
// The realm re-renders on every retarget, so a stale error must not outlive the
|
|
35
|
+
// render that produced it — otherwise a fixed story stays marked until reload.
|
|
36
|
+
watch(() => [props.story.id, props.variant.id], () => {
|
|
37
|
+
errorStore.clear(props.story.id, props.variant.id)
|
|
38
|
+
})
|
|
27
39
|
|
|
28
40
|
// Iframe
|
|
29
41
|
|
|
@@ -104,6 +116,15 @@ useEventListener(window, 'message', (event) => {
|
|
|
104
116
|
if (!fromCurrentOccupant(event.data)) break
|
|
105
117
|
setPreviewReady()
|
|
106
118
|
break
|
|
119
|
+
case SANDBOX_ERROR:
|
|
120
|
+
if (!fromCurrentOccupant(event.data)) break
|
|
121
|
+
errorStore.report({
|
|
122
|
+
message: event.data.message,
|
|
123
|
+
stack: event.data.stack,
|
|
124
|
+
storyId: props.story.id,
|
|
125
|
+
variantId: props.variant.id,
|
|
126
|
+
})
|
|
127
|
+
break
|
|
107
128
|
case PREVIEW_SETTINGS_REQUEST:
|
|
108
129
|
syncSettings()
|
|
109
130
|
break
|
|
@@ -303,5 +324,28 @@ function onIframeLoad() {
|
|
|
303
324
|
data-testid="preview-iframe"
|
|
304
325
|
@load="onIframeLoad()"
|
|
305
326
|
/>
|
|
327
|
+
|
|
328
|
+
<!--
|
|
329
|
+
Covers the preview rather than sitting beside it: a component that threw
|
|
330
|
+
has rendered something, but nothing about it can be trusted — the template
|
|
331
|
+
below is exactly what makes a broken story look like a working one (#323).
|
|
332
|
+
-->
|
|
333
|
+
<div
|
|
334
|
+
v-if="storyError"
|
|
335
|
+
class="poveste-story-error absolute inset-0 overflow-auto p-6 bg-white dark:bg-gray-900"
|
|
336
|
+
data-testid="story-error"
|
|
337
|
+
>
|
|
338
|
+
<div class="flex items-center gap-2 text-red-600 dark:text-red-400 font-medium">
|
|
339
|
+
<Icon icon="carbon:warning-alt" class="w-5 h-5 flex-none" />
|
|
340
|
+
<span>This component threw while rendering</span>
|
|
341
|
+
</div>
|
|
342
|
+
<p class="mt-2 font-mono text-sm text-red-700 dark:text-red-300" data-testid="story-error-message">
|
|
343
|
+
{{ storyError.message }}
|
|
344
|
+
</p>
|
|
345
|
+
<pre
|
|
346
|
+
v-if="storyError.stack"
|
|
347
|
+
class="mt-4 text-xs whitespace-pre-wrap text-gray-600 dark:text-gray-400"
|
|
348
|
+
>{{ storyError.stack }}</pre>
|
|
349
|
+
</div>
|
|
306
350
|
</StoryResponsivePreview>
|
|
307
351
|
</template>
|
|
@@ -3,6 +3,7 @@ import type { Story } from '../../types'
|
|
|
3
3
|
import { Icon } from '@iconify/vue'
|
|
4
4
|
import { computed, ref } from 'vue'
|
|
5
5
|
import { useRoute } from 'vue-router'
|
|
6
|
+
import { useStoryErrorStore } from '../../stores/story-errors'
|
|
6
7
|
import { useScrollOnActive } from '../../util/scroll'
|
|
7
8
|
import BaseListItemLink from '../base/BaseListItemLink.vue'
|
|
8
9
|
|
|
@@ -13,6 +14,12 @@ const props = withDefaults(defineProps<{
|
|
|
13
14
|
depth: 0,
|
|
14
15
|
})
|
|
15
16
|
|
|
17
|
+
// Only ever means "a variant of this story was seen to throw". A variant that
|
|
18
|
+
// has not rendered cannot have reported, so no marker is not a clean bill of
|
|
19
|
+
// health — see the store, and the open half of #323.
|
|
20
|
+
const errorStore = useStoryErrorStore()
|
|
21
|
+
const hasError = computed(() => errorStore.storyHasError(props.story.id))
|
|
22
|
+
|
|
16
23
|
const filePadding = computed(() => {
|
|
17
24
|
return `${props.depth * 12}px`
|
|
18
25
|
})
|
|
@@ -51,6 +58,14 @@ useScrollOnActive(isActive, el)
|
|
|
51
58
|
<span class="truncate">{{ story.title }}</span>
|
|
52
59
|
</span>
|
|
53
60
|
|
|
61
|
+
<Icon
|
|
62
|
+
v-if="hasError"
|
|
63
|
+
icon="carbon:warning-alt"
|
|
64
|
+
class="w-4 h-4 flex-none ml-auto text-red-500"
|
|
65
|
+
data-testid="story-error-marker"
|
|
66
|
+
aria-label="A variant of this story threw while rendering"
|
|
67
|
+
/>
|
|
68
|
+
|
|
54
69
|
<span
|
|
55
70
|
v-if="!story.docsOnly"
|
|
56
71
|
class="opacity-40 text-sm"
|
package/src/app/sandbox.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { StoryFile } from './types'
|
|
2
|
+
import { reportStoryError } from '@poveste/shared'
|
|
2
3
|
import { usePreferredDark } from '@vueuse/core'
|
|
3
4
|
import { createPinia } from 'pinia'
|
|
4
5
|
import { files, onUpdate } from 'virtual:$poveste-stories'
|
|
@@ -56,6 +57,17 @@ if (window.parent && window.parent !== window) {
|
|
|
56
57
|
window.parent.postMessage({ type: PREVIEW_SETTINGS_REQUEST }, window.location.origin)
|
|
57
58
|
}
|
|
58
59
|
|
|
60
|
+
// The framework hooks catch what the framework swallows; this catches what
|
|
61
|
+
// escapes it — a throw from an event handler, a rejected promise, an async
|
|
62
|
+
// effect. Neither alone is enough: Vue never lets a render error reach the
|
|
63
|
+
// window, and no framework hook sees an error raised outside its own call
|
|
64
|
+
// stack (#323).
|
|
65
|
+
function reportUncaught(error: unknown) {
|
|
66
|
+
reportStoryError(error, { storyId: storyId.value, variantId: variantId.value })
|
|
67
|
+
}
|
|
68
|
+
window.addEventListener('error', event => reportUncaught(event.error ?? event.message))
|
|
69
|
+
window.addEventListener('unhandledrejection', event => reportUncaught(event.reason))
|
|
70
|
+
|
|
59
71
|
// The story preview has its own color scheme, independent from the app chrome.
|
|
60
72
|
// The fallback covers settings stored before the option existed.
|
|
61
73
|
const prefersDark = usePreferredDark()
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { StoryError } from '@poveste/shared'
|
|
2
|
+
import { defineStore } from 'pinia'
|
|
3
|
+
import { computed, ref } from 'vue'
|
|
4
|
+
|
|
5
|
+
// Errors reported by a sandbox while rendering a variant (#323).
|
|
6
|
+
//
|
|
7
|
+
// This only knows about variants that have actually rendered. A variant is not
|
|
8
|
+
// mounted until it scrolls into the grid's window (#103), so the absence of a
|
|
9
|
+
// marker means "not seen to fail", never "fine" — telling a book it is healthy
|
|
10
|
+
// would need a rendering pass, which is the open half of #323.
|
|
11
|
+
export const useStoryErrorStore = defineStore('story-errors', () => {
|
|
12
|
+
const errors = ref(new Map<string, StoryError>())
|
|
13
|
+
|
|
14
|
+
const key = (storyId: string, variantId?: string | null) => `${storyId}::${variantId ?? ''}`
|
|
15
|
+
|
|
16
|
+
function report(error: StoryError) {
|
|
17
|
+
if (!error.storyId) {
|
|
18
|
+
return
|
|
19
|
+
}
|
|
20
|
+
errors.value.set(key(error.storyId, error.variantId), error)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// A variant is re-rendered on every retarget, so last render wins: a story
|
|
24
|
+
// that has been fixed must stop being marked without a reload.
|
|
25
|
+
function clear(storyId: string, variantId?: string | null) {
|
|
26
|
+
errors.value.delete(key(storyId, variantId))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const forVariant = computed(() => (storyId: string, variantId?: string | null) =>
|
|
30
|
+
errors.value.get(key(storyId, variantId)))
|
|
31
|
+
|
|
32
|
+
const storyHasError = computed(() => (storyId: string) => {
|
|
33
|
+
for (const [entry] of errors.value) {
|
|
34
|
+
if (entry.startsWith(`${storyId}::`)) {
|
|
35
|
+
return true
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return false
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
errors,
|
|
43
|
+
report,
|
|
44
|
+
clear,
|
|
45
|
+
forVariant,
|
|
46
|
+
storyHasError,
|
|
47
|
+
}
|
|
48
|
+
})
|