srcdev-nuxt-components 9.5.5 → 9.5.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/.claude/skills/component-compliance-checklist.md +6 -2
  2. package/.claude/skills/components/action-menu.md +21 -15
  3. package/.claude/skills/components/deep-expanding-menu.md +18 -6
  4. package/.claude/skills/components/display-tooltip-defined.md +2 -1
  5. package/.claude/skills/components/display-tooltip.md +10 -4
  6. package/.claude/skills/components/navigation-items.md +1 -0
  7. package/.claude/skills/components/opening-hours.md +12 -0
  8. package/.claude/skills/components/pop-over.md +11 -8
  9. package/.claude/skills/components/price-list.md +10 -1
  10. package/.claude/skills/components/responsive-header.md +25 -0
  11. package/.claude/skills/components/select-menu.md +18 -8
  12. package/.claude/skills/components/site-header.md +2 -0
  13. package/.claude/skills/components/ui-block-decorated.md +7 -0
  14. package/.claude/skills/composable-anchored-popover.md +108 -0
  15. package/.claude/skills/composable-tooltips-guide.md +3 -1
  16. package/.claude/skills/index.md +1 -0
  17. package/.claude/skills/storybook-add-story.md +14 -0
  18. package/.vscode/srcdev-component-responsive-header.code-snippets +3 -1
  19. package/.vscode/srcdev-component-site-header.code-snippets +3 -1
  20. package/app/assets/styles/setup/06.utility-classes/animations/_animation-scroller-x.css +5 -3
  21. package/app/assets/styles/setup/06.utility-classes/animations/_auto-rotate.css +5 -3
  22. package/app/assets/styles/setup/06.utility-classes/animations/_entry-exit-blur.css +5 -3
  23. package/app/assets/styles/setup/06.utility-classes/animations/_entry-slide-in.css +5 -3
  24. package/app/assets/styles/setup/06.utility-classes/animations/_entry-zoom-reveal.css +5 -3
  25. package/app/components/01.atoms/animations/entry/CONSUMER-STYLING.md +6 -2
  26. package/app/components/01.atoms/display-tooltip/CONSUMER-STYLING.md +15 -3
  27. package/app/components/01.atoms/display-tooltip/DisplayTooltip.vue +62 -3
  28. package/app/components/01.atoms/display-tooltip/tests/DisplayTooltip.spec.ts +36 -1
  29. package/app/components/01.atoms/display-tooltip/tests/__snapshots__/DisplayTooltip.spec.ts.snap +1 -1
  30. package/app/components/01.atoms/navigation/skip-links/SkipLinks.vue +1 -1
  31. package/app/components/01.atoms/pop-over/CONSUMER-STYLING.md +9 -3
  32. package/app/components/01.atoms/pop-over/PopOver.vue +70 -9
  33. package/app/components/01.atoms/pop-over/tests/PopOver.spec.ts +53 -0
  34. package/app/components/01.atoms/pop-over/tests/__snapshots__/PopOver.spec.ts.snap +1 -1
  35. package/app/components/01.atoms/ui-block-decorated/CONSUMER-STYLING.md +12 -4
  36. package/app/components/01.atoms/ui-block-decorated/UiBlockDecorated.vue +20 -16
  37. package/app/components/01.atoms/ui-block-decorated/stories/UiBlockDecorated.stories.ts +124 -96
  38. package/app/components/02.molecules/action-menu/ActionMenu.vue +41 -16
  39. package/app/components/02.molecules/action-menu/CONSUMER-STYLING.md +16 -3
  40. package/app/components/02.molecules/action-menu/tests/ActionMenu.spec.ts +28 -0
  41. package/app/components/02.molecules/action-menu/tests/__snapshots__/ActionMenu.spec.ts.snap +3 -3
  42. package/app/components/02.molecules/display-tooltip-defined/DisplayTooltipDefined.vue +2 -1
  43. package/app/components/02.molecules/display-tooltip-defined/stories/DisplayTooltipDefined.stories.ts +7 -4
  44. package/app/components/02.molecules/display-tooltip-defined/tests/DisplayTooltipDefined.spec.ts +20 -1
  45. package/app/components/02.molecules/display-tooltip-defined/tests/__snapshots__/DisplayTooltipDefined.spec.ts.snap +1 -1
  46. package/app/components/02.molecules/navigation/deep-expanding-menu/CONSUMER-STYLING.md +18 -4
  47. package/app/components/02.molecules/navigation/deep-expanding-menu/DeepExpandingMenu.vue +87 -16
  48. package/app/components/02.molecules/navigation/deep-expanding-menu/tests/DeepExpandingMenu.spec.ts +83 -1
  49. package/app/components/02.molecules/navigation/deep-expanding-menu/tests/__snapshots__/DeepExpandingMenu.spec.ts.snap +2 -2
  50. package/app/components/02.molecules/navigation/site-navigation/SiteNavigation.vue +1 -1
  51. package/app/components/02.molecules/navigation/tab-navigation/TabNavigation.vue +1 -1
  52. package/app/components/02.molecules/opening-hours/CONSUMER-STYLING.md +11 -1
  53. package/app/components/02.molecules/opening-hours/OpeningHours.vue +71 -43
  54. package/app/components/02.molecules/opening-hours/stories/OpeningHours.stories.ts +138 -6
  55. package/app/components/02.molecules/opening-hours/tests/OpeningHours.spec.ts +53 -0
  56. package/app/components/02.molecules/opening-hours/tests/__snapshots__/OpeningHours.spec.ts.snap +1 -1
  57. package/app/components/02.molecules/opening-hours/tests/buildOpeningHoursSpecification.spec.ts +37 -1
  58. package/app/components/02.molecules/price-list/CONSUMER-STYLING.md +17 -5
  59. package/app/components/02.molecules/price-list/PriceList.vue +32 -9
  60. package/app/components/02.molecules/price-list/stories/PriceList.stories.ts +125 -29
  61. package/app/components/02.molecules/price-list/tests/PriceList.spec.ts +20 -0
  62. package/app/components/02.molecules/select-menu/CONSUMER-STYLING.md +17 -5
  63. package/app/components/02.molecules/select-menu/SelectMenu.vue +51 -16
  64. package/app/components/02.molecules/select-menu/tests/SelectMenu.spec.ts +36 -0
  65. package/app/components/03.organisms/google-reviews/stories/GoogleReviews.stories.ts +9 -4
  66. package/app/components/03.organisms/responsive-header/CONSUMER-STYLING.md +14 -0
  67. package/app/components/03.organisms/responsive-header/NavigationItems.vue +23 -14
  68. package/app/components/03.organisms/responsive-header/ResponsiveHeader.vue +58 -30
  69. package/app/components/03.organisms/responsive-header/stories/ResponsiveHeader.stories.ts +149 -18
  70. package/app/components/03.organisms/responsive-header/tests/.nuxt/app.config.mjs +21 -0
  71. package/app/components/03.organisms/responsive-header/tests/.nuxt/components.d.ts +66 -0
  72. package/app/components/03.organisms/responsive-header/tests/.nuxt/imports.d.ts +30 -0
  73. package/app/components/03.organisms/responsive-header/tests/.nuxt/manifest/meta/test.json +1 -0
  74. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/app.config.d.ts +35 -0
  75. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/build.d.ts +23 -0
  76. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/builder-env.d.ts +1 -0
  77. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/components.d.ts +71 -0
  78. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/imports.d.ts +386 -0
  79. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/middleware.d.ts +11 -0
  80. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/modules.d.ts +79 -0
  81. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/nitro-layouts.d.ts +17 -0
  82. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/nitro-nuxt.d.ts +64 -0
  83. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/plugins.d.ts +29 -0
  84. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/runtime-config.d.ts +32 -0
  85. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/shared-imports.d.ts +10 -0
  86. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/vue-shim.d.ts +0 -0
  87. package/app/components/03.organisms/responsive-header/tests/NavigationItems.spec.ts +16 -0
  88. package/app/components/03.organisms/responsive-header/tests/ResponsiveHeader.spec.ts +91 -6
  89. package/app/components/03.organisms/responsive-header/tests/__snapshots__/ResponsiveHeader.spec.ts.snap +9 -9
  90. package/app/components/03.organisms/responsive-header/tests/node_modules/.cache/nuxt/chrome-workspace.json +1 -0
  91. package/app/components/03.organisms/site-header/SiteHeader.vue +8 -0
  92. package/app/components/03.organisms/site-header/tests/__snapshots__/SiteHeader.spec.ts.snap +1 -1
  93. package/app/composables/tests/useAnchoredPopover.spec.ts +231 -0
  94. package/app/composables/tests/useTooltips.spec.ts +69 -0
  95. package/app/composables/useAnchoredPopover.ts +183 -0
  96. package/app/composables/useTooltips.ts +37 -20
  97. package/app/utils/opening-hours.ts +23 -2
  98. package/package.json +1 -1
