astro-component-kit 0.4.0 → 0.5.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 +139 -27
- package/bin/cli.js +67 -0
- package/bin/installer.js +179 -0
- package/package.json +35 -5
- package/src/components/index.ts +2 -0
- package/src/components/lib/Galaxy3D.astro +249 -137
- package/src/components/lib/app/AppSidebar.astro +249 -0
- package/src/components/lib/app/AppTopbar.astro +284 -0
- package/src/components/lib/app/ArrivalsBoard.astro +176 -0
- package/src/components/lib/app/AuthCard.astro +213 -0
- package/src/components/lib/app/DataTable.astro +346 -0
- package/src/components/lib/app/GuestProfileCard.astro +133 -0
- package/src/components/lib/app/HousekeepingBoard.astro +189 -0
- package/src/components/lib/app/PageHeader.astro +157 -0
- package/src/components/lib/app/PlanUsageCard.astro +112 -0
- package/src/components/lib/app/RoomRack.astro +207 -0
- package/src/components/lib/app/SettingsSection.astro +96 -0
- package/src/components/lib/app/SwitchRow.astro +71 -0
- package/src/components/lib/app/TeamMembers.astro +190 -0
- package/src/components/lib/app/app-icons.ts +52 -0
- package/src/components/lib/app/index.ts +19 -0
- package/src/components/lib/charts/AreaChart.astro +4 -4
- package/src/components/lib/charts/BarChart.astro +3 -3
- package/src/components/lib/charts/ChartFrame.astro +10 -10
- package/src/components/lib/charts/DonutChart.astro +2 -2
- package/src/components/lib/charts/DumbbellChart.astro +1 -1
- package/src/components/lib/charts/ForecastLineChart.astro +1 -1
- package/src/components/lib/charts/GaugeChart.astro +1 -1
- package/src/components/lib/charts/HeatmapChart.astro +1 -1
- package/src/components/lib/charts/HorizontalBarChart.astro +2 -2
- package/src/components/lib/charts/KpiStatTile.astro +4 -4
- package/src/components/lib/charts/LinearProgressGauge.astro +4 -4
- package/src/components/lib/charts/MetricDeltaTable.astro +3 -3
- package/src/components/lib/charts/RadarMetricChart.astro +4 -4
- package/src/components/lib/charts/RadialBarChart.astro +1 -1
- package/src/components/lib/charts/RetentionCohortTable.astro +2 -2
- package/src/components/lib/charts/TreemapChart.astro +1 -1
- package/src/components/lib/ecommerce/CartDrawer.astro +483 -0
- package/src/components/lib/ecommerce/CartPageList.astro +290 -0
- package/src/components/lib/ecommerce/CartToast.astro +276 -0
- package/src/components/lib/ecommerce/CategoryTiles.astro +143 -0
- package/src/components/lib/ecommerce/CheckoutForm.astro +423 -0
- package/src/components/lib/ecommerce/CountdownBanner.astro +230 -0
- package/src/components/lib/ecommerce/FilterBar.astro +278 -0
- package/src/components/lib/ecommerce/FilterSidebar.astro +405 -0
- package/src/components/lib/ecommerce/LiveActivityToast.astro +213 -0
- package/src/components/lib/ecommerce/OrderSummary.astro +295 -0
- package/src/components/lib/ecommerce/ProductBuyBox.astro +247 -0
- package/src/components/lib/ecommerce/ProductCardMinimal.astro +229 -0
- package/src/components/lib/ecommerce/ProductGallery.astro +145 -0
- package/src/components/lib/ecommerce/ProductGrid.astro +118 -0
- package/src/components/lib/ecommerce/ProductMedia.astro +113 -0
- package/src/components/lib/ecommerce/ProductRating.astro +78 -0
- package/src/components/lib/ecommerce/ProductToolbar.astro +93 -0
- package/src/components/lib/ecommerce/PromoBar.astro +145 -0
- package/src/components/lib/ecommerce/StockIndicator.astro +127 -0
- package/src/components/lib/ecommerce/StoreHeader.astro +446 -0
- package/src/components/lib/ecommerce/StoreHero.astro +260 -0
- package/src/components/lib/ecommerce/StoreProductCard.astro +307 -0
- package/src/components/lib/ecommerce/TrustBadges.astro +101 -0
- package/src/components/lib/ecommerce/VariantPicker.astro +145 -0
- package/src/components/lib/ecommerce/cart-store.ts +151 -0
- package/src/components/lib/ecommerce/catalog-filter.ts +62 -0
- package/src/components/lib/ecommerce/index.ts +34 -0
- package/src/components/lib/ecommerce/product-icons.ts +28 -0
- package/src/components/lib/ecommerce/types.ts +70 -0
- package/src/components/lib/forms/AnimatedInput.astro +4 -2
- package/src/components/lib/forms/CustomRadioGroup.astro +3 -1
- package/src/components/lib/index.ts +2 -0
- package/src/components/lib/layout/BentoGrid3.astro +6 -4
- package/src/components/lib/layout/CenterFixedBox.astro +2 -2
- package/src/components/lib/layout/CenteredContainer.astro +4 -2
- package/src/components/lib/layout/ClusterHorizontal.astro +2 -2
- package/src/components/lib/layout/FeatureHero.astro +4 -2
- package/src/components/lib/layout/FooterCenteredMinimal.astro +4 -2
- package/src/components/lib/layout/FullBleedSection.astro +2 -2
- package/src/components/lib/layout/GalleryGrid4.astro +2 -2
- package/src/components/lib/layout/HolyGrailGrid.astro +11 -9
- package/src/components/lib/layout/LandingSectionSplit.astro +5 -3
- package/src/components/lib/layout/MasonryGrid.astro +2 -2
- package/src/components/lib/layout/MinimalFooterGrid.astro +1 -1
- package/src/components/lib/layout/ScrollSnapSection.astro +1 -1
- package/src/components/lib/layout/SidebarLayoutWrapper.astro +4 -2
- package/src/components/lib/layout/SplitScreenLayout.astro +3 -3
- package/src/components/lib/layout/StepLayoutVertical.astro +1 -1
- package/src/components/lib/layout/StickySidebarWrapper.astro +4 -2
- package/src/components/lib/sections/AgencyContactBrief.astro +194 -0
- package/src/components/lib/sections/AgencyFooterCta.astro +94 -0
- package/src/components/lib/sections/AgencyHeader.astro +161 -0
- package/src/components/lib/sections/AgencyHeroKinetic.astro +172 -0
- package/src/components/lib/sections/AgencyServicesList.astro +113 -0
- package/src/components/lib/sections/AgencyTeamGrid.astro +111 -0
- package/src/components/lib/sections/AgencyWorkGrid.astro +147 -0
- package/src/components/lib/sections/BlogGridSection.astro +9 -7
- package/src/components/lib/sections/ButtonsShowcase.astro +20 -18
- package/src/components/lib/sections/CardsShowcase.astro +28 -26
- package/src/components/lib/sections/CareersOpenings.astro +10 -8
- package/src/components/lib/sections/ChangelogSection.astro +20 -7
- package/src/components/lib/sections/ContactGlassForm.astro +10 -8
- package/src/components/lib/sections/ContactSplitMap.astro +11 -9
- package/src/components/lib/sections/CtaAppDownload.astro +13 -11
- package/src/components/lib/sections/CtaGlowBanner.astro +12 -10
- package/src/components/lib/sections/CtaNewsletterSection.astro +13 -11
- package/src/components/lib/sections/CtaSplitImage.astro +13 -11
- package/src/components/lib/sections/DataShowcase.astro +23 -21
- package/src/components/lib/sections/FaqAccordionSection.astro +8 -8
- package/src/components/lib/sections/FaqTwoColumn.astro +9 -7
- package/src/components/lib/sections/FeatureAlternatingRows.astro +10 -8
- package/src/components/lib/sections/FeatureBentoGrid.astro +13 -11
- package/src/components/lib/sections/FeatureIconGrid.astro +6 -6
- package/src/components/lib/sections/FeatureTabsShowcase.astro +13 -11
- package/src/components/lib/sections/FeatureTimelineFlow.astro +8 -6
- package/src/components/lib/sections/FeedbackShowcase.astro +15 -13
- package/src/components/lib/sections/FooterMegaSection.astro +13 -11
- package/src/components/lib/sections/FormsShowcase.astro +22 -20
- package/src/components/lib/sections/HeroDashboardPreview.astro +11 -9
- package/src/components/lib/sections/HeroGlass.astro +22 -20
- package/src/components/lib/sections/HeroGradient.astro +15 -13
- package/src/components/lib/sections/HeroMinimalCentered.astro +10 -10
- package/src/components/lib/sections/HeroShowcaseSplit.astro +18 -16
- package/src/components/lib/sections/HeroSplit.astro +13 -11
- package/src/components/lib/sections/HeroTerminal.astro +14 -12
- package/src/components/lib/sections/HeroWaitlist.astro +10 -8
- package/src/components/lib/sections/HowItWorksSteps.astro +12 -10
- package/src/components/lib/sections/IntegrationsGrid.astro +7 -7
- package/src/components/lib/sections/LayoutShowcase.astro +21 -19
- package/src/components/lib/sections/LogoCloudSection.astro +4 -4
- package/src/components/lib/sections/LogoWallGrid.astro +6 -4
- package/src/components/lib/sections/NavigationShowcase.astro +15 -13
- package/src/components/lib/sections/OverlaysShowcase.astro +21 -19
- package/src/components/lib/sections/PricingComparisonMatrix.astro +15 -13
- package/src/components/lib/sections/PricingSinglePlan.astro +13 -11
- package/src/components/lib/sections/PricingTierCards.astro +11 -9
- package/src/components/lib/sections/PricingToggleCards.astro +13 -13
- package/src/components/lib/sections/ShopCategoryHero.astro +103 -0
- package/src/components/lib/sections/ShopCollectionMosaic.astro +131 -0
- package/src/components/lib/sections/ShopFooterWordmark.astro +100 -0
- package/src/components/lib/sections/ShopHeaderEditorial.astro +396 -0
- package/src/components/lib/sections/ShopHeroEditorial.astro +151 -0
- package/src/components/lib/sections/ShopLookbook.astro +250 -0
- package/src/components/lib/sections/ShopNewsletterEditorial.astro +128 -0
- package/src/components/lib/sections/ShopProductRail.astro +150 -0
- package/src/components/lib/sections/ShopReviewsEditorial.astro +133 -0
- package/src/components/lib/sections/ShopValuesMarquee.astro +91 -0
- package/src/components/lib/sections/StatsCounterBand.astro +9 -7
- package/src/components/lib/sections/StatsDashboardSection.astro +4 -2
- package/src/components/lib/sections/StatsMetricShowcase.astro +9 -7
- package/src/components/lib/sections/TeamGridSection.astro +12 -10
- package/src/components/lib/sections/TestimonialCarousel.astro +10 -8
- package/src/components/lib/sections/TestimonialFeatured.astro +7 -5
- package/src/components/lib/sections/TestimonialGridSection.astro +7 -7
- package/src/components/lib/sections/TestimonialWallGlass.astro +11 -9
- package/src/components/lib/sections/TypographyShowcase.astro +10 -8
- package/src/components/lib/sections/index.ts +10 -0
- package/src/components/lib/templates/ecommerce/StoreCategoryPage.astro +160 -0
- package/src/components/lib/templates/ecommerce/StoreCheckoutPage.astro +98 -0
- package/src/components/lib/templates/ecommerce/StoreHomePage.astro +191 -0
- package/src/components/lib/templates/ecommerce/StoreLayout.astro +169 -0
- package/src/components/lib/templates/ecommerce/StoreProductPage.astro +165 -0
- package/src/components/lib/templates/ecommerce/data.ts +187 -0
- package/src/components/lib/templates/ecommerce/index.ts +14 -0
- package/src/components/lib/templates/ecommerce-editorial/EditorialBagPage.astro +52 -0
- package/src/components/lib/templates/ecommerce-editorial/EditorialCheckoutPage.astro +123 -0
- package/src/components/lib/templates/ecommerce-editorial/EditorialHomePage.astro +102 -0
- package/src/components/lib/templates/ecommerce-editorial/EditorialLayout.astro +216 -0
- package/src/components/lib/templates/ecommerce-editorial/EditorialProductPage.astro +205 -0
- package/src/components/lib/templates/ecommerce-editorial/EditorialShopPage.astro +112 -0
- package/src/components/lib/templates/ecommerce-editorial/data.ts +125 -0
- package/src/components/lib/templates/ecommerce-editorial/index.ts +16 -0
- package/src/components/lib/templates/saas-hotel/AppLayout.astro +83 -0
- package/src/components/lib/templates/saas-hotel/DashboardPage.astro +66 -0
- package/src/components/lib/templates/saas-hotel/GuestsPage.astro +65 -0
- package/src/components/lib/templates/saas-hotel/LandingPage.astro +213 -0
- package/src/components/lib/templates/saas-hotel/ReportsPage.astro +63 -0
- package/src/components/lib/templates/saas-hotel/ReservationsPage.astro +49 -0
- package/src/components/lib/templates/saas-hotel/RoomsPage.astro +48 -0
- package/src/components/lib/templates/saas-hotel/SettingsPage.astro +115 -0
- package/src/components/lib/templates/saas-hotel/SignInPage.astro +50 -0
- package/src/components/lib/templates/saas-hotel/data.ts +229 -0
- package/src/components/lib/templates/saas-hotel/index.ts +19 -0
- package/src/components/lib/templates/saas-hotel/lobby-theme.css +90 -0
- package/src/data/registry/app.ts +93 -0
- package/src/data/registry/buttons.ts +3279 -0
- package/src/data/registry/cards.ts +1753 -0
- package/src/data/registry/charts-svg.ts +557 -0
- package/src/data/registry/charts.ts +500 -0
- package/src/data/registry/data.ts +986 -0
- package/src/data/registry/ecommerce.ts +269 -0
- package/src/data/registry/feedback.ts +1824 -0
- package/src/data/registry/forms.ts +2513 -0
- package/src/data/registry/index.ts +64 -0
- package/src/data/registry/layout.ts +206 -0
- package/src/data/registry/navigation.ts +936 -0
- package/src/data/registry/overlays.ts +1133 -0
- package/src/data/registry/sections-extra.ts +397 -0
- package/src/data/registry/sections.ts +361 -0
- package/src/data/registry/sources.ts +41 -0
- package/src/data/registry/templates.ts +414 -0
- package/src/data/registry/types.ts +30 -0
- package/src/data/registry/typography.ts +777 -0
- package/src/mcp/http-server.ts +324 -0
- package/src/mcp/index.ts +67 -0
- package/src/mcp/installer.ts +178 -0
- package/src/mcp/registry-adapter.ts +278 -0
- package/src/mcp/stdio-server.ts +11 -0
- package/src/mcp/tools.ts +247 -0
- package/src/mcp/types.ts +53 -0
- package/src/styles/global.css +95 -0
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* DataTable — Client-side data table for SaaS apps: search, status filter chips, sortable
|
|
4
|
+
* columns, row selection with a bulk-action bar, pagination and typed cells
|
|
5
|
+
* (text, person, status, money, date, number, tags).
|
|
6
|
+
*
|
|
7
|
+
* @param {string} title - Card title (optional).
|
|
8
|
+
* @param {Array<{key: string, label: string, type?: 'text'|'person'|'status'|'money'|'date'|'number'|'tags', align?: 'left'|'right', sortable?: boolean}>} columns - Column definitions.
|
|
9
|
+
* @param {Array<Record<string, any>>} rows - Row data; `person` cells take `{ name, sub? }`, `status` cells take a string.
|
|
10
|
+
* @param {Record<string, 'success'|'warning'|'danger'|'info'|'default'>} statusTones - Status → tone map.
|
|
11
|
+
* @param {string} filterKey - Column used for the filter chips (optional).
|
|
12
|
+
* @param {boolean} selectable - Show row checkboxes and the bulk bar. Default true.
|
|
13
|
+
* @param {string[]} bulkActions - Bulk bar buttons. Default ['Export', 'Send email'].
|
|
14
|
+
* @param {number} pageSize - Rows per page. Default 8.
|
|
15
|
+
* @param {string} searchPlaceholder - Search placeholder. Default 'Search…'.
|
|
16
|
+
* @param {string} currency - ISO currency for money cells. Default 'EUR'.
|
|
17
|
+
* @param {string} locale - Number/date locale. Default 'en-GB'.
|
|
18
|
+
* @param {string} emptyText - Shown when nothing matches. Default 'No results match your filters.'
|
|
19
|
+
* @param {string} id - Table id (unique per page). Default 'data-table'.
|
|
20
|
+
* @param {string} variant - Style (card|plain). Default 'card'.
|
|
21
|
+
* @param {string} density - Row height (compact|comfortable). Default 'comfortable'.
|
|
22
|
+
* @param {string} className - Extra class.
|
|
23
|
+
*
|
|
24
|
+
* Accessibility: real <table> with a caption; sortable headers are buttons inside <th> with
|
|
25
|
+
* aria-sort; checkboxes have row labels; result counts and page changes are announced politely.
|
|
26
|
+
*/
|
|
27
|
+
import { initials, hueFrom } from './app-icons';
|
|
28
|
+
|
|
29
|
+
type CellType = 'text' | 'person' | 'status' | 'money' | 'date' | 'number' | 'tags';
|
|
30
|
+
interface Column { key: string; label: string; type?: CellType; align?: 'left' | 'right'; sortable?: boolean }
|
|
31
|
+
interface Props {
|
|
32
|
+
title?: string;
|
|
33
|
+
columns?: Column[];
|
|
34
|
+
rows?: Array<Record<string, any>>;
|
|
35
|
+
statusTones?: Record<string, 'success' | 'warning' | 'danger' | 'info' | 'default'>;
|
|
36
|
+
filterKey?: string;
|
|
37
|
+
selectable?: boolean;
|
|
38
|
+
bulkActions?: string[];
|
|
39
|
+
pageSize?: number;
|
|
40
|
+
searchPlaceholder?: string;
|
|
41
|
+
currency?: string;
|
|
42
|
+
locale?: string;
|
|
43
|
+
emptyText?: string;
|
|
44
|
+
id?: string;
|
|
45
|
+
variant?: 'card' | 'plain';
|
|
46
|
+
density?: 'compact' | 'comfortable';
|
|
47
|
+
className?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const {
|
|
51
|
+
title = 'Reservations',
|
|
52
|
+
columns = [
|
|
53
|
+
{ key: 'ref', label: 'Booking', sortable: true },
|
|
54
|
+
{ key: 'guest', label: 'Guest', type: 'person', sortable: true },
|
|
55
|
+
{ key: 'room', label: 'Room', sortable: true },
|
|
56
|
+
{ key: 'checkIn', label: 'Check-in', type: 'date', sortable: true },
|
|
57
|
+
{ key: 'nights', label: 'Nights', type: 'number', align: 'right', sortable: true },
|
|
58
|
+
{ key: 'status', label: 'Status', type: 'status' },
|
|
59
|
+
{ key: 'total', label: 'Total', type: 'money', align: 'right', sortable: true },
|
|
60
|
+
],
|
|
61
|
+
rows = [
|
|
62
|
+
{ ref: 'MR-2841', guest: { name: 'Elena Duarte', sub: 'Booking.com' }, room: '204 · Deluxe', checkIn: '2026-11-04', nights: 3, status: 'Confirmed', total: 612 },
|
|
63
|
+
{ ref: 'MR-2840', guest: { name: 'James Whitfield', sub: 'Direct' }, room: '310 · Suite', checkIn: '2026-11-04', nights: 2, status: 'Checked in', total: 890 },
|
|
64
|
+
{ ref: 'MR-2839', guest: { name: 'Aiko Tanaka', sub: 'Expedia' }, room: '108 · Standard', checkIn: '2026-11-05', nights: 4, status: 'Pending', total: 476 },
|
|
65
|
+
{ ref: 'MR-2838', guest: { name: 'Marco Bellini', sub: 'Direct' }, room: '212 · Deluxe', checkIn: '2026-11-06', nights: 1, status: 'Cancelled', total: 0 },
|
|
66
|
+
],
|
|
67
|
+
statusTones = { Confirmed: 'info', 'Checked in': 'success', 'Checked out': 'default', Pending: 'warning', Cancelled: 'danger', 'No show': 'danger' },
|
|
68
|
+
filterKey = 'status',
|
|
69
|
+
selectable = true,
|
|
70
|
+
bulkActions = ['Export', 'Send email'],
|
|
71
|
+
pageSize = 8,
|
|
72
|
+
searchPlaceholder = 'Search…',
|
|
73
|
+
currency = 'EUR',
|
|
74
|
+
locale = 'en-GB',
|
|
75
|
+
emptyText = 'No results match your filters.',
|
|
76
|
+
id = 'data-table',
|
|
77
|
+
variant = 'card',
|
|
78
|
+
density = 'comfortable',
|
|
79
|
+
className = '',
|
|
80
|
+
} = Astro.props;
|
|
81
|
+
|
|
82
|
+
const money = (n: number) => new Intl.NumberFormat(locale, { style: 'currency', currency, maximumFractionDigits: 0 }).format(n);
|
|
83
|
+
const date = (v: string) => new Intl.DateTimeFormat(locale, { day: 'numeric', month: 'short', timeZone: 'UTC' }).format(new Date(`${v}T00:00:00Z`));
|
|
84
|
+
const filters = filterKey ? Array.from(new Set(rows.map((r) => String(r[filterKey])))) : [];
|
|
85
|
+
const searchText = (r: Record<string, any>) =>
|
|
86
|
+
columns.map((c) => (c.type === 'person' ? `${r[c.key]?.name ?? ''} ${r[c.key]?.sub ?? ''}` : Array.isArray(r[c.key]) ? r[c.key].join(' ') : r[c.key] ?? '')).join(' ').toLowerCase();
|
|
87
|
+
const sortValue = (c: Column, v: any) => (c.type === 'person' ? v?.name ?? '' : v ?? '');
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
<section class:list={['dt', `dt--${variant}`, `dt--${density}`, className]} data-table={id} data-page-size={pageSize} aria-labelledby={`${id}-title`}>
|
|
91
|
+
<div class="dt__toolbar">
|
|
92
|
+
<h2 class="dt__title" id={`${id}-title`}>{title} <span class="dt__count" aria-live="polite"><span data-count>{rows.length}</span></span></h2>
|
|
93
|
+
<div class="dt__tools">
|
|
94
|
+
<label class="dt__search">
|
|
95
|
+
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg>
|
|
96
|
+
<span class="sr-only">{searchPlaceholder}</span>
|
|
97
|
+
<input type="search" placeholder={searchPlaceholder} data-search />
|
|
98
|
+
</label>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
{filters.length > 1 && (
|
|
102
|
+
<div class="dt__chips" role="group" aria-label="Filter by status">
|
|
103
|
+
<button type="button" class="dt__chip" aria-pressed="true" data-filter="">All <span>{rows.length}</span></button>
|
|
104
|
+
{filters.map((f) => (
|
|
105
|
+
<button type="button" class="dt__chip" aria-pressed="false" data-filter={f}>{f} <span>{rows.filter((r) => String(r[filterKey]) === f).length}</span></button>
|
|
106
|
+
))}
|
|
107
|
+
</div>
|
|
108
|
+
)}
|
|
109
|
+
|
|
110
|
+
{selectable && (
|
|
111
|
+
<div class="dt__bulk" hidden data-bulk>
|
|
112
|
+
<span><strong data-selected>0</strong> selected</span>
|
|
113
|
+
{bulkActions.map((a) => <button type="button" class="dt__bulk-btn">{a}</button>)}
|
|
114
|
+
<button type="button" class="dt__bulk-clear" data-clear>Clear</button>
|
|
115
|
+
</div>
|
|
116
|
+
)}
|
|
117
|
+
|
|
118
|
+
<div class="dt__scroll" tabindex="0" role="region" aria-labelledby={`${id}-title`}>
|
|
119
|
+
<table class="dt__table">
|
|
120
|
+
<caption class="sr-only">{title}</caption>
|
|
121
|
+
<thead>
|
|
122
|
+
<tr>
|
|
123
|
+
{selectable && <th class="dt__check" scope="col"><input type="checkbox" aria-label="Select all rows on this page" data-all /></th>}
|
|
124
|
+
{columns.map((c) => (
|
|
125
|
+
<th scope="col" class:list={[{ 'dt__right': c.align === 'right' }]} aria-sort={c.sortable ? 'none' : undefined}>
|
|
126
|
+
{c.sortable ? (
|
|
127
|
+
<button type="button" class="dt__sort" data-sort={c.key} data-type={c.type ?? 'text'}>
|
|
128
|
+
{c.label}
|
|
129
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m7 15 5 5 5-5M7 9l5-5 5 5"/></svg>
|
|
130
|
+
</button>
|
|
131
|
+
) : c.label}
|
|
132
|
+
</th>
|
|
133
|
+
))}
|
|
134
|
+
</tr>
|
|
135
|
+
</thead>
|
|
136
|
+
<tbody data-body>
|
|
137
|
+
{rows.map((r, i) => (
|
|
138
|
+
<tr data-row data-search={searchText(r)} data-filter-value={filterKey ? String(r[filterKey]) : ''} data-index={i} {...Object.fromEntries(columns.map((c) => [`data-v-${c.key.toLowerCase()}`, String(sortValue(c, r[c.key]))]))}>
|
|
139
|
+
{selectable && <td class="dt__check"><input type="checkbox" aria-label={`Select ${columns[0] ? sortValue(columns[0], r[columns[0].key]) : `row ${i + 1}`}`} data-select /></td>}
|
|
140
|
+
{columns.map((c) => {
|
|
141
|
+
const v = r[c.key];
|
|
142
|
+
return (
|
|
143
|
+
<td class:list={[{ 'dt__right': c.align === 'right' }]}>
|
|
144
|
+
{c.type === 'person' ? (
|
|
145
|
+
<span class="dt__person">
|
|
146
|
+
<span class="dt__avatar" style={`--h: ${hueFrom(v?.name ?? '')}`} aria-hidden="true">{initials(v?.name ?? '?')}</span>
|
|
147
|
+
<span><strong>{v?.name}</strong>{v?.sub && <small>{v.sub}</small>}</span>
|
|
148
|
+
</span>
|
|
149
|
+
) : c.type === 'status' ? (
|
|
150
|
+
<span class:list={['dt__status', `dt__status--${statusTones[v] ?? 'default'}`]}><i aria-hidden="true"></i>{v}</span>
|
|
151
|
+
) : c.type === 'money' ? money(Number(v) || 0)
|
|
152
|
+
: c.type === 'date' ? <time datetime={v}>{date(v)}</time>
|
|
153
|
+
: c.type === 'tags' ? <span class="dt__tags">{(v ?? []).map((tg: string) => <span>{tg}</span>)}</span>
|
|
154
|
+
: v}
|
|
155
|
+
</td>
|
|
156
|
+
);
|
|
157
|
+
})}
|
|
158
|
+
</tr>
|
|
159
|
+
))}
|
|
160
|
+
</tbody>
|
|
161
|
+
</table>
|
|
162
|
+
<p class="dt__empty" hidden data-empty>{emptyText}</p>
|
|
163
|
+
</div>
|
|
164
|
+
|
|
165
|
+
<div class="dt__foot">
|
|
166
|
+
<p class="dt__range" aria-live="polite" data-range></p>
|
|
167
|
+
<div class="dt__pager">
|
|
168
|
+
<button type="button" class="dt__page" aria-label="Previous page" data-prev>‹</button>
|
|
169
|
+
<span class="dt__pages" data-pages></span>
|
|
170
|
+
<button type="button" class="dt__page" aria-label="Next page" data-next>›</button>
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
</section>
|
|
174
|
+
|
|
175
|
+
<script>
|
|
176
|
+
document.querySelectorAll<HTMLElement>('[data-table]:not([data-bound])').forEach((root) => {
|
|
177
|
+
root.dataset.bound = '';
|
|
178
|
+
const body = root.querySelector<HTMLTableSectionElement>('[data-body]')!;
|
|
179
|
+
const rows = Array.from(body.querySelectorAll<HTMLTableRowElement>('[data-row]'));
|
|
180
|
+
const size = Number(root.dataset.pageSize) || 8;
|
|
181
|
+
const search = root.querySelector<HTMLInputElement>('[data-search]');
|
|
182
|
+
const chips = Array.from(root.querySelectorAll<HTMLButtonElement>('[data-filter]'));
|
|
183
|
+
const sorters = Array.from(root.querySelectorAll<HTMLButtonElement>('[data-sort]'));
|
|
184
|
+
const all = root.querySelector<HTMLInputElement>('[data-all]');
|
|
185
|
+
const bulk = root.querySelector<HTMLElement>('[data-bulk]');
|
|
186
|
+
const pagesEl = root.querySelector<HTMLElement>('[data-pages]')!;
|
|
187
|
+
let query = '';
|
|
188
|
+
let filter = '';
|
|
189
|
+
let sortKey = '';
|
|
190
|
+
let sortDir = 1;
|
|
191
|
+
let page = 0;
|
|
192
|
+
|
|
193
|
+
function visibleRows() {
|
|
194
|
+
return rows.filter((r) => (!query || (r.dataset.search || '').includes(query)) && (!filter || r.dataset.filterValue === filter));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function render() {
|
|
198
|
+
let list = visibleRows();
|
|
199
|
+
if (sortKey) {
|
|
200
|
+
const type = sorters.find((s) => s.dataset.sort === sortKey)?.dataset.type;
|
|
201
|
+
const attr = `v${sortKey.toLowerCase().replace(/^./, (c) => c.toUpperCase())}`;
|
|
202
|
+
list = [...list].sort((a, b) => {
|
|
203
|
+
const A = a.dataset[attr] ?? '', B = b.dataset[attr] ?? '';
|
|
204
|
+
const cmp = type === 'money' || type === 'number' ? Number(A) - Number(B) : A.localeCompare(B);
|
|
205
|
+
return cmp * sortDir;
|
|
206
|
+
});
|
|
207
|
+
} else {
|
|
208
|
+
list = [...list].sort((a, b) => Number(a.dataset.index) - Number(b.dataset.index));
|
|
209
|
+
}
|
|
210
|
+
const pages = Math.max(1, Math.ceil(list.length / size));
|
|
211
|
+
page = Math.min(page, pages - 1);
|
|
212
|
+
const shown = list.slice(page * size, page * size + size);
|
|
213
|
+
rows.forEach((r) => { r.hidden = true; });
|
|
214
|
+
body.append(...list);
|
|
215
|
+
shown.forEach((r) => { r.hidden = false; });
|
|
216
|
+
root.querySelector('[data-count]')!.textContent = String(list.length);
|
|
217
|
+
root.querySelector<HTMLElement>('[data-empty]')!.hidden = list.length > 0;
|
|
218
|
+
root.querySelector('[data-range]')!.textContent = list.length ? `${page * size + 1}–${page * size + shown.length} of ${list.length}` : '';
|
|
219
|
+
pagesEl.replaceChildren(...Array.from({ length: pages }, (_, i) => {
|
|
220
|
+
const b = document.createElement('button');
|
|
221
|
+
b.type = 'button';
|
|
222
|
+
b.className = 'dt__page';
|
|
223
|
+
b.textContent = String(i + 1);
|
|
224
|
+
b.setAttribute('aria-label', `Page ${i + 1}`);
|
|
225
|
+
if (i === page) b.setAttribute('aria-current', 'page');
|
|
226
|
+
b.addEventListener('click', () => { page = i; render(); });
|
|
227
|
+
return b;
|
|
228
|
+
}));
|
|
229
|
+
root.querySelector<HTMLButtonElement>('[data-prev]')!.disabled = page === 0;
|
|
230
|
+
root.querySelector<HTMLButtonElement>('[data-next]')!.disabled = page >= pages - 1;
|
|
231
|
+
syncSelection();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function syncSelection() {
|
|
235
|
+
const boxes = rows.map((r) => r.querySelector<HTMLInputElement>('[data-select]')).filter(Boolean) as HTMLInputElement[];
|
|
236
|
+
const selected = boxes.filter((b) => b.checked);
|
|
237
|
+
rows.forEach((r) => r.classList.toggle('is-selected', Boolean(r.querySelector<HTMLInputElement>('[data-select]')?.checked)));
|
|
238
|
+
if (bulk) {
|
|
239
|
+
bulk.hidden = selected.length === 0;
|
|
240
|
+
bulk.querySelector('[data-selected]')!.textContent = String(selected.length);
|
|
241
|
+
}
|
|
242
|
+
if (all) {
|
|
243
|
+
const pageBoxes = rows.filter((r) => !r.hidden).map((r) => r.querySelector<HTMLInputElement>('[data-select]')!).filter(Boolean);
|
|
244
|
+
all.checked = pageBoxes.length > 0 && pageBoxes.every((b) => b.checked);
|
|
245
|
+
all.indeterminate = !all.checked && pageBoxes.some((b) => b.checked);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
search?.addEventListener('input', () => { query = search.value.trim().toLowerCase(); page = 0; render(); });
|
|
250
|
+
chips.forEach((c) => c.addEventListener('click', () => {
|
|
251
|
+
filter = c.dataset.filter || '';
|
|
252
|
+
chips.forEach((x) => x.setAttribute('aria-pressed', String(x === c)));
|
|
253
|
+
page = 0;
|
|
254
|
+
render();
|
|
255
|
+
}));
|
|
256
|
+
sorters.forEach((s) => s.addEventListener('click', () => {
|
|
257
|
+
if (sortKey === s.dataset.sort) sortDir = sortDir === 1 ? -1 : sortDir === -1 ? 0 : 1;
|
|
258
|
+
else { sortKey = s.dataset.sort || ''; sortDir = 1; }
|
|
259
|
+
if (sortDir === 0) sortKey = '';
|
|
260
|
+
sorters.forEach((x) => x.closest('th')?.setAttribute('aria-sort', x === s && sortKey ? (sortDir === 1 ? 'ascending' : 'descending') : 'none'));
|
|
261
|
+
render();
|
|
262
|
+
}));
|
|
263
|
+
root.querySelector('[data-prev]')?.addEventListener('click', () => { page = Math.max(0, page - 1); render(); });
|
|
264
|
+
root.querySelector('[data-next]')?.addEventListener('click', () => { page += 1; render(); });
|
|
265
|
+
root.addEventListener('change', (e) => { if ((e.target as HTMLElement).matches('[data-select]')) syncSelection(); });
|
|
266
|
+
all?.addEventListener('change', () => {
|
|
267
|
+
rows.filter((r) => !r.hidden).forEach((r) => { const b = r.querySelector<HTMLInputElement>('[data-select]'); if (b) b.checked = all.checked; });
|
|
268
|
+
syncSelection();
|
|
269
|
+
});
|
|
270
|
+
root.querySelector('[data-clear]')?.addEventListener('click', () => {
|
|
271
|
+
rows.forEach((r) => { const b = r.querySelector<HTMLInputElement>('[data-select]'); if (b) b.checked = false; });
|
|
272
|
+
syncSelection();
|
|
273
|
+
});
|
|
274
|
+
render();
|
|
275
|
+
});
|
|
276
|
+
</script>
|
|
277
|
+
|
|
278
|
+
<style>
|
|
279
|
+
.dt { container-type: inline-size; display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--sp-3, 0.75rem); min-width: 0; }
|
|
280
|
+
.dt--card { padding: var(--sp-5, 1.25rem); border-radius: var(--r-lg, 16px); border: 1px solid var(--c-border, rgba(255, 255, 255, 0.08)); background: var(--c-bg-1, #0a0a0a); }
|
|
281
|
+
.dt__toolbar { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3, 0.75rem); }
|
|
282
|
+
.dt__title { display: flex; align-items: center; gap: var(--sp-2, 0.5rem); margin: 0; font-size: 1rem; font-weight: 700; color: var(--c-text-1, #fff); }
|
|
283
|
+
.dt__count { padding: 1px 8px; border-radius: 999px; background: rgb(var(--ink-rgb, 255 255 255) / 0.07); font-size: 0.72rem; color: var(--c-text-2, #a3a3a3); }
|
|
284
|
+
.dt__search { display: flex; align-items: center; gap: 6px; height: 38px; padding: 0 0.7rem; border-radius: 10px; border: 1px solid var(--c-border, rgba(255, 255, 255, 0.1)); color: var(--c-text-3, #737373); }
|
|
285
|
+
.dt__search:focus-within { border-color: var(--app-accent, var(--c-primary-light, #d4a017)); }
|
|
286
|
+
.dt__search input { width: 200px; border: 0; outline: 0; background: none; color: var(--c-text-1, #fff); font: inherit; font-size: 0.85rem; }
|
|
287
|
+
|
|
288
|
+
.dt__chips { display: flex; gap: 6px; overflow-x: auto; scrollbar-width: none; }
|
|
289
|
+
.dt__chip { display: inline-flex; align-items: center; gap: 6px; min-height: 32px; padding: 0 0.7rem; border-radius: 999px; border: 1px solid var(--c-border, rgba(255, 255, 255, 0.1)); background: transparent; color: var(--c-text-2, #a3a3a3); font: inherit; font-size: 0.78rem; font-weight: 600; white-space: nowrap; cursor: pointer; }
|
|
290
|
+
.dt__chip span { color: var(--c-text-3, #737373); }
|
|
291
|
+
.dt__chip[aria-pressed='true'] { background: var(--c-text-1, #fff); border-color: var(--c-text-1, #fff); color: var(--c-bg, #050505); }
|
|
292
|
+
.dt__chip[aria-pressed='true'] span { color: inherit; opacity: 0.7; }
|
|
293
|
+
|
|
294
|
+
.dt__bulk { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2, 0.5rem); padding: var(--sp-2, 0.5rem) var(--sp-3, 0.75rem); border-radius: 10px; background: color-mix(in srgb, var(--app-accent, var(--c-primary-light, #d4a017)) 12%, transparent); font-size: 0.82rem; color: var(--c-text-1, #fff); }
|
|
295
|
+
.dt__bulk[hidden] { display: none; }
|
|
296
|
+
.dt__bulk > span { margin-right: auto; }
|
|
297
|
+
.dt__bulk-btn, .dt__bulk-clear { min-height: 32px; padding: 0 0.7rem; border-radius: 8px; border: 1px solid var(--c-border, rgba(255, 255, 255, 0.15)); background: var(--c-bg-1, #0a0a0a); color: var(--c-text-1, #fff); font: inherit; font-size: 0.78rem; font-weight: 600; cursor: pointer; }
|
|
298
|
+
.dt__bulk-clear { background: transparent; border-color: transparent; text-decoration: underline; }
|
|
299
|
+
|
|
300
|
+
.dt__scroll { overflow-x: auto; border-radius: 10px; border: 1px solid var(--c-border, rgba(255, 255, 255, 0.08)); }
|
|
301
|
+
.dt--plain .dt__scroll { border: 0; }
|
|
302
|
+
.dt__table { width: 100%; border-collapse: collapse; font-size: 0.85rem; }
|
|
303
|
+
.dt__table th { position: sticky; top: 0; padding: 0 var(--sp-3, 0.75rem); height: 40px; background: rgb(var(--ink-rgb, 255 255 255) / 0.03); color: var(--c-text-3, #737373); font-size: 0.72rem; font-weight: 700; letter-spacing: 0.04em; text-align: left; text-transform: uppercase; white-space: nowrap; border-bottom: 1px solid var(--c-border, rgba(255, 255, 255, 0.08)); }
|
|
304
|
+
.dt__table td { padding: 0 var(--sp-3, 0.75rem); height: 56px; color: var(--c-text-2, #a3a3a3); border-bottom: 1px solid var(--c-border, rgba(255, 255, 255, 0.06)); white-space: nowrap; }
|
|
305
|
+
.dt--compact .dt__table td { height: 44px; }
|
|
306
|
+
.dt__table tbody tr:last-child td { border-bottom: 0; }
|
|
307
|
+
.dt__table tbody tr:hover td { background: rgb(var(--ink-rgb, 255 255 255) / 0.025); }
|
|
308
|
+
.dt__table tbody tr.is-selected td { background: color-mix(in srgb, var(--app-accent, var(--c-primary-light, #d4a017)) 8%, transparent); }
|
|
309
|
+
.dt__table tr[hidden] { display: none; }
|
|
310
|
+
.dt__right { text-align: right !important; font-variant-numeric: tabular-nums; }
|
|
311
|
+
.dt__check { width: 44px; }
|
|
312
|
+
.dt__check input { width: 16px; height: 16px; accent-color: var(--app-accent, var(--c-primary-light, #d4a017)); cursor: pointer; }
|
|
313
|
+
.dt__sort { display: inline-flex; align-items: center; gap: 4px; min-height: 32px; padding: 0; border: 0; background: none; color: inherit; font: inherit; letter-spacing: inherit; text-transform: inherit; cursor: pointer; }
|
|
314
|
+
.dt__sort svg { opacity: 0.4; }
|
|
315
|
+
th[aria-sort='ascending'] .dt__sort, th[aria-sort='descending'] .dt__sort { color: var(--c-text-1, #fff); }
|
|
316
|
+
th[aria-sort='ascending'] .dt__sort svg, th[aria-sort='descending'] .dt__sort svg { opacity: 1; color: var(--app-accent, var(--c-primary-light, #d4a017)); }
|
|
317
|
+
.dt__person { display: flex; align-items: center; gap: var(--sp-3, 0.75rem); }
|
|
318
|
+
.dt__person strong { display: block; color: var(--c-text-1, #fff); font-weight: 600; }
|
|
319
|
+
.dt__person small { display: block; font-size: 0.75rem; color: var(--c-text-3, #737373); }
|
|
320
|
+
.dt__avatar { display: grid; place-items: center; flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; background: hsl(var(--h) 50% 45%); color: #fff; font-size: 0.7rem; font-weight: 700; }
|
|
321
|
+
.dt__status { --tone: var(--c-text-2, #a3a3a3); display: inline-flex; align-items: center; gap: 6px; padding: 3px 9px; border-radius: 999px; background: color-mix(in srgb, var(--tone) 12%, transparent); color: var(--tone); font-size: 0.75rem; font-weight: 600; }
|
|
322
|
+
.dt__status i { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
|
|
323
|
+
.dt__status--success { --tone: var(--c-success, #34d399); }
|
|
324
|
+
.dt__status--warning { --tone: var(--c-warning, #fbbf24); }
|
|
325
|
+
.dt__status--danger { --tone: var(--c-danger, #f87171); }
|
|
326
|
+
.dt__status--info { --tone: var(--app-accent, var(--c-primary-light, #d4a017)); }
|
|
327
|
+
.dt__tags { display: inline-flex; gap: 4px; }
|
|
328
|
+
.dt__tags span { padding: 2px 7px; border-radius: 6px; background: rgb(var(--ink-rgb, 255 255 255) / 0.06); font-size: 0.72rem; }
|
|
329
|
+
.dt__empty { margin: 0; padding: var(--sp-10, 2.5rem); text-align: center; color: var(--c-text-3, #737373); }
|
|
330
|
+
.dt__empty[hidden] { display: none; }
|
|
331
|
+
|
|
332
|
+
.dt__foot { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3, 0.75rem); }
|
|
333
|
+
.dt__range { margin: 0; font-size: 0.8rem; color: var(--c-text-3, #737373); }
|
|
334
|
+
.dt__pager, .dt__pages { display: flex; gap: 4px; }
|
|
335
|
+
.dt__pages :global(.dt__page), .dt__page { min-width: 34px; height: 34px; border: 1px solid var(--c-border, rgba(255, 255, 255, 0.1)); border-radius: 8px; background: transparent; color: var(--c-text-2, #a3a3a3); font: inherit; font-size: 0.8rem; cursor: pointer; }
|
|
336
|
+
.dt__pages :global(.dt__page[aria-current='page']) { background: var(--c-text-1, #fff); color: var(--c-bg, #050505); border-color: var(--c-text-1, #fff); }
|
|
337
|
+
.dt__page:disabled { opacity: 0.35; cursor: default; }
|
|
338
|
+
.dt :is(button, input, [tabindex='0']):focus-visible, .dt__pages :global(.dt__page:focus-visible) { outline: 2px solid var(--app-accent, var(--c-primary-light, #d4a017)); outline-offset: 2px; }
|
|
339
|
+
.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
|
|
340
|
+
|
|
341
|
+
@container (max-width: 560px) {
|
|
342
|
+
.dt__toolbar { flex-direction: column; align-items: stretch; }
|
|
343
|
+
.dt__search input { width: 100%; }
|
|
344
|
+
.dt__foot { flex-direction: column; }
|
|
345
|
+
}
|
|
346
|
+
</style>
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* GuestProfileCard — CRM profile for a guest or customer: avatar, loyalty tier, contact details,
|
|
4
|
+
* lifetime stats, preference tags, upcoming stay and internal notes.
|
|
5
|
+
*
|
|
6
|
+
* @param {{name: string, email?: string, phone?: string, country?: string, since?: string}} guest - Identity and contact.
|
|
7
|
+
* @param {string} tier - Loyalty tier label (optional). Default 'Gold'.
|
|
8
|
+
* @param {Array<{label: string, value: string}>} stats - Lifetime figures (stays, nights, spend…).
|
|
9
|
+
* @param {string[]} preferences - Preference tags.
|
|
10
|
+
* @param {{room: string, dates: string, status?: string}} upcoming - Next stay (optional).
|
|
11
|
+
* @param {string[]} notes - Staff notes (optional).
|
|
12
|
+
* @param {Array<{label: string, href?: string, primary?: boolean}>} actions - Buttons in the header.
|
|
13
|
+
* @param {string} variant - Style (card|plain). Default 'card'.
|
|
14
|
+
* @param {string} className - Extra class.
|
|
15
|
+
*
|
|
16
|
+
* Accessibility: h2 with the guest name; contact items are real mailto/tel links; stats are a
|
|
17
|
+
* description list; tags are a plain list.
|
|
18
|
+
*/
|
|
19
|
+
import { appIcon, initials, hueFrom } from './app-icons';
|
|
20
|
+
|
|
21
|
+
interface Props {
|
|
22
|
+
guest?: { name: string; email?: string; phone?: string; country?: string; since?: string };
|
|
23
|
+
tier?: string;
|
|
24
|
+
stats?: Array<{ label: string; value: string }>;
|
|
25
|
+
preferences?: string[];
|
|
26
|
+
upcoming?: { room: string; dates: string; status?: string };
|
|
27
|
+
notes?: string[];
|
|
28
|
+
actions?: Array<{ label: string; href?: string; primary?: boolean }>;
|
|
29
|
+
variant?: 'card' | 'plain';
|
|
30
|
+
className?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const {
|
|
34
|
+
guest = { name: 'Elena Duarte', email: 'elena.duarte@mail.com', phone: '+34 612 345 678', country: 'Spain', since: 'Mar 2022' },
|
|
35
|
+
tier = 'Gold',
|
|
36
|
+
stats = [{ label: 'Stays', value: '7' }, { label: 'Nights', value: '19' }, { label: 'Lifetime spend', value: '€4,820' }, { label: 'Avg. rating', value: '4.9' }],
|
|
37
|
+
preferences = ['High floor', 'Feather-free pillows', 'Late check-out', 'Sparkling water'],
|
|
38
|
+
upcoming = { room: '204 · Deluxe King', dates: 'Nov 4 – Nov 7', status: 'Confirmed' },
|
|
39
|
+
notes = ['Celebrating anniversary — prepare welcome card.', 'Prefers WhatsApp for contact.'],
|
|
40
|
+
actions = [{ label: 'Message' }, { label: 'New booking', primary: true }],
|
|
41
|
+
variant = 'card',
|
|
42
|
+
className = '',
|
|
43
|
+
} = Astro.props;
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
<article class:list={['gp', `gp--${variant}`, className]} aria-labelledby="gp-name">
|
|
47
|
+
<header class="gp__head">
|
|
48
|
+
<span class="gp__avatar" style={`--h: ${hueFrom(guest.name)}`} aria-hidden="true">{initials(guest.name)}</span>
|
|
49
|
+
<div class="gp__id">
|
|
50
|
+
<h2 class="gp__name" id="gp-name">{guest.name}</h2>
|
|
51
|
+
<p class="gp__sub">
|
|
52
|
+
{tier && <span class="gp__tier"><svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><Fragment set:html={appIcon('star')} /></svg>{tier} member</span>}
|
|
53
|
+
{guest.since && <span>Guest since {guest.since}</span>}
|
|
54
|
+
</p>
|
|
55
|
+
</div>
|
|
56
|
+
<div class="gp__actions">
|
|
57
|
+
{actions.map((a) => {
|
|
58
|
+
const Tag = a.href ? 'a' : 'button';
|
|
59
|
+
return <Tag class:list={['gp__btn', { 'gp__btn--primary': a.primary }]} href={a.href} type={a.href ? undefined : 'button'}>{a.label}</Tag>;
|
|
60
|
+
})}
|
|
61
|
+
</div>
|
|
62
|
+
</header>
|
|
63
|
+
|
|
64
|
+
<ul class="gp__contact">
|
|
65
|
+
{guest.email && <li><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><Fragment set:html={appIcon('mail')} /></svg><a href={`mailto:${guest.email}`}>{guest.email}</a></li>}
|
|
66
|
+
{guest.phone && <li><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><Fragment set:html={appIcon('phone')} /></svg><a href={`tel:${guest.phone.replace(/\s/g, '')}`}>{guest.phone}</a></li>}
|
|
67
|
+
{guest.country && <li><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><Fragment set:html={appIcon('plane')} /></svg>{guest.country}</li>}
|
|
68
|
+
</ul>
|
|
69
|
+
|
|
70
|
+
<dl class="gp__stats">
|
|
71
|
+
{stats.map((s) => <div><dt>{s.label}</dt><dd>{s.value}</dd></div>)}
|
|
72
|
+
</dl>
|
|
73
|
+
|
|
74
|
+
{upcoming && (
|
|
75
|
+
<div class="gp__block">
|
|
76
|
+
<h3>Upcoming stay</h3>
|
|
77
|
+
<p class="gp__stay"><strong>{upcoming.room}</strong><span>{upcoming.dates}</span>{upcoming.status && <em>{upcoming.status}</em>}</p>
|
|
78
|
+
</div>
|
|
79
|
+
)}
|
|
80
|
+
{preferences.length > 0 && (
|
|
81
|
+
<div class="gp__block">
|
|
82
|
+
<h3>Preferences</h3>
|
|
83
|
+
<ul class="gp__tags">{preferences.map((p) => <li>{p}</li>)}</ul>
|
|
84
|
+
</div>
|
|
85
|
+
)}
|
|
86
|
+
{notes.length > 0 && (
|
|
87
|
+
<div class="gp__block">
|
|
88
|
+
<h3>Staff notes</h3>
|
|
89
|
+
<ul class="gp__notes">{notes.map((n) => <li>{n}</li>)}</ul>
|
|
90
|
+
</div>
|
|
91
|
+
)}
|
|
92
|
+
</article>
|
|
93
|
+
|
|
94
|
+
<style>
|
|
95
|
+
.gp { container-type: inline-size; display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--sp-5, 1.25rem); min-width: 0; }
|
|
96
|
+
.gp--card { padding: var(--sp-6, 1.5rem); border-radius: var(--r-lg, 16px); border: 1px solid var(--c-border, rgba(255, 255, 255, 0.08)); background: var(--c-bg-1, #0a0a0a); }
|
|
97
|
+
.gp__head { display: flex; align-items: center; gap: var(--sp-4, 1rem); }
|
|
98
|
+
.gp__avatar { display: grid; place-items: center; flex-shrink: 0; width: 56px; height: 56px; border-radius: 16px; background: hsl(var(--h) 50% 45%); color: #fff; font-size: 1.05rem; font-weight: 800; }
|
|
99
|
+
.gp__id { flex: 1; min-width: 0; }
|
|
100
|
+
.gp__name { margin: 0; font-size: 1.25rem; font-weight: 800; letter-spacing: -0.02em; color: var(--c-text-1, #fff); }
|
|
101
|
+
.gp__sub { display: flex; flex-wrap: wrap; gap: 4px 12px; margin: 4px 0 0; font-size: 0.8rem; color: var(--c-text-3, #737373); }
|
|
102
|
+
.gp__tier { display: inline-flex; align-items: center; gap: 4px; color: var(--c-warning, #fbbf24); font-weight: 700; }
|
|
103
|
+
.gp__actions { display: flex; gap: var(--sp-2, 0.5rem); }
|
|
104
|
+
.gp__btn { display: inline-flex; align-items: center; min-height: 38px; padding: 0 0.85rem; border-radius: 10px; border: 1px solid var(--c-border, rgba(255, 255, 255, 0.12)); background: transparent; color: var(--c-text-1, #fff); font: inherit; font-size: 0.82rem; font-weight: 600; text-decoration: none; cursor: pointer; }
|
|
105
|
+
.gp__btn:hover { background: rgb(var(--ink-rgb, 255 255 255) / 0.05); color: var(--c-text-1, #fff); }
|
|
106
|
+
.gp__btn--primary { border-color: transparent; background: var(--app-accent, var(--c-primary-light, #d4a017)); color: var(--c-on-primary, #050505); }
|
|
107
|
+
.gp__btn--primary:hover { background: var(--app-accent, var(--c-primary-light, #d4a017)); color: var(--c-on-primary, #050505); filter: brightness(1.08); }
|
|
108
|
+
.gp :is(a, button):focus-visible { outline: 2px solid var(--app-accent, var(--c-primary-light, #d4a017)); outline-offset: 2px; }
|
|
109
|
+
|
|
110
|
+
.gp__contact { display: flex; flex-wrap: wrap; gap: var(--sp-2, 0.5rem) var(--sp-5, 1.25rem); margin: 0; padding: 0; list-style: none; font-size: 0.85rem; color: var(--c-text-2, #a3a3a3); }
|
|
111
|
+
.gp__contact li { display: inline-flex; align-items: center; gap: 6px; }
|
|
112
|
+
.gp__contact a { color: var(--c-text-1, #fff); text-decoration: none; }
|
|
113
|
+
.gp__contact a:hover { text-decoration: underline; }
|
|
114
|
+
.gp__stats { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 1px; margin: 0; border-radius: 12px; overflow: hidden; background: var(--c-border, rgba(255, 255, 255, 0.08)); border: 1px solid var(--c-border, rgba(255, 255, 255, 0.08)); }
|
|
115
|
+
.gp__stats div { display: flex; flex-direction: column-reverse; padding: var(--sp-3, 0.75rem) var(--sp-4, 1rem); background: var(--c-bg-1, #0a0a0a); }
|
|
116
|
+
.gp__stats dt { font-size: 0.72rem; color: var(--c-text-3, #737373); }
|
|
117
|
+
.gp__stats dd { margin: 0; font-size: 1.2rem; font-weight: 800; color: var(--c-text-1, #fff); font-variant-numeric: tabular-nums; }
|
|
118
|
+
.gp__block h3 { margin: 0 0 var(--sp-2, 0.5rem); font-size: 0.72rem; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; color: var(--c-text-3, #737373); }
|
|
119
|
+
.gp__stay { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 12px; margin: 0; padding: var(--sp-3, 0.75rem) var(--sp-4, 1rem); border-radius: 10px; background: color-mix(in srgb, var(--app-accent, var(--c-primary-light, #d4a017)) 8%, transparent); font-size: 0.85rem; color: var(--c-text-2, #a3a3a3); }
|
|
120
|
+
.gp__stay strong { color: var(--c-text-1, #fff); }
|
|
121
|
+
.gp__stay em { margin-left: auto; font-style: normal; font-size: 0.75rem; font-weight: 700; color: var(--app-accent, var(--c-primary-light, #d4a017)); }
|
|
122
|
+
.gp__tags { display: flex; flex-wrap: wrap; gap: 6px; margin: 0; padding: 0; list-style: none; }
|
|
123
|
+
.gp__tags li { padding: 4px 10px; border-radius: 999px; border: 1px solid var(--c-border, rgba(255, 255, 255, 0.1)); font-size: 0.78rem; color: var(--c-text-1, #fff); }
|
|
124
|
+
.gp__notes { display: grid; gap: 6px; margin: 0; padding: 0; list-style: none; }
|
|
125
|
+
.gp__notes li { padding: var(--sp-2, 0.5rem) var(--sp-3, 0.75rem); border-left: 2px solid var(--c-warning, #fbbf24); background: rgb(var(--ink-rgb, 255 255 255) / 0.03); font-size: 0.82rem; color: var(--c-text-2, #a3a3a3); }
|
|
126
|
+
|
|
127
|
+
@container (max-width: 560px) {
|
|
128
|
+
.gp__head { flex-wrap: wrap; }
|
|
129
|
+
.gp__actions { width: 100%; }
|
|
130
|
+
.gp__btn { flex: 1; justify-content: center; }
|
|
131
|
+
.gp__stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
132
|
+
}
|
|
133
|
+
</style>
|