@voyantjs/cruises-ui 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -0
- package/dist/components/enrichment-program-list.d.ts +13 -0
- package/dist/components/enrichment-program-list.d.ts.map +1 -0
- package/dist/components/enrichment-program-list.js +50 -0
- package/dist/components/external-badge.d.ts +14 -0
- package/dist/components/external-badge.d.ts.map +1 -0
- package/dist/components/external-badge.js +13 -0
- package/dist/components/pricing-grid.d.ts +21 -0
- package/dist/components/pricing-grid.d.ts.map +1 -0
- package/dist/components/pricing-grid.js +70 -0
- package/dist/components/quote-display.d.ts +14 -0
- package/dist/components/quote-display.d.ts.map +1 -0
- package/dist/components/quote-display.js +42 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/package.json +62 -0
package/README.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# @voyantjs/cruises-ui
|
|
2
|
+
|
|
3
|
+
Importable React UI components for Voyant cruises. Bundler-consumed (Vite, Next.js, webpack, etc.).
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @voyantjs/cruises-ui @voyantjs/cruises-react @voyantjs/voyant-ui @tanstack/react-query react react-dom
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
`@voyantjs/voyant-ui` provides the design-system primitives. `@voyantjs/cruises-react` provides the data-layer hooks. Both are required peers.
|
|
12
|
+
|
|
13
|
+
All components accept a `className` prop and merge it with `cn()`. Wrap or compose to extend; use the registry copy-paste path (`npx shadcn add @voyant/...`) for components you want to fork outright.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type * as React from "react";
|
|
2
|
+
export interface EnrichmentProgramListProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
3
|
+
cruiseKey: string;
|
|
4
|
+
emptyState?: React.ReactNode;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Connected list of expedition enrichment staff (naturalists, historians,
|
|
8
|
+
* photographers, lecturers, experts) for a given cruise. Pure read-only
|
|
9
|
+
* display — for editing, pair with a separate enrichment-form component
|
|
10
|
+
* driven by `useEnrichmentMutation`.
|
|
11
|
+
*/
|
|
12
|
+
export declare function EnrichmentProgramList({ cruiseKey, emptyState, className, ...props }: EnrichmentProgramListProps): import("react/jsx-runtime").JSX.Element;
|
|
13
|
+
//# sourceMappingURL=enrichment-program-list.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"enrichment-program-list.d.ts","sourceRoot":"","sources":["../../src/components/enrichment-program-list.tsx"],"names":[],"mappings":"AAQA,OAAO,KAAK,KAAK,KAAK,MAAM,OAAO,CAAA;AAuBnC,MAAM,WAAW,0BAA2B,SAAQ,KAAK,CAAC,cAAc,CAAC,cAAc,CAAC;IACtF,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;CAC7B;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,EACpC,SAAS,EACT,UAAU,EACV,SAAS,EACT,GAAG,KAAK,EACT,EAAE,0BAA0B,2CAiE5B"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEnrichmentPrograms } from "@voyantjs/cruises-react";
|
|
4
|
+
import { Avatar, AvatarFallback, AvatarImage } from "@voyantjs/voyant-ui/components/avatar";
|
|
5
|
+
import { Badge } from "@voyantjs/voyant-ui/components/badge";
|
|
6
|
+
import { Card, CardContent } from "@voyantjs/voyant-ui/components/card";
|
|
7
|
+
import { cn } from "@voyantjs/voyant-ui/lib/utils";
|
|
8
|
+
import { Camera, Compass, GraduationCap, Mic, ScrollText, Sparkles } from "lucide-react";
|
|
9
|
+
const KIND_LABEL = {
|
|
10
|
+
naturalist: "Naturalist",
|
|
11
|
+
historian: "Historian",
|
|
12
|
+
photographer: "Photographer",
|
|
13
|
+
lecturer: "Lecturer",
|
|
14
|
+
expert: "Expert",
|
|
15
|
+
other: "Specialist",
|
|
16
|
+
};
|
|
17
|
+
const KIND_ICON = {
|
|
18
|
+
naturalist: Compass,
|
|
19
|
+
historian: ScrollText,
|
|
20
|
+
photographer: Camera,
|
|
21
|
+
lecturer: Mic,
|
|
22
|
+
expert: GraduationCap,
|
|
23
|
+
other: Sparkles,
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Connected list of expedition enrichment staff (naturalists, historians,
|
|
27
|
+
* photographers, lecturers, experts) for a given cruise. Pure read-only
|
|
28
|
+
* display — for editing, pair with a separate enrichment-form component
|
|
29
|
+
* driven by `useEnrichmentMutation`.
|
|
30
|
+
*/
|
|
31
|
+
export function EnrichmentProgramList({ cruiseKey, emptyState, className, ...props }) {
|
|
32
|
+
const { data, isLoading } = useEnrichmentPrograms(cruiseKey);
|
|
33
|
+
if (isLoading) {
|
|
34
|
+
return (_jsx("div", { "data-slot": "enrichment-loading", className: cn("py-8 text-center", className), ...props, children: _jsx("p", { className: "text-muted-foreground", children: "Loading enrichment programs\u2026" }) }));
|
|
35
|
+
}
|
|
36
|
+
if (!data || data.length === 0) {
|
|
37
|
+
return (_jsx("div", { "data-slot": "enrichment-empty", className: cn("py-8 text-center", className), ...props, children: emptyState ?? (_jsx("p", { className: "text-muted-foreground", children: "No enrichment programs published for this cruise." })) }));
|
|
38
|
+
}
|
|
39
|
+
return (_jsx("div", { "data-slot": "enrichment-list", className: cn("grid gap-4 sm:grid-cols-2", className), ...props, children: data.map((program) => {
|
|
40
|
+
const Icon = KIND_ICON[program.kind];
|
|
41
|
+
const initials = program.name
|
|
42
|
+
.split(/\s+/)
|
|
43
|
+
.map((part) => part[0])
|
|
44
|
+
.filter(Boolean)
|
|
45
|
+
.slice(0, 2)
|
|
46
|
+
.join("")
|
|
47
|
+
.toUpperCase();
|
|
48
|
+
return (_jsx(Card, { "data-slot": "enrichment-card", className: "overflow-hidden", children: _jsxs(CardContent, { className: "flex gap-4 p-4", children: [_jsxs(Avatar, { className: "size-14 shrink-0", children: [program.bioImageUrl ? (_jsx(AvatarImage, { src: program.bioImageUrl, alt: program.name })) : null, _jsx(AvatarFallback, { children: initials || "?" })] }), _jsxs("div", { className: "min-w-0 flex-1 space-y-1", children: [_jsx("div", { className: "flex items-center gap-2", children: _jsxs(Badge, { variant: "secondary", className: "font-normal", children: [_jsx(Icon, { "aria-hidden": true, className: "mr-1 size-3" }), KIND_LABEL[program.kind]] }) }), _jsx("div", { className: "font-semibold truncate", children: program.name }), program.title ? (_jsx("div", { className: "text-sm text-muted-foreground truncate", children: program.title })) : null, program.description ? (_jsx("p", { className: "text-sm text-muted-foreground line-clamp-3", children: program.description })) : null] })] }) }, program.id));
|
|
49
|
+
}) }));
|
|
50
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Badge } from "@voyantjs/voyant-ui/components/badge";
|
|
2
|
+
import type * as React from "react";
|
|
3
|
+
export interface ExternalCruiseBadgeProps extends React.ComponentPropsWithoutRef<typeof Badge> {
|
|
4
|
+
/** The adapter that sourced the cruise (e.g. "voyant-connect", "custom"). */
|
|
5
|
+
sourceProvider: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Small inline badge that surfaces cruise provenance to admin/storefront UIs.
|
|
9
|
+
* Renders as a subtle "External · <provider>" pill so operators (and shoppers,
|
|
10
|
+
* if the storefront opts in) can see at a glance which inventory comes from
|
|
11
|
+
* upstream and which is the operator's own.
|
|
12
|
+
*/
|
|
13
|
+
export declare function ExternalCruiseBadge({ sourceProvider, className, ...props }: ExternalCruiseBadgeProps): import("react/jsx-runtime").JSX.Element;
|
|
14
|
+
//# sourceMappingURL=external-badge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"external-badge.d.ts","sourceRoot":"","sources":["../../src/components/external-badge.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,EAAE,MAAM,sCAAsC,CAAA;AAE5D,OAAO,KAAK,KAAK,KAAK,MAAM,OAAO,CAAA;AAEnC,MAAM,WAAW,wBAAyB,SAAQ,KAAK,CAAC,wBAAwB,CAAC,OAAO,KAAK,CAAC;IAC5F,6EAA6E;IAC7E,cAAc,EAAE,MAAM,CAAA;CACvB;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,cAAc,EACd,SAAS,EACT,GAAG,KAAK,EACT,EAAE,wBAAwB,2CAe1B"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Badge } from "@voyantjs/voyant-ui/components/badge";
|
|
4
|
+
import { cn } from "@voyantjs/voyant-ui/lib/utils";
|
|
5
|
+
/**
|
|
6
|
+
* Small inline badge that surfaces cruise provenance to admin/storefront UIs.
|
|
7
|
+
* Renders as a subtle "External · <provider>" pill so operators (and shoppers,
|
|
8
|
+
* if the storefront opts in) can see at a glance which inventory comes from
|
|
9
|
+
* upstream and which is the operator's own.
|
|
10
|
+
*/
|
|
11
|
+
export function ExternalCruiseBadge({ sourceProvider, className, ...props }) {
|
|
12
|
+
return (_jsxs(Badge, { "data-slot": "external-cruise-badge", variant: "outline", className: cn("font-normal", className), title: `Sourced via ${sourceProvider}`, ...props, children: [_jsx("span", { "aria-hidden": "true", className: "mr-1", children: "\u2197" }), "External \u00B7 ", sourceProvider] }));
|
|
13
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { PriceRecord } from "@voyantjs/cruises-react";
|
|
2
|
+
import type * as React from "react";
|
|
3
|
+
export interface PricingGridProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
4
|
+
/** Flat list of prices for a single sailing (typically from useSailing(...,{include:["pricing"]})). */
|
|
5
|
+
prices: PriceRecord[];
|
|
6
|
+
/** Optional cabin-category id → display name resolver for nicer row labels. */
|
|
7
|
+
categoryLabel?: (categoryId: string) => string;
|
|
8
|
+
/** Currency formatter; defaults to `${currency} ${amount}`. */
|
|
9
|
+
formatPrice?: (amount: string, currency: string) => string;
|
|
10
|
+
/** Click handler for a (category, occupancy, fareCode) cell — typically opens the booking flow. */
|
|
11
|
+
onCellSelect?: (price: PriceRecord) => void;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The cabin × occupancy pricing matrix that's the heart of any cruise booking
|
|
15
|
+
* flow. Rows = cabin categories; columns = occupancy variants present in the
|
|
16
|
+
* data; cells = lowest available fare per (category, occupancy). Cells render
|
|
17
|
+
* an availability badge and the price; clicking surfaces the underlying row
|
|
18
|
+
* for the booking flow to consume.
|
|
19
|
+
*/
|
|
20
|
+
export declare function PricingGrid({ prices, categoryLabel, formatPrice, onCellSelect, className, ...props }: PricingGridProps): import("react/jsx-runtime").JSX.Element;
|
|
21
|
+
//# sourceMappingURL=pricing-grid.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pricing-grid.d.ts","sourceRoot":"","sources":["../../src/components/pricing-grid.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAA;AAW1D,OAAO,KAAK,KAAK,KAAK,MAAM,OAAO,CAAA;AAEnC,MAAM,WAAW,gBAAiB,SAAQ,KAAK,CAAC,cAAc,CAAC,cAAc,CAAC;IAC5E,uGAAuG;IACvG,MAAM,EAAE,WAAW,EAAE,CAAA;IACrB,+EAA+E;IAC/E,aAAa,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,MAAM,CAAA;IAC9C,+DAA+D;IAC/D,WAAW,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,MAAM,CAAA;IAC1D,mGAAmG;IACnG,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,CAAA;CAC5C;AA2BD;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,EAC1B,MAAM,EACN,aAAa,EACb,WAAgC,EAChC,YAAY,EACZ,SAAS,EACT,GAAG,KAAK,EACT,EAAE,gBAAgB,2CA+FlB"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Badge } from "@voyantjs/voyant-ui/components/badge";
|
|
4
|
+
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@voyantjs/voyant-ui/components/table";
|
|
5
|
+
import { cn } from "@voyantjs/voyant-ui/lib/utils";
|
|
6
|
+
const AVAILABILITY_VARIANT = {
|
|
7
|
+
available: "default",
|
|
8
|
+
limited: "secondary",
|
|
9
|
+
on_request: "outline",
|
|
10
|
+
wait_list: "outline",
|
|
11
|
+
sold_out: "destructive",
|
|
12
|
+
};
|
|
13
|
+
const AVAILABILITY_LABEL = {
|
|
14
|
+
available: "Available",
|
|
15
|
+
limited: "Limited",
|
|
16
|
+
on_request: "On request",
|
|
17
|
+
wait_list: "Wait list",
|
|
18
|
+
sold_out: "Sold out",
|
|
19
|
+
};
|
|
20
|
+
function defaultFormatPrice(amount, currency) {
|
|
21
|
+
const n = Number(amount);
|
|
22
|
+
if (!Number.isFinite(n))
|
|
23
|
+
return `${currency} ${amount}`;
|
|
24
|
+
return `${currency} ${n.toLocaleString(undefined, { maximumFractionDigits: 0 })}`;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The cabin × occupancy pricing matrix that's the heart of any cruise booking
|
|
28
|
+
* flow. Rows = cabin categories; columns = occupancy variants present in the
|
|
29
|
+
* data; cells = lowest available fare per (category, occupancy). Cells render
|
|
30
|
+
* an availability badge and the price; clicking surfaces the underlying row
|
|
31
|
+
* for the booking flow to consume.
|
|
32
|
+
*/
|
|
33
|
+
export function PricingGrid({ prices, categoryLabel, formatPrice = defaultFormatPrice, onCellSelect, className, ...props }) {
|
|
34
|
+
if (prices.length === 0) {
|
|
35
|
+
return (_jsx("div", { "data-slot": "pricing-grid-empty", className: cn("py-8 text-center", className), ...props, children: _jsx("p", { className: "text-muted-foreground", children: "No pricing published for this sailing." }) }));
|
|
36
|
+
}
|
|
37
|
+
// Pivot the flat list into a (categoryId × occupancy) → cheapest price row.
|
|
38
|
+
const occupancies = Array.from(new Set(prices.map((p) => p.occupancy))).sort((a, b) => a - b);
|
|
39
|
+
const grouped = new Map();
|
|
40
|
+
for (const p of prices) {
|
|
41
|
+
let row = grouped.get(p.cabinCategoryId);
|
|
42
|
+
if (!row) {
|
|
43
|
+
row = new Map();
|
|
44
|
+
grouped.set(p.cabinCategoryId, row);
|
|
45
|
+
}
|
|
46
|
+
const existing = row.get(p.occupancy);
|
|
47
|
+
if (!existing || Number(p.pricePerPerson) < Number(existing.pricePerPerson)) {
|
|
48
|
+
row.set(p.occupancy, p);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return (_jsx("div", { "data-slot": "pricing-grid", className: cn("overflow-x-auto", className), ...props, children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { className: "w-[220px]", children: "Cabin category" }), occupancies.map((occ) => (_jsx(TableHead, { className: "text-center", children: occ === 1
|
|
52
|
+
? "Single"
|
|
53
|
+
: occ === 2
|
|
54
|
+
? "Double"
|
|
55
|
+
: occ === 3
|
|
56
|
+
? "Triple"
|
|
57
|
+
: occ === 4
|
|
58
|
+
? "Quad"
|
|
59
|
+
: `${occ}-occupancy` }, occ)))] }) }), _jsx(TableBody, { children: Array.from(grouped.entries()).map(([categoryId, row]) => (_jsxs(TableRow, { children: [_jsx(TableCell, { className: "font-medium", children: categoryLabel?.(categoryId) ?? categoryId }), occupancies.map((occ) => {
|
|
60
|
+
const price = row.get(occ);
|
|
61
|
+
if (!price) {
|
|
62
|
+
return (_jsx(TableCell, { className: "text-center text-muted-foreground", children: "\u2014" }, occ));
|
|
63
|
+
}
|
|
64
|
+
return (_jsxs(TableCell, { className: cn("text-center align-top", onCellSelect &&
|
|
65
|
+
price.availability !== "sold_out" &&
|
|
66
|
+
"cursor-pointer hover:bg-accent"), onClick: onCellSelect && price.availability !== "sold_out"
|
|
67
|
+
? () => onCellSelect(price)
|
|
68
|
+
: undefined, "data-slot": "pricing-grid-cell", children: [_jsx("div", { className: "font-semibold", children: formatPrice(price.pricePerPerson, price.currency) }), _jsx("div", { className: "text-xs text-muted-foreground", children: "per person" }), _jsx(Badge, { variant: AVAILABILITY_VARIANT[price.availability], className: "mt-2 font-normal", children: AVAILABILITY_LABEL[price.availability] })] }, occ));
|
|
69
|
+
})] }, categoryId))) })] }) }));
|
|
70
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Quote } from "@voyantjs/cruises-react";
|
|
2
|
+
import { Card } from "@voyantjs/voyant-ui/components/card";
|
|
3
|
+
import type * as React from "react";
|
|
4
|
+
export interface QuoteDisplayProps extends React.ComponentPropsWithoutRef<typeof Card> {
|
|
5
|
+
quote: Quote;
|
|
6
|
+
formatPrice?: (amount: string, currency: string) => string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Renders an itemised cruise quote: base per person, components grouped by
|
|
10
|
+
* direction (additions / inclusions / credits), and totals. Mirrors what the
|
|
11
|
+
* server's composeQuote returns; pure presentational.
|
|
12
|
+
*/
|
|
13
|
+
export declare function QuoteDisplay({ quote, formatPrice, className, ...props }: QuoteDisplayProps): import("react/jsx-runtime").JSX.Element;
|
|
14
|
+
//# sourceMappingURL=quote-display.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"quote-display.d.ts","sourceRoot":"","sources":["../../src/components/quote-display.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,yBAAyB,CAAA;AACpD,OAAO,EAAE,IAAI,EAA2B,MAAM,qCAAqC,CAAA;AAEnF,OAAO,KAAK,KAAK,KAAK,MAAM,OAAO,CAAA;AAInC,MAAM,WAAW,iBAAkB,SAAQ,KAAK,CAAC,wBAAwB,CAAC,OAAO,IAAI,CAAC;IACpF,KAAK,EAAE,KAAK,CAAA;IACZ,WAAW,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,MAAM,CAAA;CAC3D;AAwBD;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,EAC3B,KAAK,EACL,WAAgC,EAChC,SAAS,EACT,GAAG,KAAK,EACT,EAAE,iBAAiB,2CAkFnB"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Card, CardContent, CardHeader } from "@voyantjs/voyant-ui/components/card";
|
|
4
|
+
import { cn } from "@voyantjs/voyant-ui/lib/utils";
|
|
5
|
+
function defaultFormatPrice(amount, currency) {
|
|
6
|
+
const negative = amount.startsWith("-");
|
|
7
|
+
const abs = negative ? amount.slice(1) : amount;
|
|
8
|
+
const n = Number(abs);
|
|
9
|
+
if (!Number.isFinite(n))
|
|
10
|
+
return `${currency} ${amount}`;
|
|
11
|
+
return `${negative ? "-" : ""}${currency} ${n.toLocaleString(undefined, {
|
|
12
|
+
minimumFractionDigits: 2,
|
|
13
|
+
maximumFractionDigits: 2,
|
|
14
|
+
})}`;
|
|
15
|
+
}
|
|
16
|
+
const COMPONENT_KIND_LABEL = {
|
|
17
|
+
gratuity: "Gratuity",
|
|
18
|
+
onboard_credit: "Onboard credit",
|
|
19
|
+
port_charge: "Port charges",
|
|
20
|
+
tax: "Tax",
|
|
21
|
+
ncf: "Non-comm fees",
|
|
22
|
+
airfare: "Airfare",
|
|
23
|
+
transfer: "Transfer",
|
|
24
|
+
insurance: "Insurance",
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Renders an itemised cruise quote: base per person, components grouped by
|
|
28
|
+
* direction (additions / inclusions / credits), and totals. Mirrors what the
|
|
29
|
+
* server's composeQuote returns; pure presentational.
|
|
30
|
+
*/
|
|
31
|
+
export function QuoteDisplay({ quote, formatPrice = defaultFormatPrice, className, ...props }) {
|
|
32
|
+
const additions = quote.components.filter((c) => c.direction === "addition");
|
|
33
|
+
const inclusions = quote.components.filter((c) => c.direction === "inclusion");
|
|
34
|
+
const credits = quote.components.filter((c) => c.direction === "credit");
|
|
35
|
+
return (_jsxs(Card, { "data-slot": "quote-display", className: cn(className), ...props, children: [_jsx(CardHeader, { children: _jsxs("div", { className: "flex items-baseline justify-between", children: [_jsxs("div", { children: [_jsx("h3", { className: "text-base font-semibold", children: "Your quote" }), quote.fareCodeName ? (_jsxs("p", { className: "text-sm text-muted-foreground", children: [quote.fareCodeName, quote.fareCode ? ` (${quote.fareCode})` : ""] })) : null] }), _jsxs("div", { className: "text-right", children: [_jsx("div", { className: "text-2xl font-bold", children: formatPrice(quote.totalForCabin, quote.currency) }), _jsxs("div", { className: "text-xs text-muted-foreground", children: ["for ", quote.guestCount, " guest", quote.guestCount === 1 ? "" : "s", " (", quote.occupancy, "-occupancy cabin)"] })] })] }) }), _jsxs(CardContent, { className: "space-y-4 text-sm", children: [_jsx(Row, { label: `Base · ${formatPrice(quote.basePerPerson, quote.currency)} pp × ${quote.guestCount}` }), additions.length > 0 ? (_jsx(Section, { title: "Additions", children: additions.map((c) => (_jsx(Row, { label: `${c.label ?? COMPONENT_KIND_LABEL[c.kind]}${c.perPerson ? " (per person)" : " (per cabin)"}`, amount: `+ ${formatPrice(c.amount, c.currency)}` }, `add-${c.kind}-${c.label ?? ""}-${c.amount}`))) })) : null, credits.length > 0 ? (_jsx(Section, { title: "Credits", children: credits.map((c) => (_jsx(Row, { label: `${c.label ?? COMPONENT_KIND_LABEL[c.kind]}${c.perPerson ? " (per person)" : " (per cabin)"}`, amount: `− ${formatPrice(c.amount, c.currency)}`, amountClassName: "text-emerald-600" }, `cred-${c.kind}-${c.label ?? ""}-${c.amount}`))) })) : null, inclusions.length > 0 ? (_jsx(Section, { title: "Included", children: inclusions.map((c) => (_jsx(Row, { label: c.label ?? COMPONENT_KIND_LABEL[c.kind], amount: "Included" }, `inc-${c.kind}-${c.label ?? ""}`))) })) : null, _jsxs("div", { className: "border-t pt-3", children: [_jsx(Row, { label: "Per person", amount: formatPrice(quote.totalPerPerson, quote.currency), amountClassName: "font-semibold" }), _jsx(Row, { label: "Total for cabin", amount: formatPrice(quote.totalForCabin, quote.currency), amountClassName: "font-bold text-base" })] })] })] }));
|
|
36
|
+
}
|
|
37
|
+
function Section({ title, children }) {
|
|
38
|
+
return (_jsxs("div", { className: "space-y-1", children: [_jsx("h4", { className: "text-xs font-medium uppercase tracking-wider text-muted-foreground", children: title }), children] }));
|
|
39
|
+
}
|
|
40
|
+
function Row({ label, amount, amountClassName, }) {
|
|
41
|
+
return (_jsxs("div", { className: "flex items-baseline justify-between gap-4", children: [_jsx("span", { className: "text-muted-foreground", children: label }), amount ? _jsx("span", { className: cn("tabular-nums", amountClassName), children: amount }) : null] }));
|
|
42
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { EnrichmentProgramList, type EnrichmentProgramListProps, } from "./components/enrichment-program-list";
|
|
2
|
+
export { ExternalCruiseBadge, type ExternalCruiseBadgeProps } from "./components/external-badge";
|
|
3
|
+
export { PricingGrid, type PricingGridProps } from "./components/pricing-grid";
|
|
4
|
+
export { QuoteDisplay, type QuoteDisplayProps } from "./components/quote-display";
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,qBAAqB,EACrB,KAAK,0BAA0B,GAChC,MAAM,sCAAsC,CAAA;AAC7C,OAAO,EAAE,mBAAmB,EAAE,KAAK,wBAAwB,EAAE,MAAM,6BAA6B,CAAA;AAChG,OAAO,EAAE,WAAW,EAAE,KAAK,gBAAgB,EAAE,MAAM,2BAA2B,CAAA;AAC9E,OAAO,EAAE,YAAY,EAAE,KAAK,iBAAiB,EAAE,MAAM,4BAA4B,CAAA"}
|
package/dist/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@voyantjs/cruises-ui",
|
|
3
|
+
"version": "0.13.0",
|
|
4
|
+
"license": "FSL-1.1-Apache-2.0",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/voyantjs/voyant.git",
|
|
8
|
+
"directory": "packages/cruises-ui"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"sideEffects": false,
|
|
12
|
+
"exports": {
|
|
13
|
+
".": "./src/index.ts",
|
|
14
|
+
"./components/*": "./src/components/*.tsx"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc -p tsconfig.build.json",
|
|
18
|
+
"clean": "rm -rf dist",
|
|
19
|
+
"prepack": "pnpm run build",
|
|
20
|
+
"typecheck": "tsc --noEmit",
|
|
21
|
+
"lint": "biome check src/",
|
|
22
|
+
"test": "vitest run --passWithNoTests"
|
|
23
|
+
},
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"@tanstack/react-query": "^5.0.0",
|
|
26
|
+
"@voyantjs/cruises-react": "workspace:*",
|
|
27
|
+
"@voyantjs/voyant-ui": "workspace:*",
|
|
28
|
+
"react": "^19.0.0",
|
|
29
|
+
"react-dom": "^19.0.0"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@tanstack/react-query": "^5.96.2",
|
|
33
|
+
"@types/react": "^19.2.14",
|
|
34
|
+
"@types/react-dom": "^19.2.3",
|
|
35
|
+
"@voyantjs/cruises-react": "workspace:*",
|
|
36
|
+
"@voyantjs/voyant-typescript-config": "workspace:*",
|
|
37
|
+
"@voyantjs/voyant-ui": "workspace:*",
|
|
38
|
+
"lucide-react": "^0.475.0",
|
|
39
|
+
"react": "^19.2.4",
|
|
40
|
+
"react-dom": "^19.2.4",
|
|
41
|
+
"typescript": "^6.0.2",
|
|
42
|
+
"vitest": "^4.1.2"
|
|
43
|
+
},
|
|
44
|
+
"files": [
|
|
45
|
+
"dist"
|
|
46
|
+
],
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public",
|
|
49
|
+
"exports": {
|
|
50
|
+
".": {
|
|
51
|
+
"types": "./dist/index.d.ts",
|
|
52
|
+
"import": "./dist/index.js"
|
|
53
|
+
},
|
|
54
|
+
"./components/*": {
|
|
55
|
+
"types": "./dist/components/*.d.ts",
|
|
56
|
+
"import": "./dist/components/*.js"
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
"main": "./dist/index.js",
|
|
60
|
+
"types": "./dist/index.d.ts"
|
|
61
|
+
}
|
|
62
|
+
}
|