@@ -0,0 +1,69 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import { mountSuspended } from "@nuxt/test-utils/runtime";
3
+ import { defineComponent, h, nextTick, ref } from "vue";
4
+ import { useTooltipsGuide } from "../useTooltips";
5
+ import DisplayTooltipDefined from "~/components/02.molecules/display-tooltip-defined/DisplayTooltipDefined.vue";
6
+
7
+ const flush = async () => {
8
+ for (let i = 0; i < 5; i++) await nextTick();
9
+ };
10
+
11
+ const mountGuide = () =>
12
+ mountSuspended(
13
+ defineComponent({
14
+ setup() {
15
+ const containerRef = ref<HTMLElement | null>(null);
16
+ const guide = useTooltipsGuide(containerRef, { autoStart: false, startDelay: 0 });
17
+ return { containerRef, guide };
18
+ },
19
+ render() {
20
+ return h("div", { ref: "containerRef" }, [
21
+ h(DisplayTooltipDefined, { tooltipId: "one", contentText: { tooltipTitle: { tag: "h4", text: "One" } } }),
22
+ h(DisplayTooltipDefined, { tooltipId: "two", contentText: { tooltipTitle: { tag: "h4", text: "Two" } } }),
23
+ ]);
24
+ },
25
+ }),
26
+ { attachTo: document.body }
27
+ );
28
+
29
+ describe("useTooltipsGuide without the Popover API", () => {
30
+ beforeEach(() => {
31
+ Object.defineProperty(HTMLElement.prototype, "showPopover", { value: undefined, writable: true, configurable: true });
32
+ });
33
+
34
+ afterEach(() => {
35
+ delete (HTMLElement.prototype as unknown as Record<string, unknown>)["showPopover"];
36
+ document.body.innerHTML = "";
37
+ });
38
+
39
+ it("steps through each tooltip as its close button is clicked", async () => {
40
+ const wrapper = await mountGuide();
41
+ const vm = wrapper.vm as unknown as { guide: ReturnType<typeof useTooltipsGuide> };
42
+ const popovers = () => wrapper.findAll(".display-tooltip-popover");
43
+
44
+ vm.guide.initializePopovers();
45
+ void vm.guide.startGuide();
46
+ await flush();
47
+ expect(popovers()[0]!.classes()).toContain("display-tooltip-popover-open");
48
+
49
+ await wrapper.findAll(".display-tooltip-close-button")[0]!.trigger("click");
50
+ await flush();
51
+ expect(popovers()[0]!.classes()).not.toContain("display-tooltip-popover-open");
52
+ expect(popovers()[1]!.classes()).toContain("display-tooltip-popover-open");
53
+ expect(vm.guide.currentTooltipIndex.value).toBe(1);
54
+ });
55
+
56
+ it("stopGuide closes the open tooltip without calling Popover API methods", async () => {
57
+ const wrapper = await mountGuide();
58
+ const vm = wrapper.vm as unknown as { guide: ReturnType<typeof useTooltipsGuide> };
59
+
60
+ vm.guide.initializePopovers();
61
+ void vm.guide.startGuide();
62
+ await flush();
63
+
64
+ expect(() => vm.guide.stopGuide()).not.toThrow();
65
+ await flush();
66
+ expect(wrapper.find(".display-tooltip-popover-open").exists()).toBe(false);
67
+ expect(vm.guide.isGuideRunning.value).toBe(false);
68
+ });
69
+ });
@@ -0,0 +1,183 @@
1
+ import type { MaybeRefOrGetter, Ref } from "vue";
2
+
3
+ interface AnchoredPopoverOptions {
4
+ rootRef: Readonly<Ref<HTMLElement | null>>;
5
+ triggerRef: Readonly<Ref<HTMLElement | null>>;
6
+ popoverRef: Readonly<Ref<HTMLElement | null>>;
7
+ side?: MaybeRefOrGetter<AnchoredPopoverSide>;
8
+ onOpen?: () => void;
9
+ }
10
+
11
+ export type AnchoredPopoverSide = "top" | "right" | "bottom" | "left";
12
+
13
+ type PopoverToggleEvent = Event & { newState?: string };
14
+
15
+ const OPPOSITE_SIDE: Record<AnchoredPopoverSide, AnchoredPopoverSide> = {
16
+ top: "bottom",
17
+ bottom: "top",
18
+ left: "right",
19
+ right: "left",
20
+ };
21
+
22
+ // Native popover + anchor positioning where supported; JS positioning without anchor positioning
23
+ // (Safari 17-18); JS open/close, outside click and Escape without the Popover API (Safari 16).
24
+ export function useAnchoredPopover({ rootRef, triggerRef, popoverRef, side = "bottom", onOpen }: AnchoredPopoverOptions) {
25
+ const isOpen = ref(false);
26
+ const usesFallbackPopover = ref(false);
27
+ const needsPositioning = ref(false);
28
+ const placement = ref<AnchoredPopoverSide>(toValue(side));
29
+ const anchor = ref({ top: 0, bottom: 0, left: 0, right: 0, viewportWidth: 0, viewportHeight: 0 });
30
+
31
+ // `--_anchor-{edge}` mirrors anchor({edge}) for top/left; `-inverse` is the same edge measured
32
+ // from the viewport's bottom/right, for bottom/right.
33
+ const positionStyle = computed(() => {
34
+ if (!needsPositioning.value) return undefined;
35
+ const { top, bottom, left, right, viewportWidth, viewportHeight } = anchor.value;
36
+ return {
37
+ "--_anchor-top": `${top}px`,
38
+ "--_anchor-bottom": `${bottom}px`,
39
+ "--_anchor-left": `${left}px`,
40
+ "--_anchor-right": `${right}px`,
41
+ "--_anchor-top-inverse": `${viewportHeight - top}px`,
42
+ "--_anchor-bottom-inverse": `${viewportHeight - bottom}px`,
43
+ "--_anchor-left-inverse": `${viewportWidth - left}px`,
44
+ "--_anchor-right-inverse": `${viewportWidth - right}px`,
45
+ };
46
+ });
47
+
48
+ const popoverPlacement = computed(() => (needsPositioning.value ? placement.value : undefined));
49
+
50
+ const updatePosition = () => {
51
+ const trigger = triggerRef.value;
52
+ if (!trigger) return;
53
+ const rect = trigger.getBoundingClientRect();
54
+ const viewportWidth = document.documentElement.clientWidth;
55
+ const viewportHeight = document.documentElement.clientHeight;
56
+ const width = popoverRef.value?.offsetWidth ?? 0;
57
+ const height = popoverRef.value?.offsetHeight ?? 0;
58
+
59
+ const fits: Record<AnchoredPopoverSide, boolean> = {
60
+ bottom: rect.bottom + height <= viewportHeight,
61
+ top: rect.top - height >= 0,
62
+ right: rect.right + width <= viewportWidth,
63
+ left: rect.left - width >= 0,
64
+ };
65
+
66
+ anchor.value = { top: rect.top, bottom: rect.bottom, left: rect.left, right: rect.right, viewportWidth, viewportHeight };
67
+ const preferred = toValue(side);
68
+ placement.value = fits[preferred] || !fits[OPPOSITE_SIDE[preferred]] ? preferred : OPPOSITE_SIDE[preferred];
69
+ };
70
+
71
+ let frame = 0;
72
+ const schedulePositionUpdate = () => {
73
+ if (frame) return;
74
+ frame = requestAnimationFrame(() => {
75
+ frame = 0;
76
+ updatePosition();
77
+ });
78
+ };
79
+
80
+ const handleDocumentPointerdown = (event: PointerEvent) => {
81
+ if (!rootRef.value?.contains(event.target as Node)) hide();
82
+ };
83
+
84
+ const handleDocumentKeydown = (event: KeyboardEvent) => {
85
+ if (event.key !== "Escape") return;
86
+ hide();
87
+ triggerRef.value?.focus();
88
+ };
89
+
90
+ const addListeners = () => {
91
+ if (needsPositioning.value) {
92
+ window.addEventListener("scroll", schedulePositionUpdate, { capture: true, passive: true });
93
+ window.addEventListener("resize", schedulePositionUpdate, { passive: true });
94
+ }
95
+ if (usesFallbackPopover.value) {
96
+ document.addEventListener("pointerdown", handleDocumentPointerdown);
97
+ document.addEventListener("keydown", handleDocumentKeydown);
98
+ }
99
+ };
100
+
101
+ const removeListeners = () => {
102
+ window.removeEventListener("scroll", schedulePositionUpdate, { capture: true });
103
+ window.removeEventListener("resize", schedulePositionUpdate);
104
+ document.removeEventListener("pointerdown", handleDocumentPointerdown);
105
+ document.removeEventListener("keydown", handleDocumentKeydown);
106
+ if (frame) cancelAnimationFrame(frame);
107
+ frame = 0;
108
+ };
109
+
110
+ const afterOpen = () => {
111
+ if (needsPositioning.value) updatePosition();
112
+ addListeners();
113
+ };
114
+
115
+ const show = () => {
116
+ if (!usesFallbackPopover.value) {
117
+ popoverRef.value?.showPopover();
118
+ return;
119
+ }
120
+ if (isOpen.value) return;
121
+ if (needsPositioning.value) updatePosition();
122
+ isOpen.value = true;
123
+ nextTick(() => {
124
+ afterOpen();
125
+ onOpen?.();
126
+ });
127
+ };
128
+
129
+ const hide = () => {
130
+ if (!usesFallbackPopover.value) {
131
+ popoverRef.value?.hidePopover();
132
+ return;
133
+ }
134
+ if (!isOpen.value) return;
135
+ // Matches native popovers, which return focus to the invoker when focus was inside.
136
+ if (popoverRef.value?.contains(document.activeElement)) triggerRef.value?.focus();
137
+ isOpen.value = false;
138
+ removeListeners();
139
+ };
140
+
141
+ /** Bound to the trigger's click. A no-op where `popovertarget` already toggles natively. */
142
+ const handleTriggerClick = () => {
143
+ if (!usesFallbackPopover.value) return;
144
+ if (isOpen.value) hide();
145
+ else show();
146
+ };
147
+
148
+ /** Bound to the popover's `beforetoggle`, so the first open frame is already in place. */
149
+ const handleBeforeToggle = (event: Event) => {
150
+ if (needsPositioning.value && (event as PopoverToggleEvent).newState === "open") updatePosition();
151
+ };
152
+
153
+ /** Bound to the popover's `toggle`. */
154
+ const handleToggle = (event: Event) => {
155
+ isOpen.value = (event as PopoverToggleEvent).newState === "open";
156
+ if (isOpen.value) {
157
+ afterOpen();
158
+ onOpen?.();
159
+ } else {
160
+ removeListeners();
161
+ }
162
+ };
163
+
164
+ onMounted(() => {
165
+ usesFallbackPopover.value = typeof HTMLElement.prototype.showPopover !== "function";
166
+ needsPositioning.value = typeof CSS !== "undefined" && typeof CSS.supports === "function" && !CSS.supports("anchor-name: --a");
167
+ });
168
+
169
+ onBeforeUnmount(removeListeners);
170
+
171
+ return {
172
+ isOpen,
173
+ usesFallbackPopover,
174
+ needsPositioning,
175
+ positionStyle,
176
+ popoverPlacement,
177
+ show,
178
+ hide,
179
+ handleTriggerClick,
180
+ handleBeforeToggle,
181
+ handleToggle,
182
+ };
183
+ }
@@ -32,23 +32,40 @@ export const useTooltipsGuide = (
32
32
  })
