@poveste/app 0.11.0 → 0.12.1

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 (43) hide show
  1. package/dist/app/style/main.pcss +2 -3
  2. package/dist/bundled/App.vue_vue_type_script_setup_true_lang.js +14 -11
  3. package/dist/bundled/app.css +9 -9
  4. package/dist/bundled/components/app/AppActions.vue_vue_type_script_setup_true_lang.js +2 -0
  5. package/dist/bundled/components/app/AppHeader.vue_vue_type_script_setup_true_lang.js +6 -5
  6. package/dist/bundled/components/app/TopBarChipItem.js +5 -2
  7. package/dist/bundled/components/base/BaseCheckbox.vue_vue_type_script_setup_true_lang.js +8 -6
  8. package/dist/bundled/components/base/BaseListItemLink.js +2 -0
  9. package/dist/bundled/components/panel/StatePresets.vue_vue_type_script_setup_true_lang.js +4 -2
  10. package/dist/bundled/components/panel/StoryEvents.vue_vue_type_script_setup_true_lang.js +1 -1
  11. package/dist/bundled/components/panel/StorySourceCode.js +1 -1
  12. package/dist/bundled/components/panel/StorySourceCode.vue_vue_type_script_setup_true_lang.js +1 -1
  13. package/dist/bundled/components/search/SearchItem.js +1 -1
  14. package/dist/bundled/components/search/SearchItem.vue_vue_type_script_setup_true_lang.js +4 -2
  15. package/dist/bundled/components/search/SearchPane.vue_vue_type_script_setup_true_lang.js +69 -33
  16. package/dist/bundled/components/story/StoryResponsivePreview.js +1 -1
  17. package/dist/bundled/components/story/StoryResponsivePreview.vue_vue_type_script_setup_true_lang.js +3 -3
  18. package/dist/bundled/components/story/StoryVariantSinglePreviewRemote.vue_vue_type_script_setup_true_lang.js +2 -1
  19. package/dist/bundled/components/toolbar/DevOnlyToolbarOpenInEditor.vue_vue_type_script_setup_true_lang.js +1 -0
  20. package/dist/bundled/components/toolbar/ToolbarNewTab.vue_vue_type_script_setup_true_lang.js +1 -0
  21. package/dist/bundled/components/toolbar/ToolbarTextDirection.vue_vue_type_script_setup_true_lang.js +1 -0
  22. package/dist/bundled/components/tree/StoryListItem.js +1 -1
  23. package/dist/bundled/components/tree/StoryListItem.vue_vue_type_script_setup_true_lang.js +9 -7
  24. package/dist/style.css +1 -1
  25. package/package.json +9 -8
  26. package/src/app/App.vue +37 -18
  27. package/src/app/components/app/AppActions.vue +2 -0
  28. package/src/app/components/app/AppHeader.vue +7 -2
  29. package/src/app/components/app/TopBarChipItem.vue +3 -0
  30. package/src/app/components/base/BaseCheckbox.vue +4 -1
  31. package/src/app/components/base/BaseListItemLink.vue +1 -0
  32. package/src/app/components/panel/StatePresets.vue +12 -0
  33. package/src/app/components/panel/StoryEvents.vue +2 -2
  34. package/src/app/components/panel/StorySourceCode.vue +5 -2
  35. package/src/app/components/search/SearchItem.vue +2 -0
  36. package/src/app/components/search/SearchPane.vue +42 -0
  37. package/src/app/components/story/StoryResponsivePreview.vue +2 -1
  38. package/src/app/components/story/StoryVariantSinglePreviewRemote.vue +1 -0
  39. package/src/app/components/toolbar/DevOnlyToolbarOpenInEditor.vue +1 -0
  40. package/src/app/components/toolbar/ToolbarNewTab.vue +1 -0
  41. package/src/app/components/toolbar/ToolbarTextDirection.vue +1 -0
  42. package/src/app/components/tree/StoryListItem.vue +2 -0
  43. package/src/app/style/main.pcss +2 -3
@@ -25,7 +25,6 @@
25
25
  its utility classes live in a package v4 never scans. */
26
26
  @source "../../../../poveste/src/node/markdown.ts";
27
27
 
28
- /* TODO(#36): port the v3 typography theme() customization to v4 CSS. */
29
28
  @plugin '@tailwindcss/typography';
30
29
 
31
30
  /* Chrome dark mode. Deliberately a distinct class from the story-side
@@ -181,7 +180,7 @@
181
180
  /*
182
181
  * Typography (@tailwindcss/typography) customization — ported from the v3
183
182
  * `theme.extend.typography` JS config to CSS. Unlayered so it overrides the
184
- * plugin's layered defaults. Needs a visual pass on the docs/markdown pages.
183
+ * plugin's layered defaults.
185
184
  */
186
185
  .prose {
187
186
  --tw-prose-invert-quote-borders: var(--color-gray-800);
@@ -207,7 +206,7 @@
207
206
  }
208
207
  }
209
208
 
