@chesapeake/stack 0.1.6 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -1
- package/README.md +3 -1
- package/components/retail/HoursStatusStrip.tsx +55 -0
- package/components/retail/LocationHero.tsx +64 -0
- package/components/retail/StickyShopBar.tsx +44 -0
- package/components/retail/StoreSwitcher.tsx +122 -0
- package/components/retail/hours.ts +56 -0
- package/components/retail/index.ts +16 -0
- package/index.ts +2 -0
- package/lib/githubIssue.ts +3 -1
- package/package.json +7 -1
- package/types/retail-location.ts +73 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
|
6
6
|
and this project adheres to
|
|
7
7
|
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
8
8
|
|
|
9
|
+
## [0.1.7] - 2026-10-11
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Retail P0 blocks in `components/retail/`: `LocationHero`,
|
|
14
|
+
`HoursStatusStrip`, `StickyShopBar`, and `StoreSwitcher`
|
|
15
|
+
- `types/retail-location.ts` — `RetailLocation` and supporting types
|
|
16
|
+
(`Address`, `WeeklyHours`, `FaqItem`, `CommercePlatform`)
|
|
17
|
+
- `examples/retail-location-page.tsx` composing the P0 blocks
|
|
18
|
+
- `isOpenAt` and `hoursForDay` helpers for local weekly hours
|
|
19
|
+
|
|
9
20
|
## [0.1.6] - 2026-09-19
|
|
10
21
|
|
|
11
22
|
### Added
|
|
@@ -90,8 +101,10 @@ and this project adheres to
|
|
|
90
101
|
- Full dark mode support via CSS custom properties
|
|
91
102
|
- Apache 2.0 license
|
|
92
103
|
|
|
93
|
-
[0.1.
|
|
104
|
+
[0.1.7]: https://github.com/chesapeakedev/stack/releases/tag/v0.1.7
|
|
94
105
|
[0.1.6]: https://github.com/chesapeakedev/stack/releases/tag/v0.1.6
|
|
106
|
+
[0.1.5]: https://github.com/chesapeakedev/stack/releases/tag/v0.1.5
|
|
107
|
+
[0.1.4]: https://github.com/chesapeakedev/stack/releases/tag/v0.1.4
|
|
95
108
|
[0.1.3]: https://github.com/chesapeakedev/stack/releases/tag/v0.1.3
|
|
96
109
|
[0.1.2]: https://github.com/chesapeakedev/stack/releases/tag/v0.1.2
|
|
97
110
|
[0.1.1]: https://github.com/chesapeakedev/stack/releases/tag/v0.1.1
|
package/README.md
CHANGED
|
@@ -335,6 +335,7 @@ installed and configured.
|
|
|
335
335
|
| [`feedback-and-notifications.tsx`](https://github.com/chesapeakedev/stack/blob/main/examples/feedback-and-notifications.tsx) | FeedbackDrawer, toast variants (success / error / promise), confirmation Dialog |
|
|
336
336
|
| [`data-display.tsx`](https://github.com/chesapeakedev/stack/blob/main/examples/data-display.tsx) | Cards, badges, avatars, tooltips, dropdown menus, and EmptyState |
|
|
337
337
|
| [`user-management.tsx`](https://github.com/chesapeakedev/stack/blob/main/examples/user-management.tsx) | UserSelector multi-select search, FriendList with presence indicators |
|
|
338
|
+
| [`retail-location-page.tsx`](https://github.com/chesapeakedev/stack/blob/main/examples/retail-location-page.tsx) | LocationHero, HoursStatusStrip, StoreSwitcher, and StickyShopBar |
|
|
338
339
|
|
|
339
340
|
## Package Contents
|
|
340
341
|
|
|
@@ -344,7 +345,8 @@ When installed via npm you get:
|
|
|
344
345
|
@chesapeake/stack/
|
|
345
346
|
├── components/
|
|
346
347
|
│ ├── ui/ # Primitive UI components (Button, Input, Card, …)
|
|
347
|
-
│
|
|
348
|
+
│ ├── ux/ # Higher-level UX components (auth, layout, feedback)
|
|
349
|
+
│ └── retail/ # Location page blocks (hero, hours, store switcher)
|
|
348
350
|
├── styles/
|
|
349
351
|
│ ├── index.css # Main stylesheet entry point
|
|
350
352
|
│ ├── tokens.css # Design tokens (CSS custom properties)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Copyright 2026 Chesapeake Computing
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import type { WeeklyHours } from "../../types/retail-location.ts";
|
|
5
|
+
import { Badge } from "../ui/badge.tsx";
|
|
6
|
+
import { cn } from "../../lib/utils.ts";
|
|
7
|
+
import { hoursForDay, isOpenAt } from "./hours.ts";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Props for {@link HoursStatusStrip}.
|
|
11
|
+
*/
|
|
12
|
+
export interface HoursStatusStripProps {
|
|
13
|
+
/** Weekly hours. A `null` day is closed. */
|
|
14
|
+
hours: WeeklyHours;
|
|
15
|
+
/**
|
|
16
|
+
* Instant used to decide open/closed and which day's range to show.
|
|
17
|
+
* Defaults to the current local time.
|
|
18
|
+
*/
|
|
19
|
+
now?: Date;
|
|
20
|
+
/** Pre-order cutoff label, already formatted (for example `21:00`). */
|
|
21
|
+
preOrderCutoff?: string;
|
|
22
|
+
className?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Open/closed badge, today's hours, and an optional pre-order cutoff.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```tsx
|
|
30
|
+
* <HoursStatusStrip
|
|
31
|
+
* hours={location.hours}
|
|
32
|
+
* preOrderCutoff={location.status?.preOrderCutoff}
|
|
33
|
+
* />
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export function HoursStatusStrip({
|
|
37
|
+
hours,
|
|
38
|
+
now = new Date(),
|
|
39
|
+
preOrderCutoff,
|
|
40
|
+
className,
|
|
41
|
+
}: HoursStatusStripProps) {
|
|
42
|
+
const open = isOpenAt(hours, now);
|
|
43
|
+
const today = hoursForDay(hours, now);
|
|
44
|
+
const range = today ? `${today.open}–${today.close}` : "Closed today";
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div className={cn("flex flex-wrap items-center gap-2 text-sm", className)}>
|
|
48
|
+
<Badge variant={open ? "default" : "secondary"}>
|
|
49
|
+
{open ? "Open" : "Closed"}
|
|
50
|
+
</Badge>
|
|
51
|
+
<span>{range}</span>
|
|
52
|
+
{preOrderCutoff ? <span>Order by {preOrderCutoff}</span> : null}
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Copyright 2026 Chesapeake Computing
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import { Button } from "../ui/button.tsx";
|
|
5
|
+
import { cn } from "../../lib/utils.ts";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Props for {@link LocationHero}.
|
|
9
|
+
*/
|
|
10
|
+
export interface LocationHeroProps {
|
|
11
|
+
/** Local page headline. */
|
|
12
|
+
headline: string;
|
|
13
|
+
/** Neighborhood or district shown above the headline. */
|
|
14
|
+
neighborhood?: string;
|
|
15
|
+
/** Primary commerce action label, such as "Order pickup". */
|
|
16
|
+
primaryLabel: string;
|
|
17
|
+
/** Called when the primary commerce button is clicked. */
|
|
18
|
+
onPrimaryClick: () => void;
|
|
19
|
+
/** Secondary action label, such as "Directions". */
|
|
20
|
+
secondaryLabel: string;
|
|
21
|
+
/** Called when the directions button is clicked. */
|
|
22
|
+
onSecondaryClick: () => void;
|
|
23
|
+
className?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Location headline with a primary commerce action and a directions action.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```tsx
|
|
31
|
+
* <LocationHero
|
|
32
|
+
* neighborhood="Canton"
|
|
33
|
+
* headline="Welcome to The Forest Baltimore"
|
|
34
|
+
* primaryLabel="Order pickup"
|
|
35
|
+
* onPrimaryClick={() => {}}
|
|
36
|
+
* secondaryLabel="Directions"
|
|
37
|
+
* onSecondaryClick={() => {}}
|
|
38
|
+
* />
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export function LocationHero({
|
|
42
|
+
headline,
|
|
43
|
+
neighborhood,
|
|
44
|
+
primaryLabel,
|
|
45
|
+
onPrimaryClick,
|
|
46
|
+
secondaryLabel,
|
|
47
|
+
onSecondaryClick,
|
|
48
|
+
className,
|
|
49
|
+
}: LocationHeroProps) {
|
|
50
|
+
return (
|
|
51
|
+
<section className={cn("flex flex-col gap-4", className)}>
|
|
52
|
+
{neighborhood ? (
|
|
53
|
+
<p className="text-sm text-muted-foreground">{neighborhood}</p>
|
|
54
|
+
) : null}
|
|
55
|
+
<h2 className="text-3xl font-bold tracking-tight">{headline}</h2>
|
|
56
|
+
<div className="flex flex-wrap gap-2">
|
|
57
|
+
<Button onClick={onPrimaryClick}>{primaryLabel}</Button>
|
|
58
|
+
<Button variant="outline" onClick={onSecondaryClick}>
|
|
59
|
+
{secondaryLabel}
|
|
60
|
+
</Button>
|
|
61
|
+
</div>
|
|
62
|
+
</section>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Copyright 2026 Chesapeake Computing
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import { Button } from "../ui/button.tsx";
|
|
5
|
+
import { cn } from "../../lib/utils.ts";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Props for {@link StickyShopBar}.
|
|
9
|
+
*/
|
|
10
|
+
export interface StickyShopBarProps {
|
|
11
|
+
/** Label on the persistent commerce button. */
|
|
12
|
+
label: string;
|
|
13
|
+
/** Fired when the shop button is pressed. */
|
|
14
|
+
onShopClick: () => void;
|
|
15
|
+
className?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Mobile-only bar fixed to the bottom of the viewport with a commerce CTA.
|
|
20
|
+
* Hidden from the `md` breakpoint up.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```tsx
|
|
24
|
+
* <StickyShopBar label="Order pickup" onShopClick={() => {}} />
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export function StickyShopBar({
|
|
28
|
+
label,
|
|
29
|
+
onShopClick,
|
|
30
|
+
className,
|
|
31
|
+
}: StickyShopBarProps) {
|
|
32
|
+
return (
|
|
33
|
+
<div
|
|
34
|
+
className={cn(
|
|
35
|
+
"fixed inset-x-0 bottom-0 z-40 border-t bg-background p-3 md:hidden",
|
|
36
|
+
className
|
|
37
|
+
)}
|
|
38
|
+
>
|
|
39
|
+
<Button className="w-full" onClick={onShopClick}>
|
|
40
|
+
{label}
|
|
41
|
+
</Button>
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// Copyright 2026 Chesapeake Computing
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import { useEffect } from "react";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
Select,
|
|
8
|
+
SelectContent,
|
|
9
|
+
SelectItem,
|
|
10
|
+
SelectTrigger,
|
|
11
|
+
SelectValue,
|
|
12
|
+
} from "../ui/select.tsx";
|
|
13
|
+
import { cn } from "../../lib/utils.ts";
|
|
14
|
+
|
|
15
|
+
/** One row in {@link StoreSwitcher}. */
|
|
16
|
+
export interface StoreOption {
|
|
17
|
+
slug: string;
|
|
18
|
+
name: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Props for {@link StoreSwitcher}.
|
|
23
|
+
*
|
|
24
|
+
* Nearest-store detection is not implemented. `RetailLocation` has no
|
|
25
|
+
* coordinates, so this control only persists an explicit choice.
|
|
26
|
+
*/
|
|
27
|
+
export interface StoreSwitcherProps {
|
|
28
|
+
/** Stores the shopper can choose. */
|
|
29
|
+
locations: StoreOption[];
|
|
30
|
+
/** Selected slug. Omit it to restore the last stored slug on mount. */
|
|
31
|
+
selectedSlug?: string;
|
|
32
|
+
/** Called with the slug when the shopper picks a store, or when a stored slug is restored. */
|
|
33
|
+
onSelect: (slug: string) => void;
|
|
34
|
+
/**
|
|
35
|
+
* `localStorage` key and cookie name.
|
|
36
|
+
* @default "chesapeake.retail.location"
|
|
37
|
+
*/
|
|
38
|
+
storageKey?: string;
|
|
39
|
+
className?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const DEFAULT_STORAGE_KEY = "chesapeake.retail.location";
|
|
43
|
+
|
|
44
|
+
function readStoredSlug(storageKey: string): string | null {
|
|
45
|
+
try {
|
|
46
|
+
const stored = localStorage.getItem(storageKey);
|
|
47
|
+
if (stored) {
|
|
48
|
+
return stored;
|
|
49
|
+
}
|
|
50
|
+
} catch {
|
|
51
|
+
// Private mode and disabled storage still allow the in-memory selection.
|
|
52
|
+
}
|
|
53
|
+
const prefix = `${encodeURIComponent(storageKey)}=`;
|
|
54
|
+
const parts = document.cookie.split("; ");
|
|
55
|
+
for (const part of parts) {
|
|
56
|
+
if (part.startsWith(prefix)) {
|
|
57
|
+
return decodeURIComponent(part.slice(prefix.length));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function writeStoredSlug(storageKey: string, slug: string): void {
|
|
64
|
+
try {
|
|
65
|
+
localStorage.setItem(storageKey, slug);
|
|
66
|
+
} catch {
|
|
67
|
+
// Cookie write below still records the choice when storage is blocked.
|
|
68
|
+
}
|
|
69
|
+
document.cookie = `${encodeURIComponent(storageKey)}=${encodeURIComponent(slug)}; Path=/; SameSite=Lax`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Store picker that remembers the selection in `localStorage` and a
|
|
74
|
+
* `SameSite=Lax` cookie. Use it to keep the page's shopping context on
|
|
75
|
+
* the location in the URL.
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* ```tsx
|
|
79
|
+
* <StoreSwitcher
|
|
80
|
+
* locations={[{ slug: "baltimore-md", name: "Baltimore" }]}
|
|
81
|
+
* selectedSlug={slug}
|
|
82
|
+
* onSelect={setSlug}
|
|
83
|
+
* />
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
export function StoreSwitcher({
|
|
87
|
+
locations,
|
|
88
|
+
selectedSlug,
|
|
89
|
+
onSelect,
|
|
90
|
+
storageKey = DEFAULT_STORAGE_KEY,
|
|
91
|
+
className,
|
|
92
|
+
}: StoreSwitcherProps) {
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
if (selectedSlug) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const stored = readStoredSlug(storageKey);
|
|
98
|
+
if (stored && locations.some((location) => location.slug === stored)) {
|
|
99
|
+
onSelect(stored);
|
|
100
|
+
}
|
|
101
|
+
}, [locations, onSelect, selectedSlug, storageKey]);
|
|
102
|
+
|
|
103
|
+
const handleChange = (slug: string) => {
|
|
104
|
+
writeStoredSlug(storageKey, slug);
|
|
105
|
+
onSelect(slug);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<Select value={selectedSlug} onValueChange={handleChange}>
|
|
110
|
+
<SelectTrigger className={cn("w-full max-w-xs", className)}>
|
|
111
|
+
<SelectValue placeholder="Choose a store" />
|
|
112
|
+
</SelectTrigger>
|
|
113
|
+
<SelectContent>
|
|
114
|
+
{locations.map((location) => (
|
|
115
|
+
<SelectItem key={location.slug} value={location.slug}>
|
|
116
|
+
{location.name}
|
|
117
|
+
</SelectItem>
|
|
118
|
+
))}
|
|
119
|
+
</SelectContent>
|
|
120
|
+
</Select>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Copyright 2026 Chesapeake Computing
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import type {
|
|
5
|
+
DayHours,
|
|
6
|
+
Weekday,
|
|
7
|
+
WeeklyHours,
|
|
8
|
+
} from "../../types/retail-location.ts";
|
|
9
|
+
|
|
10
|
+
const WEEKDAYS: readonly Weekday[] = [
|
|
11
|
+
"sun",
|
|
12
|
+
"mon",
|
|
13
|
+
"tue",
|
|
14
|
+
"wed",
|
|
15
|
+
"thu",
|
|
16
|
+
"fri",
|
|
17
|
+
"sat",
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
/** Hours for the local weekday of `at`. `null` means that day is closed. */
|
|
21
|
+
export function hoursForDay(hours: WeeklyHours, at: Date): DayHours | null {
|
|
22
|
+
const key = WEEKDAYS[at.getDay()];
|
|
23
|
+
return hours[key];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function minutesOfDay(hhmm: string): number | null {
|
|
27
|
+
const match = /^(\d{2}):(\d{2})$/.exec(hhmm);
|
|
28
|
+
if (!match) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
const hour = Number(match[1]);
|
|
32
|
+
const minute = Number(match[2]);
|
|
33
|
+
if (hour > 23 || minute > 59) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
return hour * 60 + minute;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Whether `at` falls inside that local day's open range.
|
|
41
|
+
* A `null` day, an unparsable `HH:MM` value, or a time outside
|
|
42
|
+
* `[open, close)` is closed.
|
|
43
|
+
*/
|
|
44
|
+
export function isOpenAt(hours: WeeklyHours, at: Date): boolean {
|
|
45
|
+
const day = hoursForDay(hours, at);
|
|
46
|
+
if (!day) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
const open = minutesOfDay(day.open);
|
|
50
|
+
const close = minutesOfDay(day.close);
|
|
51
|
+
if (open === null || close === null) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
const now = at.getHours() * 60 + at.getMinutes();
|
|
55
|
+
return now >= open && now < close;
|
|
56
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Copyright 2026 Chesapeake Computing
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
export { hoursForDay, isOpenAt } from "./hours.ts";
|
|
5
|
+
|
|
6
|
+
export { LocationHero } from "./LocationHero.tsx";
|
|
7
|
+
export type { LocationHeroProps } from "./LocationHero.tsx";
|
|
8
|
+
|
|
9
|
+
export { HoursStatusStrip } from "./HoursStatusStrip.tsx";
|
|
10
|
+
export type { HoursStatusStripProps } from "./HoursStatusStrip.tsx";
|
|
11
|
+
|
|
12
|
+
export { StickyShopBar } from "./StickyShopBar.tsx";
|
|
13
|
+
export type { StickyShopBarProps } from "./StickyShopBar.tsx";
|
|
14
|
+
|
|
15
|
+
export { StoreSwitcher } from "./StoreSwitcher.tsx";
|
|
16
|
+
export type { StoreOption, StoreSwitcherProps } from "./StoreSwitcher.tsx";
|
package/index.ts
CHANGED
|
@@ -25,6 +25,8 @@
|
|
|
25
25
|
// Re-export everything from subdirectories for convenient imports
|
|
26
26
|
export * from "./components/ui/index.ts";
|
|
27
27
|
export * from "./components/ux/index.ts";
|
|
28
|
+
export * from "./components/retail/index.ts";
|
|
29
|
+
export * from "./types/retail-location.ts";
|
|
28
30
|
export * from "./lib/utils.ts";
|
|
29
31
|
export * from "./lib/githubIssue.ts";
|
|
30
32
|
export { default as tailwindPreset } from "./tailwind.preset.ts";
|
package/lib/githubIssue.ts
CHANGED
|
@@ -112,7 +112,9 @@ export function isGitHubPullRequestPayload(value: unknown): boolean {
|
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
export function isValidRepoName(value: string): boolean {
|
|
115
|
-
return
|
|
115
|
+
return (
|
|
116
|
+
/^[A-Za-z0-9_.-]+$/.test(value) && value.length > 0 && value.length <= 100
|
|
117
|
+
);
|
|
116
118
|
}
|
|
117
119
|
|
|
118
120
|
export function isValidIssueNumber(value: unknown): value is number {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chesapeake/stack",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "A simple, reusable React component library and design system built on shadcn/ui with Tailwind CSS",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "npm@10.2.0",
|
|
@@ -78,6 +78,11 @@
|
|
|
78
78
|
"./components/ui/textarea": "./components/ui/textarea.tsx",
|
|
79
79
|
"./components/ui/tooltip": "./components/ui/tooltip.tsx",
|
|
80
80
|
"./components/ux": "./components/ux/index.ts",
|
|
81
|
+
"./components/retail": "./components/retail/index.ts",
|
|
82
|
+
"./components/retail/LocationHero": "./components/retail/LocationHero.tsx",
|
|
83
|
+
"./components/retail/HoursStatusStrip": "./components/retail/HoursStatusStrip.tsx",
|
|
84
|
+
"./components/retail/StickyShopBar": "./components/retail/StickyShopBar.tsx",
|
|
85
|
+
"./components/retail/StoreSwitcher": "./components/retail/StoreSwitcher.tsx",
|
|
81
86
|
"./components/ux/ActivityAnimation": "./components/ux/ActivityAnimation.tsx",
|
|
82
87
|
"./components/ux/FireAnimation": "./components/ux/FireAnimation.tsx",
|
|
83
88
|
"./components/ux/GifSearch": "./components/ux/GifSearch.tsx",
|
|
@@ -130,6 +135,7 @@
|
|
|
130
135
|
"./lib/utils": "./lib/utils.ts",
|
|
131
136
|
"./lib/githubIssue": "./lib/githubIssue.ts",
|
|
132
137
|
"./types/user": "./types/user.ts",
|
|
138
|
+
"./types/retail-location": "./types/retail-location.ts",
|
|
133
139
|
"./styles": "./styles/index.css",
|
|
134
140
|
"./tailwind.preset": "./tailwind.preset.ts",
|
|
135
141
|
"./eslint.config": "./eslint.config.js",
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Copyright 2026 Chesapeake Computing
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Canonical retail location content model.
|
|
6
|
+
*
|
|
7
|
+
* Shared with the Chesapeake audit rubric (chesapeakedev/chesapeake#485).
|
|
8
|
+
* Phase 1 deep-links into `commerce.platform`; it does not replace checkout.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** Street address for one store. */
|
|
12
|
+
export interface Address {
|
|
13
|
+
street: string;
|
|
14
|
+
city: string;
|
|
15
|
+
region: string;
|
|
16
|
+
postalCode: string;
|
|
17
|
+
country: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Lowercase weekday key used by {@link WeeklyHours}. */
|
|
21
|
+
export type Weekday = "mon" | "tue" | "wed" | "thu" | "fri" | "sat" | "sun";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Open and close as 24-hour local times (`HH:MM`).
|
|
25
|
+
* A closed day is `null` on {@link WeeklyHours}, not a zero-length range.
|
|
26
|
+
*/
|
|
27
|
+
export interface DayHours {
|
|
28
|
+
open: string;
|
|
29
|
+
close: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** One entry per weekday. `null` means the store is closed that day. */
|
|
33
|
+
export type WeeklyHours = Record<Weekday, DayHours | null>;
|
|
34
|
+
|
|
35
|
+
/** One question and answer on a location page. */
|
|
36
|
+
export interface FaqItem {
|
|
37
|
+
question: string;
|
|
38
|
+
answer: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Checkout platforms Phase 1 deep-links into. Phase 1 does not replace them. */
|
|
42
|
+
export type CommercePlatform =
|
|
43
|
+
| "dutchie"
|
|
44
|
+
| "toast"
|
|
45
|
+
| "shopify"
|
|
46
|
+
| "square"
|
|
47
|
+
| "custom";
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Public facts for one retail location.
|
|
51
|
+
* `storeId` is the id the commerce platform publishes. When the page does
|
|
52
|
+
* not publish one, use a stable public slug.
|
|
53
|
+
*/
|
|
54
|
+
export interface RetailLocation {
|
|
55
|
+
slug: string;
|
|
56
|
+
name: string;
|
|
57
|
+
headline: string;
|
|
58
|
+
neighborhood?: string;
|
|
59
|
+
address: Address;
|
|
60
|
+
phone: string;
|
|
61
|
+
email?: string;
|
|
62
|
+
hours: WeeklyHours;
|
|
63
|
+
status?: { preOrderCutoff?: string; badge?: string };
|
|
64
|
+
commerce: {
|
|
65
|
+
platform: CommercePlatform;
|
|
66
|
+
storeId: string;
|
|
67
|
+
shopUrl: string;
|
|
68
|
+
categoryLinks?: Record<string, string>;
|
|
69
|
+
};
|
|
70
|
+
faqs: FaqItem[];
|
|
71
|
+
localSeo?: { landmarks: string[]; serviceAreas: string[]; guide: string };
|
|
72
|
+
social?: { googlePlaceId?: string };
|
|
73
|
+
}
|