33
33
  }
34
34
 
35
+ const findTrigger = (popover: HTMLElement) =>
36
+ document.querySelector<HTMLElement>(`[popovertarget="${popover.id}"][popovertargetaction="toggle"]`)
37
+
38
+ // Without the Popover API (Safari 16) there's no togglePopover() and :popover-open throws, so
39
+ // tooltips are driven through their trigger, whose aria-expanded tracks the open state everywhere.
40
+ const closePopover = (popover: HTMLElement) => {
41
+ const trigger = findTrigger(popover)
42
+ if (trigger?.getAttribute("aria-expanded") === "true") {
43
+ trigger.click()
44
+ return
45
+ }
46
+ if (typeof popover.hidePopover !== "function") return
47
+ try {
48
+ if (popover.matches(":popover-open")) popover.hidePopover()
49
+ } catch {
50
+ // :popover-open unsupported
51
+ }
52
+ }
53
+
35
54
  /**
36
55
  * Show a tooltip and wait for user dismissal
37
56
  */
38
57
  const showTooltipAndWaitForDismiss = (popover: HTMLElement): Promise<void> => {
39
58
  return new Promise((resolve) => {
40
- // Find the trigger button that corresponds to this popover
41
- const popoverId = popover.id
42
- const triggerButton = document.querySelector<HTMLElement>(
43
- `[popovertarget="${popoverId}"][popovertargetaction="toggle"]`
44
- )
59
+ const triggerButton = findTrigger(popover)
45
60
 
46
61
  // Use the trigger button to show the popover to maintain proper anchor relationship
47
62
  if (triggerButton) {
48
63
  triggerButton.click()
49
- } else {
50
- // Fallback if no trigger button found
64
+ } else if (typeof popover.togglePopover === "function") {
51
65
  popover.togglePopover(true)
66
+ } else {
67
+ resolve()
68
+ return
52
69
  }
53
70
 
54
71
  // Find the close button within this popover
@@ -62,10 +79,18 @@ export const useTooltipsGuide = (
62
79
  }
63
80
 
64
81
  closeButton.addEventListener("click", handleClose)
82
+ } else if (triggerButton) {
83
+ // No close button: resolve once the trigger reports the tooltip closed
84
+ const observer = new MutationObserver(() => {
85
+ if (triggerButton.getAttribute("aria-expanded") === "false") {
86
+ observer.disconnect()
87
+ resolve()
88
+ }
89
+ })
90
+ observer.observe(triggerButton, { attributes: true, attributeFilter: ["aria-expanded"] })
65
91
  } else {
66
- // Fallback: listen for the popover to be hidden
67
- const handleToggle = () => {
68
- if (!popover.matches(":popover-open")) {
92
+ const handleToggle = (event: Event) => {
93
+ if ((event as Event & { newState?: string }).newState === "closed") {
69
94
  popover.removeEventListener("toggle", handleToggle)
70
95
  resolve()
71
96
  }
@@ -105,11 +130,7 @@ export const useTooltipsGuide = (
105
130
  if (isGuideRunning.value) return
106
131
 
107
132
  // Close any currently open popovers
108
- popovers.forEach((popover) => {
109
- if (popover.matches(":popover-open")) {
110
- popover.togglePopover(false)
111
- }
112
- })
133
+ popovers.forEach(closePopover)
113
134
 
114
135
  // Reset state and start the guide
115
136
  autoRunGuide.value = true
@@ -123,11 +144,7 @@ export const useTooltipsGuide = (
123
144
  if (!isGuideRunning.value) return
124
145
 
125
146
  // Close any currently open popovers
126
- popovers.forEach((popover) => {
127
- if (popover.matches(":popover-open")) {
128
- popover.togglePopover(false)
129
- }
130
- })
147
+ popovers.forEach(closePopover)
131
148
 
132
149
  isGuideRunning.value = false
133
150
  autoRunGuide.value = false
@@ -8,8 +8,29 @@ import type {
8
8
 
9
9
  const SCHEMA_DAYS = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
10
10
 
11
- export const resolveOpeningStatus = (entry: { status?: OpeningStatus; sessions?: OpeningSession[] }): OpeningStatus =>
12
- entry.status ?? (entry.sessions?.length ? "open" : "closed");
11
+ // "HH:MM" from 00:00 to 23:59, else null so the caller can show the raw value instead of throwing
12
+ export const parseOpeningTime = (time: string): { hours: number; minutes: number } | null => {
13
+ const match = /^(\d{1,2}):(\d{2})$/.exec(time.trim());
14
+ if (!match) return null;
15
+ const hours = Number(match[1]);
16
+ const minutes = Number(match[2]);
17
+ return hours < 24 && minutes < 60 ? { hours, minutes } : null;
18
+ };
19
+
20
+ // A real calendar date in "YYYY-MM-DD" form, as a UTC midnight Date, else null
21
+ export const parseOpeningDate = (iso: string): Date | null => {
22
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(iso.trim());
23
+ if (!match) return null;
24
+ const [y, m, d] = [Number(match[1]), Number(match[2]), Number(match[3])];
25
+ const date = new Date(Date.UTC(y, m - 1, d));
26
+ return date.getUTCFullYear() === y && date.getUTCMonth() === m - 1 && date.getUTCDate() === d ? date : null;
27
+ };
28
+
29
+ // "open" with no sessions has nothing to show, so it's treated as closed
30
+ export const resolveOpeningStatus = (entry: { status?: OpeningStatus; sessions?: OpeningSession[] }): OpeningStatus => {
31
+ const status = entry.status ?? "open";
32
+ return status === "open" && !entry.sessions?.length ? "closed" : status;
33
+ };
13
34
 
14
35
  const specsFor = (
15
36
  entry: { status?: OpeningStatus; sessions?: OpeningSession[] },
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "srcdev-nuxt-components",
3
3
  "type": "module",
4
- "version": "9.5.5",
4
+ "version": "9.5.7",
5
5
  "main": "nuxt.config.ts",
6
6
  "types": "types.d.ts",
7
7
  "license": "MIT",