210
- &:is(h1, h2, h3, h4, th) {
209
+ &:is(h1, h2, h3, h4) {
211
210
  margin-bottom: 0.75rem;
212
211
 
213
212
  &:not(:first-child) {
@@ -18,7 +18,7 @@ import SearchModal_default from "./components/search/SearchModal.js";
18
18
  import GenericMountStory_default from "./components/story/GenericMountStory.js";
19
19
  import { mapFile } from "./util/mapping.js";
20
20
  import { useTitle } from "@poveste/vendors/vue-use";
21
- import { Transition, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createVNode, defineComponent, normalizeStyle, onMounted, openBlock, ref, resolveComponent, unref, watch, withCtx } from "@poveste/vendors/vue";
21
+ import { Fragment, Transition, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createVNode, defineComponent, normalizeStyle, onMounted, openBlock, ref, resolveComponent, toDisplayString, unref, watch, withCtx } from "@poveste/vendors/vue";
22
22
  import { files, onUpdate, tree } from "virtual:$poveste-stories";
23
23
  //#region src/app/App.vue?vue&type=script&setup=true&lang.ts
24
24
  var _hoisted_1 = { class: "poveste-app-root h-full" };
@@ -31,12 +31,15 @@ var _hoisted_3 = {
31
31
  class: "h-full flex flex-col divide-y divide-gray-100 dark:divide-gray-800"
32
32
  };
33
33
  var _hoisted_4 = { class: "flex items-center gap-2 pr-4" };
34
- var _hoisted_5 = { class: "flex flex-col h-full bg-gray-100 dark:bg-gray-750 __poveste-pane-shadow-from-right" };
35
- var _hoisted_6 = { class: "flex flex-col h-full" };
34
+ var _hoisted_5 = { class: "grow flex flex-col min-h-0" };
35
+ var _hoisted_6 = { class: "flex flex-col h-full bg-gray-100 dark:bg-gray-750 __poveste-pane-shadow-from-right" };
36
36
  var _hoisted_7 = {
37
- key: 2,
38
- class: "h-full flex flex-col"
37
+ "aria-label": "Stories",
38
+ class: "flex-1 flex flex-col min-h-0"
39
39
  };
40
+ var _hoisted_8 = { class: "flex flex-col h-full" };
41
+ var _hoisted_9 = { class: "sr-only" };
42
+ var _hoisted_10 = { class: "h-full flex flex-col" };
40
43
  var App_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineComponent({
41
44
  name: "PovesteApp",
42
45
  setup(__props) {
@@ -118,7 +121,7 @@ var App_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCompon
118
121
  tree: tree$1.value,
119
122
  stories: stories.value
120
123
  }, null, 8, ["tree", "stories"]),
121
- createVNode(_component_RouterView, { class: "grow" })
124
+ createElementVNode("main", _hoisted_5, [createVNode(_component_RouterView, { class: "grow" })])
122
125
  ])) : unref(layoutStore).settings.storyListVisible ? (openBlock(), createBlock(BaseSplitPane_default, {
123
126
  key: 1,
124
127
  "save-id": "main-horiz",
@@ -127,20 +130,20 @@ var App_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCompon
127
130
  "default-split": 15,
128
131
  class: "h-full"
129
132
  }, {
130
- first: withCtx(() => [createElementVNode("div", _hoisted_5, [createVNode(AppHeader_default, { class: "flex-none" }), createVNode(StoryList_default, {
133
+ first: withCtx(() => [createElementVNode("div", _hoisted_6, [createVNode(AppHeader_default, { class: "flex-none" }), createElementVNode("nav", _hoisted_7, [createVNode(StoryList_default, {
131
134
  tree: tree$1.value,
132
135
  stories: stories.value,
133
136
  class: "flex-1"
134
- }, null, 8, ["tree", "stories"])])]),
135
- last: withCtx(() => [createElementVNode("div", _hoisted_6, [createVNode(TopBar_default, {
137
+ }, null, 8, ["tree", "stories"])])])]),
138
+ last: withCtx(() => [createElementVNode("main", _hoisted_8, [createVNode(TopBar_default, {
136
139
  onLayout: _cache[2] || (_cache[2] = ($event) => isLayoutOpen.value = true),
137
140
  onSearch: _cache[3] || (_cache[3] = ($event) => isSearchOpen.value = true)
138
141
  }), createVNode(_component_RouterView, { class: "flex-1 min-h-0" })])]),
139
142
  _: 1
140
- })) : (openBlock(), createElementBlock("div", _hoisted_7, [createVNode(TopBar_default, {
143
+ })) : (openBlock(), createElementBlock(Fragment, { key: 2 }, [createElementVNode("header", _hoisted_9, [createElementVNode("h1", null, toDisplayString(unref(povesteConfig).theme?.title ?? "Poveste"), 1)]), createElementVNode("main", _hoisted_10, [createVNode(TopBar_default, {
141
144
  onLayout: _cache[4] || (_cache[4] = ($event) => isLayoutOpen.value = true),
142
145
  onSearch: _cache[5] || (_cache[5] = ($event) => isSearchOpen.value = true)
143
- }), createVNode(_component_RouterView, { class: "flex-1 min-h-0" })])),
146
+ }), createVNode(_component_RouterView, { class: "flex-1 min-h-0" })])], 64)),
144
147
  !unref(isMobile) ? (openBlock(), createBlock(LayoutModal_default, {
145
148
  key: 3,
146
149
  shown: isLayoutOpen.value,
@@ -1,9 +1,9 @@
1
1
  @scope (:root) to (.__poveste-render-story:not(.__poveste-render-custom-controls)) {
2
- .bind-tree-margin[data-v-a603aebf] {
3
- margin-left: var(--v18724f1f);
2
+ .bind-tree-margin[data-v-184d6cf3] {
3
+ margin-left: var(--v516f2a64);
4
4
  }
5
- .bind-icon-color[data-v-a603aebf] {
6
- color: var(--v45b473db);
5
+ .bind-icon-color[data-v-184d6cf3] {
6
+ color: var(--v2fa7200a);
7
7
  }
8
8
  }
9
9
  @scope (:root) to (.__poveste-render-story:not(.__poveste-render-custom-controls)) {
@@ -93,7 +93,7 @@
93
93
  }
94
94
  }
95
95
  @scope (:root) to (.__poveste-render-story:not(.__poveste-render-custom-controls)) {
96
- .__poveste-code-placeholder[data-v-61bf773e] {
96
+ .__poveste-code-placeholder[data-v-3d88bdb3] {
97
97
  color: inherit;
98
98
  font-size: inherit;
99
99
  }
@@ -108,8 +108,8 @@
108
108
  }
109
109
  }
110
110
  @scope (:root) to (.__poveste-render-story:not(.__poveste-render-custom-controls)) {
111
- .bind-preview-bg[data-v-fa6bb28c] {
112
- background-color: var(--v6d3c2da2);
111
+ .bind-preview-bg[data-v-380b627d] {
112
+ background-color: var(--v0cb511a0);
113
113
  }
114
114
  }
115
115
  @scope (:root) to (.__poveste-render-story:not(.__poveste-render-custom-controls)) {
@@ -149,8 +149,8 @@ img.colorize-black[data-v-f632abed] {
149
149
  }
150
150
  }
151
151
  @scope (:root) to (.__poveste-render-story:not(.__poveste-render-custom-controls)) {
152
- .bind-icon-color[data-v-f8cfc1bd] {
153
- color: var(--v1c6fbe98);
152
+ .bind-icon-color[data-v-f4bf5500] {
153
+ color: var(--v1311c0c1);
154
154
  }
155
155
  }
156
156
  /*$vite$:1*/
@@ -34,6 +34,7 @@ var AppActions_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defin
34
34
  _: 1
35
35
  })), [[_directive_tooltip, unref(makeTooltip)("Layout", ({ isMac }) => isMac ? "meta+shift+l" : "ctrl+shift+l")]]) : createCommentVNode("", true),
36
36
  withDirectives((openBlock(), createBlock(TopBarChipItem_default, {
37
+ "aria-label": "Search stories",
37
38
  "data-testid": "search-btn",
38
39
  onClick: _cache[1] || (_cache[1] = ($event) => _ctx.$emit("search"))
39
40
  }, {
@@ -45,6 +46,7 @@ var AppActions_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defin
45
46
  })), [[_directive_tooltip, unref(makeTooltip)("Search", ({ isMac }) => isMac ? "meta+k" : "ctrl+k")]]),
46
47
  !unref(povesteConfig).theme.hideColorSchemeSwitch ? withDirectives((openBlock(), createBlock(TopBarChipItem_default, {
47
48
  key: 1,
49
+ "aria-label": "Toggle dark mode",
48
50
  onClick: _cache[2] || (_cache[2] = ($event) => unref(toggleDark)())
49
51
  }, {
50
52
  default: withCtx(() => [createVNode(unref(Icon), {
@@ -1,19 +1,20 @@
1
1
  import { povesteConfig } from "../../util/config.js";
2
2
  import AppLogo_default from "./AppLogo.js";
3
- import { createElementBlock, createElementVNode, createVNode, defineComponent, openBlock, unref } from "@poveste/vendors/vue";
3
+ import { createElementBlock, createElementVNode, createVNode, defineComponent, openBlock, toDisplayString, unref } from "@poveste/vendors/vue";
4
4
  //#region src/app/components/app/AppHeader.vue?vue&type=script&setup=true&lang.ts
5
5
  var _hoisted_1 = { class: "poveste-app-header px-4 h-14 flex items-center gap-2" };
6
- var _hoisted_2 = { class: "py-3 sm:py-4 flex-1 h-full flex items-center pr-2" };
7
- var _hoisted_3 = ["href"];
6
+ var _hoisted_2 = { class: "sr-only" };
7
+ var _hoisted_3 = { class: "py-3 sm:py-4 flex-1 h-full flex items-center pr-2" };
8
+ var _hoisted_4 = ["href"];
8
9
  var AppHeader_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineComponent({
9
10
  __name: "AppHeader",
10
11
  setup(__props) {
11
12
  return (_ctx, _cache) => {
12
- return openBlock(), createElementBlock("div", _hoisted_1, [createElementVNode("div", _hoisted_2, [createElementVNode("a", {
13
+ return openBlock(), createElementBlock("header", _hoisted_1, [createElementVNode("h1", _hoisted_2, toDisplayString(unref(povesteConfig).theme?.title ?? "Poveste"), 1), createElementVNode("div", _hoisted_3, [createElementVNode("a", {
13
14
  href: unref(povesteConfig).theme?.logoHref,
14
15
  target: "_blank",
15
16
  class: "w-full h-full flex items-center"
16
- }, [createVNode(AppLogo_default, { class: "max-w-full max-h-full" })], 8, _hoisted_3)])]);
17
+ }, [createVNode(AppLogo_default, { class: "max-w-full max-h-full" })], 8, _hoisted_4)])]);
17
18
  };
18
19
  }
19
20
  });
@@ -2,9 +2,12 @@ import _plugin_vue_export_helper_default from "../../_virtual/_plugin-vue_export
2
2
  import { createElementBlock, openBlock, renderSlot } from "@poveste/vendors/vue";
3
3
  //#region src/app/components/app/TopBarChipItem.vue
4
4
  var _sfc_main = {};
5
- var _hoisted_1 = { 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" };
5
+ var _hoisted_1 = ["role"];
6
6
  function _sfc_render(_ctx, _cache) {
7
- return openBlock(), createElementBlock("a", _hoisted_1, [renderSlot(_ctx.$slots, "default")]);
7
+ return openBlock(), createElementBlock("a", {
8
+ role: _ctx.$attrs.href ? void 0 : "button",
9
+ 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"
10
+ }, [renderSlot(_ctx.$slots, "default")], 8, _hoisted_1);
8
11
  }
9
12
  var TopBarChipItem_default = /*#__PURE__*/ _plugin_vue_export_helper_default(_sfc_main, [["render", _sfc_render]]);
10
13
  //#endregion
@@ -1,13 +1,14 @@
1
1
  import { computed, createElementBlock, createElementVNode, defineComponent, normalizeClass, openBlock, ref, renderSlot, watch, withKeys, withModifiers } from "@poveste/vendors/vue";
2
2
  //#region src/app/components/base/BaseCheckbox.vue?vue&type=script&setup=true&lang.ts
3
- var _hoisted_1 = { class: "text-white w-[16px] h-[16px] relative" };
4
- var _hoisted_2 = {
3
+ var _hoisted_1 = ["aria-checked"];
4
+ var _hoisted_2 = { class: "text-white w-[16px] h-[16px] relative" };
5
+ var _hoisted_3 = {
5
6
  width: "16",
6
7
  height: "16",
7
8
  viewBox: "0 0 24 24",
8
9
  class: "relative z-10"
9
10
  };
10
- var _hoisted_3 = ["stroke-dasharray", "stroke-dashoffset"];
11
+ var _hoisted_4 = ["stroke-dasharray", "stroke-dashoffset"];
11
12
  var BaseCheckbox_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineComponent({
12
13
  __name: "BaseCheckbox",
13
14
  props: { modelValue: {
@@ -18,6 +19,7 @@ var BaseCheckbox_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ def
18
19
  setup(__props, { emit: __emit }) {
19
20
  const props = __props;
20
21
  const emit = __emit;
22
+ const animationEnabled = ref(false);
21
23
  function toggle() {
22
24
  emit("update:modelValue", !props.modelValue);
23
25
  animationEnabled.value = true;
@@ -26,7 +28,6 @@ var BaseCheckbox_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ def
26
28
  const dasharray = ref(0);
27
29
  const progress = computed(() => props.modelValue ? 1 : 0);
28
30
  const dashoffset = computed(() => (1 - progress.value) * dasharray.value);
29
- const animationEnabled = ref(false);
30
31
  watch(path, () => {
31
32
  dasharray.value = path.value.getTotalLength?.() ?? 21.21;
32
33
  });
@@ -34,10 +35,11 @@ var BaseCheckbox_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ def
34
35
  return openBlock(), createElementBlock("div", {
35
36
  role: "checkbox",
36
37
  tabindex: "0",
38
+ "aria-checked": __props.modelValue,
37
39
  class: "poveste-base-checkbox flex items-center gap-2 select-none px-4 py-3 cursor-pointer hover:bg-primary-100 dark:hover:bg-primary-700",
38
40
  onClick: _cache[0] || (_cache[0] = ($event) => toggle()),
39
41
  onKeydown: [_cache[1] || (_cache[1] = withKeys(withModifiers(($event) => toggle(), ["prevent"]), ["enter"])), _cache[2] || (_cache[2] = withKeys(withModifiers(($event) => toggle(), ["prevent"]), ["space"]))]
40
- }, [createElementVNode("div", _hoisted_1, [createElementVNode("div", { class: normalizeClass(["border group-active:bg-gray-500/20 rounded-sm box-border absolute inset-0 transition-border duration-150 ease-out", [__props.modelValue ? "border-primary-500 border-8" : "border-black/25 dark:border-white/25 delay-150"]]) }, null, 2), (openBlock(), createElementBlock("svg", _hoisted_2, [createElementVNode("path", {
42
+ }, [createElementVNode("div", _hoisted_2, [createElementVNode("div", { class: normalizeClass(["border group-active:bg-gray-500/20 rounded-sm box-border absolute inset-0 transition-border duration-150 ease-out", [__props.modelValue ? "border-primary-500 border-8" : "border-black/25 dark:border-white/25 delay-150"]]) }, null, 2), (openBlock(), createElementBlock("svg", _hoisted_3, [createElementVNode("path", {
41
43
  ref_key: "path",
42
44
  ref: path,
43
45
  d: "m 4 12 l 5 5 l 10 -10",
@@ -45,7 +47,7 @@ var BaseCheckbox_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ def
45
47
  class: normalizeClass(["stroke-white stroke-2 duration-200 ease-in-out", [animationEnabled.value ? "transition-all" : "transition-none", { "delay-150": __props.modelValue }]]),
46
48
  "stroke-dasharray": dasharray.value,
47
49
  "stroke-dashoffset": dashoffset.value
48
- }, null, 10, _hoisted_3)]))]), renderSlot(_ctx.$slots, "default")], 32);
50
+ }, null, 10, _hoisted_4)]))]), renderSlot(_ctx.$slots, "default")], 40, _hoisted_1);
49
51
  };
50
52
  }
51
53
  });
@@ -4,6 +4,7 @@ import { createBlock, createElementVNode, mergeProps, normalizeClass, openBlock,
4
4
  //#region src/app/components/base/BaseListItemLink.vue
5
5
  var _hoisted_1 = [
6
6
  "href",
7
+ "aria-current",
7
8
  "onClick",
8
9
  "onKeyup"
9
10
  ];
@@ -12,6 +13,7 @@ function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
12
13
  return openBlock(), createBlock(_component_RouterLink, mergeProps({ class: "poveste-base-list-item-link" }, _ctx.$attrs, { custom: "" }), {
13
14
  default: withCtx(({ isActive: linkIsActive, href, navigate }) => [createElementVNode("a", {
14
15
  href,
16
+ "aria-current": (_ctx.isActive != null ? _ctx.isActive : linkIsActive) ? "page" : void 0,
15
17
  class: normalizeClass(["flex items-center gap-2 text-gray-900 dark:text-gray-100", [_ctx.$attrs.class, (_ctx.isActive != null ? _ctx.isActive : linkIsActive) ? "active bg-primary-500 hover:bg-primary-600 text-white dark:text-black" : "hover:bg-primary-100 dark:hover:bg-primary-900"]]),
16
18
  onClick: ($event) => _ctx.handleNavigate($event, navigate),
17
19
  onKeyup: [withKeys(($event) => _ctx.handleNavigate($event, navigate), ["enter"]), withKeys(($event) => _ctx.handleNavigate($event, navigate), ["space"])]
@@ -26,8 +26,8 @@ var StatePresets_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ def
26
26
  const saveId = computed(() => `${props.story.id}:${props.variant.id}`);
27
27
  const omitKeys = ["_hPropDefs"];
28
28
  const defaultState = toPresetState(props.variant.state, omitKeys);
29
- const selectedOption = useStorage(`_poveste-presets/${saveId.value}/selected`, DEFAULT_ID);
30
- const presetStates = useStorage(`_poveste-presets/${saveId.value}/states`, /* @__PURE__ */ new Map());
29
+ const selectedOption = useStorage(`_poveste-presets/${saveId.value}/selected`, DEFAULT_ID, void 0, { writeDefaults: false });
30
+ const presetStates = useStorage(`_poveste-presets/${saveId.value}/states`, /* @__PURE__ */ new Map(), void 0, { writeDefaults: false });
31
31
  const presetsOptions = computed(() => {
32
32
  const options = { [DEFAULT_ID]: "Initial state" };
33
33
  presetStates.value.forEach((value, key) => {
@@ -125,11 +125,13 @@ var StatePresets_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ def
125
125
  _: 1
126
126
  }, 8, ["modelValue", "options"])], 512),
127
127
  withDirectives(createVNode(unref(Icon), {
128
+ "data-testid": "preset-save",
128
129
  icon: savedNotif.value ? "carbon:checkmark" : "carbon:save",
129
130
  class: normalizeClass(["cursor-pointer w-4 h-4 hover:text-primary-500 dark:hover:text-primary-400 text-gray-900 dark:text-gray-100", [canEdit.value ? "opacity-50 hover:opacity-100" : "opacity-25 pointer-events-none"]]),
130
131
  onClick: _cache[7] || (_cache[7] = ($event) => savePreset())
131
132
  }, null, 8, ["icon", "class"]), [[_directive_tooltip, savedNotif.value ? "Saved!" : canEdit.value ? "Save to preset" : null]]),
132
133
  withDirectives(createVNode(unref(Icon), {
134
+ "data-testid": "preset-create",
133
135
  icon: "carbon:add-alt",
134
136
  class: "cursor-pointer w-4 h-4 hover:text-primary-500 opacity-50 hover:opacity-100 dark:hover:text-primary-400 text-gray-900 dark:text-gray-100",
135
137
  onClick: _cache[8] || (_cache[8] = ($event) => createPreset())
@@ -12,12 +12,12 @@ var StoryEvents_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defi
12
12
  const hasEvents = computed(() => eventsStore.events.length);
13
13
  onMounted(resetUnseen);
14
14
  watch(() => eventsStore.unseen, resetUnseen);
15
+ const eventsElement = ref();
15
16
  async function resetUnseen() {
16
17
  if (eventsStore.unseen > 0) eventsStore.unseen = 0;
17
18
  await nextTick();
18
19
  eventsElement.value.scrollTo({ top: eventsElement.value.scrollHeight });
19
20
  }
20
- const eventsElement = ref();
21
21
  return (_ctx, _cache) => {
22
22
  return openBlock(), createElementBlock("div", {
23
23
  ref_key: "eventsElement",
@@ -2,6 +2,6 @@ import _plugin_vue_export_helper_default from "../../_virtual/_plugin-vue_export
2
2
  import StorySourceCode_vue_vue_type_script_setup_true_lang_default from "./StorySourceCode.vue_vue_type_script_setup_true_lang.js";
3
3
  /* empty css */
4
4
  //#region src/app/components/panel/StorySourceCode.vue
5
- var StorySourceCode_default = /*#__PURE__*/ _plugin_vue_export_helper_default(StorySourceCode_vue_vue_type_script_setup_true_lang_default, [["__scopeId", "data-v-61bf773e"]]);
5
+ var StorySourceCode_default = /*#__PURE__*/ _plugin_vue_export_helper_default(StorySourceCode_vue_vue_type_script_setup_true_lang_default, [["__scopeId", "data-v-3d88bdb3"]]);
6
6
  //#endregion
7
7
  export { StorySourceCode_default as default };
@@ -38,6 +38,7 @@ var StorySourceCode_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/
38
38
  const highlighter = shallowRef();
39
39
  const dynamicSourceCode = ref("");
40
40
  const error = ref(null);
41
+ const displayedSource = ref("dynamic");
41
42
  watch(() => [props.variant, generateSourceCodeFn.value], async () => {
42
43
  if (!generateSourceCodeFn.value) return;
43
44
  error.value = null;
@@ -63,7 +64,6 @@ var StorySourceCode_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/
63
64
  const sourceLoader = props.story.file?.source;
64
65
  if (sourceLoader) staticSourceCode.value = (await sourceLoader()).default;
65
66
  }, { immediate: true });
66
- const displayedSource = ref("dynamic");
67
67
  const displayedSourceCode = computed(() => {
68
68
  if (displayedSource.value === "dynamic") return dynamicSourceCode.value;
69
69
  return staticSourceCode.value;
@@ -2,6 +2,6 @@ import _plugin_vue_export_helper_default from "../../_virtual/_plugin-vue_export
2
2
  import SearchItem_vue_vue_type_script_setup_true_lang_default from "./SearchItem.vue_vue_type_script_setup_true_lang.js";
3
3
  /* empty css */
4
4
  //#region src/app/components/search/SearchItem.vue
5
- var SearchItem_default = /*#__PURE__*/ _plugin_vue_export_helper_default(SearchItem_vue_vue_type_script_setup_true_lang_default, [["__scopeId", "data-v-f8cfc1bd"]]);
5
+ var SearchItem_default = /*#__PURE__*/ _plugin_vue_export_helper_default(SearchItem_vue_vue_type_script_setup_true_lang_default, [["__scopeId", "data-v-f4bf5500"]]);
6
6
  //#endregion
7
7
  export { SearchItem_default as default };
@@ -4,7 +4,7 @@ import BaseListItem_default from "../base/BaseListItem.js";
4
4
  import SearchItemContent_default from "./SearchItemContent.js";
5
5
  import { createBlock, createCommentVNode, createElementBlock, createVNode, defineComponent, openBlock, ref, toRefs, unref, useCssVars, withCtx } from "@poveste/vendors/vue";
6
6
  //#region src/app/components/search/SearchItem.vue?vue&type=script&setup=true&lang.ts
7
- var _hoisted_1 = ["data-selected"];
7
+ var _hoisted_1 = ["aria-selected", "data-selected"];
8
8
  var SearchItem_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineComponent({
9
9
  __name: "SearchItem",
10
10
  props: {
@@ -19,7 +19,7 @@ var SearchItem_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defin
19
19
  },
20
20
  emits: { close: () => true },
21
21
  setup(__props, { emit: __emit }) {
22
- useCssVars((_ctx) => ({ "v1c6fbe98": __props.result.iconColor }));
22
+ useCssVars((_ctx) => ({ "v1311c0c1": __props.result.iconColor }));
23
23
  const props = __props;
24
24
  const emit = __emit;
25
25
  const el = ref();
@@ -33,6 +33,8 @@ var SearchItem_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defin
33
33
  return openBlock(), createElementBlock("div", {
34
34
  ref_key: "el",
35
35
  ref: el,
36
+ role: "option",
37
+ "aria-selected": unref(selected),
36
38
  class: "poveste-search-item",
37
39
  "data-testid": "search-item",
38
40
  "data-selected": unref(selected) ? "" : void 0
@@ -7,16 +7,29 @@ import BaseEmpty_default from "../base/BaseEmpty.js";
7
7
  import { onUpdate, searchData } from "./search-title-data.js";
8
8
  import SearchItem_default from "./SearchItem.js";
9
9
  import { refDebounced, useFocus } from "@poveste/vendors/vue-use";
10
- import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, openBlock, ref, renderList, shallowRef, unref, vModelText, watch, withCtx, withDirectives, withKeys, withModifiers } from "@poveste/vendors/vue";
10
+ import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, openBlock, ref, renderList, shallowRef, toDisplayString, unref, vModelText, watch, withCtx, withDirectives, withKeys, withModifiers } from "@poveste/vendors/vue";
11
11
  import { Icon } from "@poveste/vendors/iconify";
12
12
  import { useRouter } from "@poveste/vendors/vue-router";
13
13
  import Fuse from "fuse.js";
14
14
  import { registeredCommands } from "virtual:$poveste-commands";
15
15
  //#region src/app/components/search/SearchPane.vue?vue&type=script&setup=true&lang.ts
16
- var _hoisted_1 = {
16
+ var _hoisted_1 = [
17
+ "aria-expanded",
18
+ "aria-controls",
19
+ "aria-activedescendant"
20
+ ];
21
+ var _hoisted_2 = {
17
22
  key: 1,
23
+ id: "poveste-search-results",
24
+ role: "listbox",
25
+ "aria-label": "Search results",
18
26
  class: "max-h-[400px] overflow-y-auto rounded-b-lg"
19
27
  };
28
+ var _hoisted_3 = {
29
+ class: "sr-only",
30
+ "aria-live": "polite",
31
+ role: "status"
32
+ };
20
33
  var MAX_RESULTS = 50;
21
34
  var SearchPane_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineComponent({
22
35
  __name: "SearchPane",
@@ -182,6 +195,13 @@ var SearchPane_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defin
182
195
  return list.slice(0, MAX_RESULTS);
183
196
  });
184
197
  const { selectedIndex, selectNext, selectPrevious } = useSelection(results);
198
+ const resultsAnnouncement = computed(() => {
199
+ if (!rateLimitedSearch.value) return "";
200
+ if (!results.value.length) return "No results";
201
+ const count = `${results.value.length} result${results.value.length === 1 ? "" : "s"}`;
202
+ const current = results.value[selectedIndex.value];
203
+ return current ? `${count}. ${current.title}, ${selectedIndex.value + 1} of ${results.value.length}` : count;
204
+ });
185
205
  const router = useRouter();
186
206
  onKeyboardShortcut(["enter"], () => {
187
207
  if (!props.shown) return;
@@ -192,37 +212,53 @@ var SearchPane_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defin
192
212
  close();
193
213
  });
194
214
  return (_ctx, _cache) => {
195
- return openBlock(), createElementBlock(Fragment, null, [createElementVNode("div", {
196
- class: "poveste-search-pane flex items-center gap-4 pl-6 border border-transparent focus-visible:border-primary-500",
197
- onClick: _cache[4] || (_cache[4] = ($event) => focused.value = true)
198
- }, [createVNode(unref(Icon), {
199
- icon: "carbon:search",
200
- class: "flex-none w-4 h-4"
201
- }), withDirectives(createElementVNode("input", {
202
- ref_key: "input",
203
- ref: input,
204
- "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => searchInputText.value = $event),
205
- placeholder: "Search for stories, variants...",
206
- class: "bg-transparent w-full flex-1 pl-0 pr-6 py-4 outline-none",
207
- onKeydown: [
208
- _cache[1] || (_cache[1] = withKeys(withModifiers(($event) => unref(selectNext)(), ["prevent"]), ["down"])),
209
- _cache[2] || (_cache[2] = withKeys(withModifiers(($event) => unref(selectPrevious)(), ["prevent"]), ["up"])),
210
- _cache[3] || (_cache[3] = withKeys(($event) => close(), ["escape"]))
211
- ]
212
- }, null, 544), [[vModelText, searchInputText.value]])]), unref(rateLimitedSearch) && !results.value.length ? (openBlock(), createBlock(BaseEmpty_default, {
213
- key: 0,
214
- class: "no-animation"
215
- }, {
216
- default: withCtx(() => [..._cache[6] || (_cache[6] = [createTextVNode(" No results ", -1)])]),
217
- _: 1
218
- })) : results.value.length ? (openBlock(), createElementBlock("div", _hoisted_1, [(openBlock(true), createElementBlock(Fragment, null, renderList(results.value, (result, index) => {
219
- return openBlock(), createBlock(SearchItem_default, {
220
- key: result.id,
221
- result,
222
- selected: index === unref(selectedIndex),
223
- onClose: _cache[5] || (_cache[5] = ($event) => close())
224
- }, null, 8, ["result", "selected"]);
225
- }), 128))])) : createCommentVNode("", true)], 64);
215
+ return openBlock(), createElementBlock(Fragment, null, [
216
+ createElementVNode("div", {
217
+ class: "poveste-search-pane flex items-center gap-4 pl-6 border border-transparent focus-visible:border-primary-500",
218
+ onClick: _cache[4] || (_cache[4] = ($event) => focused.value = true)
219
+ }, [createVNode(unref(Icon), {
220
+ icon: "carbon:search",
221
+ class: "flex-none w-4 h-4"
222
+ }), withDirectives(createElementVNode("input", {
223
+ ref_key: "input",
224
+ ref: input,
225
+ "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => searchInputText.value = $event),
226
+ type: "search",
227
+ role: "combobox",
228
+ "aria-label": "Search for stories and variants",
229
+ "aria-autocomplete": "list",
230
+ "aria-expanded": results.value.length > 0,
231
+ "aria-controls": results.value.length ? "poveste-search-results" : void 0,
232
+ "aria-activedescendant": results.value.length ? `poveste-search-option-${unref(selectedIndex)}` : void 0,
233
+ placeholder: "Search for stories, variants...",
234
+ class: "bg-transparent w-full flex-1 pl-0 pr-6 py-4 outline-none",
235
+ onKeydown: [
236
+ _cache[1] || (_cache[1] = withKeys(withModifiers(($event) => unref(selectNext)(), ["prevent"]), ["down"])),
237
+ _cache[2] || (_cache[2] = withKeys(withModifiers(($event) => unref(selectPrevious)(), ["prevent"]), ["up"])),
238
+ _cache[3] || (_cache[3] = withKeys(($event) => close(), ["escape"]))
239
+ ]
240
+ }, null, 40, _hoisted_1), [[vModelText, searchInputText.value]])]),
241
+ unref(rateLimitedSearch) && !results.value.length ? (openBlock(), createBlock(BaseEmpty_default, {
242
+ key: 0,
243
+ class: "no-animation"
244
+ }, {
245
+ default: withCtx(() => [..._cache[6] || (_cache[6] = [createTextVNode(" No results ", -1)])]),
246
+ _: 1
247
+ })) : results.value.length ? (openBlock(), createElementBlock("div", _hoisted_2, [(openBlock(true), createElementBlock(Fragment, null, renderList(results.value, (result, index) => {
248
+ return openBlock(), createBlock(SearchItem_default, {
249
+ id: `poveste-search-option-${index}`,
250
+ key: result.id,
251
+ result,
252
+ selected: index === unref(selectedIndex),
253
+ onClose: _cache[5] || (_cache[5] = ($event) => close())
254
+ }, null, 8, [
255
+ "id",
256
+ "result",
257
+ "selected"
258
+ ]);
259
+ }), 128))])) : createCommentVNode("", true),
260
+ createElementVNode("div", _hoisted_3, toDisplayString(resultsAnnouncement.value), 1)
261
+ ], 64);
226
262
  };
227
263
  }
228
264
  });
@@ -2,6 +2,6 @@ import _plugin_vue_export_helper_default from "../../_virtual/_plugin-vue_export
2
2
  import StoryResponsivePreview_vue_vue_type_script_setup_true_lang_default from "./StoryResponsivePreview.vue_vue_type_script_setup_true_lang.js";
3
3
  /* empty css */
4
4
  //#region src/app/components/story/StoryResponsivePreview.vue
5
- var StoryResponsivePreview_default = /*#__PURE__*/ _plugin_vue_export_helper_default(StoryResponsivePreview_vue_vue_type_script_setup_true_lang_default, [["__scopeId", "data-v-fa6bb28c"]]);
5
+ var StoryResponsivePreview_default = /*#__PURE__*/ _plugin_vue_export_helper_default(StoryResponsivePreview_vue_vue_type_script_setup_true_lang_default, [["__scopeId", "data-v-380b627d"]]);
6
6
  //#endregion
7
7
  export { StoryResponsivePreview_default as default };
@@ -23,7 +23,7 @@ var StoryResponsivePreview_vue_vue_type_script_setup_true_lang_default = /*@__PU
23
23
  }
24
24
  },
25
25
  setup(__props) {
26
- useCssVars((_ctx) => ({ "v6d3c2da2": unref(settings).backgroundColor }));
26
+ useCssVars((_ctx) => ({ "v0cb511a0": unref(settings).backgroundColor }));
27
27
  const props = __props;
28
28
  const settings = usePreviewSettingsStore().currentSettings;
29
29
  const resizing = ref(false);
@@ -40,6 +40,7 @@ var StoryResponsivePreview_vue_vue_type_script_setup_true_lang_default = /*@__PU
40
40
  onUnmountedCleanupFns.splice(onUnmountedCleanupFns.indexOf(removeListener), 1);
41
41
  };
42
42
  }
43
+ const previewWrapper = ref();
43
44
  function useDragger(el, value, min, max, axis) {
44
45
  function onMouseDown(event) {
45
46
  event.preventDefault();
@@ -97,7 +98,6 @@ var StoryResponsivePreview_vue_vue_type_script_setup_true_lang_default = /*@__PU
97
98
  const horizontalDragger = ref();
98
99
  const verticalDragger = ref();
99
100
  const cornerDragger = ref();
100
- const previewWrapper = ref();
101
101
  useDragger(horizontalDragger, responsiveWidth, 32, 2e4, "x");
102
102
  useDragger(verticalDragger, responsiveHeight, 32, 2e4, "y");
103
103
  useDragger(cornerDragger, responsiveWidth, 32, 2e4, "x");
@@ -130,7 +130,7 @@ var StoryResponsivePreview_vue_vue_type_script_setup_true_lang_default = /*@__PU
130
130
  finalWidth: finalWidth.value,
131
131
  finalHeight: finalHeight.value,
132
132
  resizing: resizing.value
133
- }, void 0, true)]), isResponsiveEnabled.value ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [_cache[1] || (_cache[1] = createStaticVNode("<div class=\"absolute top-5 left-8 h-2 w-px bg-gray-400/25\" data-v-fa6bb28c></div><div class=\"absolute top-5 right-8 h-2 w-px bg-gray-400/25\" data-v-fa6bb28c></div><div class=\"absolute bottom-5 left-8 h-2 w-px bg-gray-400/25\" data-v-fa6bb28c></div><div class=\"absolute bottom-5 right-8 h-2 w-px bg-gray-400/25\" data-v-fa6bb28c></div><div class=\"absolute left-5 top-8 w-2 h-px bg-gray-400/25\" data-v-fa6bb28c></div><div class=\"absolute left-5 bottom-8 w-2 h-px bg-gray-400/25\" data-v-fa6bb28c></div><div class=\"absolute right-5 top-8 w-2 h-px bg-gray-400/25\" data-v-fa6bb28c></div><div class=\"absolute right-5 bottom-8 w-2 h-px bg-gray-400/25\" data-v-fa6bb28c></div>", 8))], 64)) : createCommentVNode("", true)], 2)], 10, _hoisted_3), isResponsiveEnabled.value ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [
133
+ }, void 0, true)]), isResponsiveEnabled.value ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [_cache[1] || (_cache[1] = createStaticVNode("<div class=\"absolute top-5 left-8 h-2 w-px bg-gray-400/25\" data-v-380b627d></div><div class=\"absolute top-5 right-8 h-2 w-px bg-gray-400/25\" data-v-380b627d></div><div class=\"absolute bottom-5 left-8 h-2 w-px bg-gray-400/25\" data-v-380b627d></div><div class=\"absolute bottom-5 right-8 h-2 w-px bg-gray-400/25\" data-v-380b627d></div><div class=\"absolute left-5 top-8 w-2 h-px bg-gray-400/25\" data-v-380b627d></div><div class=\"absolute left-5 bottom-8 w-2 h-px bg-gray-400/25\" data-v-380b627d></div><div class=\"absolute right-5 top-8 w-2 h-px bg-gray-400/25\" data-v-380b627d></div><div class=\"absolute right-5 bottom-8 w-2 h-px bg-gray-400/25\" data-v-380b627d></div>", 8))], 64)) : createCommentVNode("", true)], 2)], 10, _hoisted_3), isResponsiveEnabled.value ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [
134
134
  withDirectives((openBlock(), createElementBlock("div", {
135
135
  ref_key: "horizontalDragger",
136
136
  ref: horizontalDragger,
@@ -14,7 +14,7 @@ import { computed, createBlock, createCommentVNode, createElementBlock, createEl
14
14
  import { Icon } from "@poveste/vendors/iconify";
15
15
  import { SANDBOX_ERROR } from "@poveste/shared";
16
16
  //#region src/app/components/story/StoryVariantSinglePreviewRemote.vue?vue&type=script&setup=true&lang.ts
17
- var _hoisted_1 = ["src"];
17
+ var _hoisted_1 = ["src", "title"];
18
18
  var _hoisted_2 = {
19
19
  key: 0,
20
20
  class: "poveste-story-error absolute inset-0 overflow-auto p-6 bg-white dark:bg-gray-900",
@@ -222,6 +222,7 @@ var StoryVariantSinglePreviewRemote_vue_vue_type_script_setup_true_lang_default
222
222
  }]),
223
223
  style: normalizeStyle(previewStyle(isResponsiveEnabled, finalWidth, finalHeight)),
224
224
  "data-testid": "preview-iframe",
225
+ title: `Preview of ${__props.story.title} — ${__props.variant.title}`,
225
226
  onLoad: _cache[0] || (_cache[0] = ($event) => onIframeLoad())
226
227
  }, null, 46, _hoisted_1), storyError.value ? (openBlock(), createElementBlock("div", _hoisted_2, [
227
228
  createElementVNode("div", _hoisted_3, [createVNode(unref(Icon), {
@@ -13,6 +13,7 @@ var DevOnlyToolbarOpenInEditor_vue_vue_type_script_setup_true_lang_default = /*@
13
13
  return (_ctx, _cache) => {
14
14
  const _directive_tooltip = resolveDirective("tooltip");
15
15
  return withDirectives((openBlock(), createBlock(TopBarChipItem_default, {
16
+ "aria-label": "Open story in editor",
16
17
  target: "_blank",
17
18
  class: "poveste-toolbar-open-in-editor",
18
19
  onClick: _cache[0] || (_cache[0] = ($event) => unref(openInEditor)(__props.file))
@@ -19,6 +19,7 @@ var ToolbarNewTab_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ de
19
19
  return withDirectives((openBlock(), createBlock(TopBarChipItem_default, {
20
20
  href: sandboxUrl.value,
21
21
  target: "_blank",
22
+ "aria-label": "Open variant in new tab",
22
23
  class: "poveste-toolbar-new-tab"
23
24
  }, {
24
25
  default: withCtx(() => [createVNode(unref(Icon), {
@@ -10,6 +10,7 @@ var ToolbarTextDirection_vue_vue_type_script_setup_true_lang_default = /*@__PURE
10
10
  return (_ctx, _cache) => {
11
11
  const _directive_tooltip = resolveDirective("tooltip");
12
12
  return withDirectives((openBlock(), createBlock(TopBarChipItem_default, {
13
+ "aria-label": "Toggle text direction",
13
14
  class: "poveste-toolbar-text-direction",
14
15
  onClick: _cache[0] || (_cache[0] = ($event) => unref(settings).textDirection = unref(settings).textDirection === "ltr" ? "rtl" : "ltr")
15
16
  }, {
@@ -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-a603aebf"]]);
5
+ var StoryListItem_default = /*#__PURE__*/ _plugin_vue_export_helper_default(StoryListItem_vue_vue_type_script_setup_true_lang_default, [["__scopeId", "data-v-184d6cf3"]]);
6
6
  //#endregion
7
7
  export { StoryListItem_default as default };
@@ -7,10 +7,7 @@ import { useRoute } from "@poveste/vendors/vue-router";
7
7
  //#region src/app/components/tree/StoryListItem.vue?vue&type=script&setup=true&lang.ts
8
8
  var _hoisted_1 = { class: "bind-tree-margin flex items-center gap-2 pl-4 min-w-0" };
9
9
  var _hoisted_2 = { class: "truncate" };
10
- var _hoisted_3 = {
11
- key: 1,
12
- class: "opacity-40 text-sm"
13
- };
10
+ var _hoisted_3 = ["aria-label"];
14
11
  var StoryListItem_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineComponent({
15
12
  __name: "StoryListItem",
16
13
  props: {
@@ -19,8 +16,8 @@ var StoryListItem_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ de
19
16
  },
20
17
  setup(__props) {
21
18
  useCssVars((_ctx) => ({
22
- "v18724f1f": filePadding.value,
23
- "v45b473db": _ctx.story.iconColor
19
+ "v516f2a64": filePadding.value,
20
+ "v2fa7200a": _ctx.story.iconColor
24
21
  }));
25
22
  const props = __props;
26
23
  const errorStore = useStoryErrorStore();
@@ -60,7 +57,12 @@ var StoryListItem_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ de
60
57
  "data-testid": "story-error-marker",
61
58
  "aria-label": "A variant of this story threw while rendering"
62
59
  })) : createCommentVNode("", true),
63
- !__props.story.docsOnly ? (openBlock(), createElementBlock("span", _hoisted_3, toDisplayString(__props.story.variants.length), 1)) : createCommentVNode("", true)
60
+ !__props.story.docsOnly ? (openBlock(), createElementBlock("span", {
61
+ key: 1,
62
+ class: "opacity-40 text-sm",
63
+ "aria-label": `${__props.story.variants.length} ${__props.story.variants.length === 1 ? "variant" : "variants"}`,
64
+ role: "note"
65
+ }, toDisplayString(__props.story.variants.length), 9, _hoisted_3)) : createCommentVNode("", true)
64
66
  ]),
65
67
  _: 1
66
68
  }, 8, ["to"])], 512);
package/dist/style.css CHANGED
@@ -2499,7 +2499,7 @@
2499
2499
  text-decoration: underline;
2500
2500
  }
2501
2501
  }
2502
- &:is(h1, h2, h3, h4, th) {
2502
+ &:is(h1, h2, h3, h4) {
2503
2503
  margin-bottom: 0.75rem;
2504
2504
  &:not(:first-child) {
2505
2505
  margin-top: 1.25rem;
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@poveste/app",
3
3
  "type": "module",
4
- "version": "0.11.0",
4
+ "version": "0.12.1",
5
5
  "description": "Pre-bundled UI for Poveste",
6
6
  "author": {
7
- "name": "Sorin Gitlan"
7
+ "name": "Sorin Gitlan",
8
+ "url": "https://x.com/50rayn"
8
9
  },
9
10
  "license": "MIT",
10
11
  "homepage": "https://poveste.dev",
@@ -38,11 +39,11 @@
38
39
  },
39
40
  "dependencies": {
40
41
  "@fontsource/noto-sans-display": "^5.3.0",
42
+ "@poveste/controls": "^0.12.1",
43
+ "@poveste/shared": "^0.12.1",
44
+ "@poveste/vendors": "^0.12.1",
41
45
  "fuse.js": "^7.0.0",
42
- "shiki": "^3.0.0",
43
- "@poveste/controls": "^0.11.0",
44
- "@poveste/shared": "^0.11.0",
45
- "@poveste/vendors": "^0.11.0"
46
+ "shiki": "^3.0.0"
46
47
  },
47
48
  "devDependencies": {
48
49
  "@iconify-json/carbon": "^1.2.25",
@@ -54,13 +55,13 @@
54
55
  "@tailwindcss/typography": "^0.5.15",
55
56
  "@types/fs-extra": "^11.0.4",
56
57
  "@types/node": "^22.10.1",
57
- "@vitejs/plugin-vue": "^5.2.1",
58
+ "@vitejs/plugin-vue": "^6.0.0",
58
59
  "chokidar": "^4.0.1",
59
60
  "concurrently": "^9.1.0",
60
61
  "fs-extra": "^11.2.0",
61
62
  "globby": "^14.0.2",
62
63
  "pathe": "^1.1.2",
63
- "postcss": "^8.4.49",
64
+ "postcss": "^8.5.23",
64
65
  "postcss-cli": "^11.0.1",
65
66
  "tailwindcss": "^4.3.3",
66
67
  "typescript": "5.6.3",
package/src/app/App.vue CHANGED
@@ -111,6 +111,9 @@ const layoutStore = useLayoutStore()
111
111
 
112
112
  <template>
113
113
  <div class="poveste-app-root h-full">
114
+ <!-- The book is a page and axe's page-level rules apply to it, unlike the
115
+ sandbox, which renders one isolated component (#310). -->
116
+
114
117
  <div
115
118
  v-if="storyStore.currentStory"
116
119
  class="poveste-app hidden"
@@ -144,7 +147,9 @@ const layoutStore = useLayoutStore()
144
147
  :tree="tree"
145
148
  :stories="stories"
146
149
  />
147
- <RouterView class="grow" />
150
+ <main class="grow flex flex-col min-h-0">
151
+ <RouterView class="grow" />
152
+ </main>
148
153
  </div>
149
154
 
150
155
  <BaseSplitPane
@@ -158,34 +163,48 @@ const layoutStore = useLayoutStore()
158
163
  <template #first>
159
164
  <div class="flex flex-col h-full bg-gray-100 dark:bg-gray-750 __poveste-pane-shadow-from-right">
160
165
  <AppHeader class="flex-none" />
161
- <StoryList
162
- :tree="tree"
163
- :stories="stories"
164
- class="flex-1"
165
- />
166
+ <nav
167
+ aria-label="Stories"
168
+ class="flex-1 flex flex-col min-h-0"
169
+ >
170
+ <StoryList
171
+ :tree="tree"
172
+ :stories="stories"
173
+ class="flex-1"
174
+ />
175
+ </nav>
166
176
  </div>
167
177
  </template>
168
178
 
169
179
  <template #last>
170
- <div class="flex flex-col h-full">
180
+ <main class="flex flex-col h-full">
171
181
  <TopBar
172
182
  @layout="isLayoutOpen = true"
173
183
  @search="isSearchOpen = true"
174
184
  />
175
185
  <RouterView class="flex-1 min-h-0" />
176
- </div>
186
+ </main>
177
187
  </template>
178
188
  </BaseSplitPane>
179
- <div
180
- v-else
181
- class="h-full flex flex-col"
182
- >
183
- <TopBar
184
- @layout="isLayoutOpen = true"
185
- @search="isSearchOpen = true"
186
- />
187
- <RouterView class="flex-1 min-h-0" />
188
- </div>
189
+ <template v-else>
190
+ <!--
191
+ The story list is hidden in this layout, and `AppHeader` went with it —
192
+ taking the page's only `h1` and its banner. A heading has to exist in
193
+ every layout, and it has to sit inside a landmark, so this branch
194
+ carries its own (#310).
195
+ -->
196
+ <header class="sr-only">
197
+ <h1>{{ povesteConfig.theme?.title ?? 'Poveste' }}</h1>
198
+ </header>
199
+
200
+ <main class="h-full flex flex-col">
201
+ <TopBar
202
+ @layout="isLayoutOpen = true"
203
+ @search="isSearchOpen = true"
204
+ />
205
+ <RouterView class="flex-1 min-h-0" />
206
+ </main>
207
+ </template>
189
208
 
190
209
  <LayoutModal
191
210
  v-if="!isMobile"
@@ -35,6 +35,7 @@ const themeIcon = computed(() => {
35
35
 
36
36
  <TopBarChipItem
37
37
  v-tooltip="makeTooltip('Search', ({ isMac }) => isMac ? 'meta+k' : 'ctrl+k')"
38
+ aria-label="Search stories"
38
39
  data-testid="search-btn"
39
40
  @click="$emit('search')"
40
41
  >
@@ -47,6 +48,7 @@ const themeIcon = computed(() => {
47
48
  <TopBarChipItem
48
49
  v-if="!povesteConfig.theme.hideColorSchemeSwitch"
49
50
  v-tooltip="makeTooltip('Toggle dark mode', ({ isMac }) => isMac ? 'meta+shift+d' : 'ctrl+shift+d')"
51
+ aria-label="Toggle dark mode"
50
52
  @click="toggleDark()"
51
53
  >
52
54
  <Icon
@@ -4,9 +4,14 @@ import AppLogo from './AppLogo.vue'
4
4
  </script>
5
5
 
6
6
  <template>
7
- <div
7
+ <header
8
8
  class="poveste-app-header px-4 h-14 flex items-center gap-2"
9
9
  >
10
+ <!-- A page needs exactly one h1, and it has to sit inside a landmark or it
11
+ is content outside any region itself (#310). -->
12
+ <h1 class="sr-only">
13
+ {{ povesteConfig.theme?.title ?? 'Poveste' }}
14
+ </h1>
10
15
  <div class="py-3 sm:py-4 flex-1 h-full flex items-center pr-2">
11
16
  <a
12
17
  :href="povesteConfig.theme?.logoHref"
@@ -18,5 +23,5 @@ import AppLogo from './AppLogo.vue'
18
23
  />
19
24
  </a>
20
25
  </div>
21
- </div>
26
+ </header>
22
27
  </template>
@@ -1,5 +1,8 @@
1
1
  <template>
2
+ <!-- Without an href this is not a link, so `aria-label` on it is prohibited and
3
+ it has no accessible role. Chip items are used both ways (#310). -->
2
4
  <a
5
+ :role="$attrs.href ? undefined : 'button'"
3
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"
4
7
  >
5
8
  <slot />
@@ -12,6 +12,9 @@ const emit = defineEmits({
12
12
  'update:modelValue': (_newValue: boolean) => true,
13
13
  })
14
14
 
15
+ // Off until the first toggle, so the tick does not animate itself in on mount.
16
+ const animationEnabled = ref(false)
17
+
15
18
  function toggle() {
16
19
  emit('update:modelValue', !props.modelValue)
17
20
  animationEnabled.value = true
@@ -23,7 +26,6 @@ const path = ref<SVGPathElement>()
23
26
  const dasharray = ref(0)
24
27
  const progress = computed(() => props.modelValue ? 1 : 0)
25
28
  const dashoffset = computed(() => (1 - progress.value) * dasharray.value)
26
- const animationEnabled = ref(false)
27
29
 
28
30
  watch(path, () => {
29
31
  dasharray.value = path.value.getTotalLength?.() ?? 21.21
@@ -34,6 +36,7 @@ watch(path, () => {
34
36
  <div
35
37
  role="checkbox"
36
38
  tabindex="0"
39
+ :aria-checked="modelValue"
37
40
  class="poveste-base-checkbox flex items-center gap-2 select-none px-4 py-3 cursor-pointer hover:bg-primary-100 dark:hover:bg-primary-700"
38
41
  @click="toggle()"
39
42
  @keydown.enter.prevent="toggle()"
@@ -37,6 +37,7 @@ export default defineComponent({
37
37
  >
38
38
  <a
39
39
  :href="href"
40
+ :aria-current="(isActive != null ? isActive : linkIsActive) ? 'page' : undefined"
40
41
  class="flex items-center gap-2 text-gray-900 dark:text-gray-100"
41
42
  :class="[
42
43
  $attrs.class,
@@ -20,14 +20,24 @@ const omitKeys = ['_hPropDefs']
20
20
 
21
21
  const defaultState = toPresetState(props.variant.state, omitKeys)
22
22
 
23
+ /*
24
+ * `writeDefaults: false`: these are keyed per variant, so writing the default on
25
+ * mount persisted "no presets, default selected" for every variant merely
26
+ * browsed. Nothing prunes them, and a renamed variant orphans its keys forever
27
+ * because no code path can match them again (#326).
28
+ */
23
29
  const selectedOption = useStorage<string>(
24
30
  `_poveste-presets/${saveId.value}/selected`,
25
31
  DEFAULT_ID,
32
+ undefined,
33
+ { writeDefaults: false },
26
34
  )
27
35
 
28
36
  const presetStates = useStorage<Map<string, { state: Record<string, unknown>, label: string }>>(
29
37
  `_poveste-presets/${saveId.value}/states`,
30
38
  new Map(),
39
+ undefined,
40
+ { writeDefaults: false },
31
41
  )
32
42
 
33
43
  const presetsOptions = computed(() => {
@@ -176,6 +186,7 @@ onClickOutside(select, stopEditing)
176
186
  </div>
177
187
  <Icon
178
188
  v-tooltip="savedNotif ? 'Saved!' : canEdit ? 'Save to preset' : null"
189
+ data-testid="preset-save"
179
190
  :icon="savedNotif ? 'carbon:checkmark' : 'carbon:save'"
180
191
  class="cursor-pointer w-4 h-4 hover:text-primary-500 dark:hover:text-primary-400 text-gray-900 dark:text-gray-100"
181
192
  :class="[
@@ -185,6 +196,7 @@ onClickOutside(select, stopEditing)
185
196
  />
186
197
  <Icon
187
198
  v-tooltip="'Create new preset'"
199
+ data-testid="preset-create"
188
200
  icon="carbon:add-alt"
189
201
  class="cursor-pointer w-4 h-4 hover:text-primary-500 opacity-50 hover:opacity-100 dark:hover:text-primary-400 text-gray-900 dark:text-gray-100"
190
202
  @click="createPreset()"
@@ -12,6 +12,8 @@ const hasEvents = computed(() => eventsStore.events.length)
12
12
  onMounted(resetUnseen)
13
13
  watch(() => eventsStore.unseen, resetUnseen)
14
14
 
15
+ const eventsElement = ref<HTMLDivElement>()
16
+
15
17
  async function resetUnseen() {
16
18
  if (eventsStore.unseen > 0) {
17
19
  eventsStore.unseen = 0
@@ -19,8 +21,6 @@ async function resetUnseen() {
19
21
  await nextTick()
20
22
  eventsElement.value.scrollTo({ top: eventsElement.value.scrollHeight })
21
23
  }
22
-
23
- const eventsElement = ref<HTMLDivElement>()
24
24
  </script>
25
25
 
26
26
  <template>
@@ -30,6 +30,11 @@ const highlighter = shallowRef<Highlighter>()
30
30
  const dynamicSourceCode = ref('')
31
31
  const error = ref<string>(null)
32
32
 
33
+ // Above the watch that assigns it: the callback is async but runs immediately,
34
+ // and a story whose `slots()` throws reaches the auto-switch below before its
35
+ // first `await` — with the declaration underneath, that path hits the TDZ.
36
+ const displayedSource = ref<'dynamic' | 'static'>('dynamic')
37
+
33
38
  watch(() => [props.variant, generateSourceCodeFn.value], async () => {
34
39
  if (!generateSourceCodeFn.value) return
35
40
  error.value = null
@@ -75,8 +80,6 @@ watch(() => [props.story, props.story?.file?.source], async () => {
75
80
  immediate: true,
76
81
  })
77
82
 
78
- const displayedSource = ref<'dynamic' | 'static'>('dynamic')
79
-
80
83
  const displayedSourceCode = computed(() => {
81
84
  if (displayedSource.value === 'dynamic') {
82
85
  return dynamicSourceCode.value
@@ -39,6 +39,8 @@ function action() {
39
39
  <template>
40
40
  <div
41
41
  ref="el"
42
+ role="option"
43
+ :aria-selected="selected"
42
44
  class="poveste-search-item"
43
45
  data-testid="search-item"
44
46
  :data-selected="selected ? '' : undefined"
@@ -279,6 +279,23 @@ const {
279
279
  selectPrevious,
280
280
  } = useSelection(results)
281
281
 
282
+ // Announced to a screen reader on every change: the count when a search returns
283
+ // something, and which result is focused as the arrows move. Both are silent
284
+ // otherwise — arrowing only changes styling.
285
+ const resultsAnnouncement = computed(() => {
286
+ if (!rateLimitedSearch.value) {
287
+ return ''
288
+ }
289
+ if (!results.value.length) {
290
+ return 'No results'
291
+ }
292
+
293
+ const count = `${results.value.length} result${results.value.length === 1 ? '' : 's'}`
294
+ const current = results.value[selectedIndex.value]
295
+
296
+ return current ? `${count}. ${current.title}, ${selectedIndex.value + 1} of ${results.value.length}` : count
297
+ })
298
+
282
299
  // Activation
283
300
 
284
301
  const router = useRouter()
@@ -321,6 +338,13 @@ onKeyboardShortcut(['enter'], () => {
321
338
  <input
322
339
  ref="input"
323
340
  v-model="searchInputText"
341
+ type="search"
342
+ role="combobox"
343
+ aria-label="Search for stories and variants"
344
+ aria-autocomplete="list"
345
+ :aria-expanded="results.length > 0"
346
+ :aria-controls="results.length ? 'poveste-search-results' : undefined"
347
+ :aria-activedescendant="results.length ? `poveste-search-option-${selectedIndex}` : undefined"
324
348
  placeholder="Search for stories, variants..."
325
349
  class="bg-transparent w-full flex-1 pl-0 pr-6 py-4 outline-none"
326
350
  @keydown.down.prevent="selectNext()"
@@ -338,14 +362,32 @@ onKeyboardShortcut(['enter'], () => {
338
362
 
339
363
  <div
340
364
  v-else-if="results.length"
365
+ id="poveste-search-results"
366
+ role="listbox"
367
+ aria-label="Search results"
341
368
  class="max-h-[400px] overflow-y-auto rounded-b-lg"
342
369
  >
343
370
  <SearchItem
344
371
  v-for="(result, index) of results"
372
+ :id="`poveste-search-option-${index}`"
345
373
  :key="result.id"
346
374
  :result="result"
347
375
  :selected="index === selectedIndex"
348
376
  @close="close()"
349
377
  />
350
378
  </div>
379
+
380
+ <!--
381
+ Arrowing through results was a purely visual state change: `selectedIndex`
382
+ drove styling and nothing else, so a screen reader announced nothing on
383
+ typing, nothing on arrowing, and nothing on selecting (#311). The count is
384
+ what tells you the search did anything at all.
385
+ -->
386
+ <div
387
+ class="sr-only"
388
+ aria-live="polite"
389
+ role="status"
390
+ >
391
+ {{ resultsAnnouncement }}
392
+ </div>
351
393
  </template>
@@ -41,6 +41,8 @@ function addWindowListener(event: string, listener: (event: any) => unknown) {
41
41
  }
42
42
  }
43
43
 
44
+ const previewWrapper = ref<HTMLDivElement>()
45
+
44
46
  function useDragger(el: Ref<HTMLDivElement>, value: Ref<number>, min: number, max: number, axis: 'x' | 'y') {
45
47
  function onMouseDown(event: MouseEvent) {
46
48
  event.preventDefault()
@@ -109,7 +111,6 @@ const responsiveHeight = computed({
109
111
  const horizontalDragger = ref<HTMLDivElement>()
110
112
  const verticalDragger = ref<HTMLDivElement>()
111
113
  const cornerDragger = ref<HTMLDivElement>()
112
- const previewWrapper = ref<HTMLDivElement>()
113
114
 
114
115
  useDragger(horizontalDragger, responsiveWidth, 32, 20000, 'x')
115
116
  useDragger(verticalDragger, responsiveHeight, 32, 20000, 'y')
@@ -322,6 +322,7 @@ function onIframeLoad() {
322
322
  }"
323
323
  :style="previewStyle(isResponsiveEnabled, finalWidth, finalHeight)"
324
324
  data-testid="preview-iframe"
325
+ :title="`Preview of ${story.title} — ${variant.title}`"
325
326
  @load="onIframeLoad()"
326
327
  />
327
328
 
@@ -12,6 +12,7 @@ defineProps<{
12
12
  <template>
13
13
  <TopBarChipItem
14
14
  v-tooltip="tooltip"
15
+ aria-label="Open story in editor"
15
16
  target="_blank"
16
17
  class="poveste-toolbar-open-in-editor"
17
18
  @click="openInEditor(file)"
@@ -20,6 +20,7 @@ const sandboxUrl = computed(() => {
20
20
  v-tooltip="'Open variant in new tab'"
21
21
  :href="sandboxUrl"
22
22
  target="_blank"
23
+ aria-label="Open variant in new tab"
23
24
  class="poveste-toolbar-new-tab"
24
25
  >
25
26
  <Icon
@@ -9,6 +9,7 @@ const settings = usePreviewSettingsStore().currentSettings
9
9
  <template>
10
10
  <TopBarChipItem
11
11
  v-tooltip="`Switch to text direction ${settings.textDirection === 'ltr' ? 'Right to Left' : 'Left to Right'}`"
12
+ aria-label="Toggle text direction"
12
13
  class="poveste-toolbar-text-direction"
13
14
  @click="settings.textDirection = settings.textDirection === 'ltr' ? 'rtl' : 'ltr'"
14
15
  >
@@ -69,6 +69,8 @@ useScrollOnActive(isActive, el)
69
69
  <span
70
70
  v-if="!story.docsOnly"
71
71
  class="opacity-40 text-sm"
72
+ :aria-label="`${story.variants.length} ${story.variants.length === 1 ? 'variant' : 'variants'}`"
73
+ role="note"
72
74
  >
73
75
  {{ story.variants.length }}
74
76
  </span>
@@ -25,7 +25,6 @@
25
25
  its utility classes live in a package v4 never scans. */
26
26
  @source "../../../../poveste/src/node/markdown.ts";
27
27
 
28
- /* TODO(#36): port the v3 typography theme() customization to v4 CSS. */
29
28
  @plugin '@tailwindcss/typography';
30
29
 
31
30
  /* Chrome dark mode. Deliberately a distinct class from the story-side
@@ -181,7 +180,7 @@
181
180
  /*
182
181
  * Typography (@tailwindcss/typography) customization — ported from the v3
183
182
  * `theme.extend.typography` JS config to CSS. Unlayered so it overrides the
184
- * plugin's layered defaults. Needs a visual pass on the docs/markdown pages.
183
+ * plugin's layered defaults.
185
184
  */
186
185
  .prose {
187
186
  --tw-prose-invert-quote-borders: var(--color-gray-800);
@@ -207,7 +206,7 @@
207
206
  }
208
207
  }
209
208
 
210
- &:is(h1, h2, h3, h4, th) {
209
+ &:is(h1, h2, h3, h4) {
211
210
  margin-bottom: 0.75rem;
212
211
 
213
212
  &:not(:first-child) {