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,176 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* ArrivalsBoard — Front-desk list with Arrivals / Departures / In-house tabs: guest, room, time,
|
|
4
|
+
* notes and a one-click check-in / check-out action with progress counter.
|
|
5
|
+
*
|
|
6
|
+
* @param {string} title - Card title. Default 'Today at the front desk'.
|
|
7
|
+
* @param {Array<{guest: string, room: string, time: string, nights?: number, vip?: boolean, note?: string, done?: boolean}>} arrivals - Expected arrivals.
|
|
8
|
+
* @param {Array<{guest: string, room: string, time: string, balance?: string, done?: boolean}>} departures - Expected departures.
|
|
9
|
+
* @param {Array<{guest: string, room: string, until: string}>} inHouse - Current guests.
|
|
10
|
+
* @param {string} variant - Style (card|plain). Default 'card'.
|
|
11
|
+
* @param {string} className - Extra class.
|
|
12
|
+
*
|
|
13
|
+
* Accessibility: tabs follow the ARIA tabs pattern (arrow keys); action buttons say which guest
|
|
14
|
+
* they act on and switch to a pressed "done" state; progress is announced politely.
|
|
15
|
+
*/
|
|
16
|
+
import { initials, hueFrom } from './app-icons';
|
|
17
|
+
|
|
18
|
+
interface Arrival { guest: string; room: string; time: string; nights?: number; vip?: boolean; note?: string; done?: boolean }
|
|
19
|
+
interface Departure { guest: string; room: string; time: string; balance?: string; done?: boolean }
|
|
20
|
+
interface Props {
|
|
21
|
+
title?: string;
|
|
22
|
+
arrivals?: Arrival[];
|
|
23
|
+
departures?: Departure[];
|
|
24
|
+
inHouse?: Array<{ guest: string; room: string; until: string }>;
|
|
25
|
+
variant?: 'card' | 'plain';
|
|
26
|
+
className?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const {
|
|
30
|
+
title = 'Today at the front desk',
|
|
31
|
+
arrivals = [
|
|
32
|
+
{ guest: 'Elena Duarte', room: '204', time: '14:00', nights: 3, vip: true, note: 'Late arrival, extra pillows' },
|
|
33
|
+
{ guest: 'Aiko Tanaka', room: '108', time: '15:30', nights: 4 },
|
|
34
|
+
{ guest: 'The Okafor family', room: '310', time: '16:00', nights: 6, note: 'Baby cot requested', done: true },
|
|
35
|
+
],
|
|
36
|
+
departures = [
|
|
37
|
+
{ guest: 'Hans Müller', room: '112', time: '11:00', balance: '€0', done: true },
|
|
38
|
+
{ guest: 'Priya Nair', room: '207', time: '12:00', balance: '€48' },
|
|
39
|
+
],
|
|
40
|
+
inHouse = [
|
|
41
|
+
{ guest: 'James Whitfield', room: '202', until: 'Fri 7' },
|
|
42
|
+
{ guest: 'Sofía Ramos', room: '305', until: 'Sun 9' },
|
|
43
|
+
],
|
|
44
|
+
variant = 'card',
|
|
45
|
+
className = '',
|
|
46
|
+
} = Astro.props;
|
|
47
|
+
|
|
48
|
+
const tabs = [
|
|
49
|
+
{ id: 'arr', label: 'Arrivals', count: arrivals.length, done: arrivals.filter((a) => a.done).length },
|
|
50
|
+
{ id: 'dep', label: 'Departures', count: departures.length, done: departures.filter((d) => d.done).length },
|
|
51
|
+
{ id: 'inh', label: 'In-house', count: inHouse.length },
|
|
52
|
+
];
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
<section class:list={['fd', `fd--${variant}`, className]} data-frontdesk aria-labelledby="fd-title">
|
|
56
|
+
<header class="fd__head">
|
|
57
|
+
<h2 class="fd__title" id="fd-title">{title}</h2>
|
|
58
|
+
<p class="fd__progress" aria-live="polite" data-progress>{tabs[0].done}/{tabs[0].count} checked in</p>
|
|
59
|
+
</header>
|
|
60
|
+
<div class="fd__tabs" role="tablist" aria-label="Front desk lists">
|
|
61
|
+
{tabs.map((tab, i) => (
|
|
62
|
+
<button type="button" role="tab" id={`fd-tab-${tab.id}`} aria-controls={`fd-panel-${tab.id}`} aria-selected={i === 0 ? 'true' : 'false'} tabindex={i === 0 ? '0' : '-1'} data-tab={tab.id}>
|
|
63
|
+
{tab.label} <span>{tab.count}</span>
|
|
64
|
+
</button>
|
|
65
|
+
))}
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
<ul class="fd__list" role="tabpanel" id="fd-panel-arr" aria-labelledby="fd-tab-arr" data-panel="arr">
|
|
69
|
+
{arrivals.map((a) => (
|
|
70
|
+
<li class="fd__row">
|
|
71
|
+
<span class="fd__avatar" style={`--h: ${hueFrom(a.guest)}`} aria-hidden="true">{initials(a.guest)}</span>
|
|
72
|
+
<span class="fd__who">
|
|
73
|
+
<strong>{a.guest}{a.vip && <em class="fd__vip">VIP</em>}</strong>
|
|
74
|
+
<small>Room {a.room}{a.nights && <> · {a.nights} nights</>}{a.note && <> · {a.note}</>}</small>
|
|
75
|
+
</span>
|
|
76
|
+
<time class="fd__time">{a.time}</time>
|
|
77
|
+
<button type="button" class="fd__action" aria-pressed={a.done ? 'true' : 'false'} data-act="arr" data-done-label="Checked in" data-label="Check in">
|
|
78
|
+
<span>{a.done ? 'Checked in' : 'Check in'}</span>
|
|
79
|
+
<span class="sr-only"> {a.guest}</span>
|
|
80
|
+
</button>
|
|
81
|
+
</li>
|
|
82
|
+
))}
|
|
83
|
+
</ul>
|
|
84
|
+
<ul class="fd__list" role="tabpanel" id="fd-panel-dep" aria-labelledby="fd-tab-dep" data-panel="dep" hidden>
|
|
85
|
+
{departures.map((d) => (
|
|
86
|
+
<li class="fd__row">
|
|
87
|
+
<span class="fd__avatar" style={`--h: ${hueFrom(d.guest)}`} aria-hidden="true">{initials(d.guest)}</span>
|
|
88
|
+
<span class="fd__who"><strong>{d.guest}</strong><small>Room {d.room}{d.balance && <> · Balance {d.balance}</>}</small></span>
|
|
89
|
+
<time class="fd__time">{d.time}</time>
|
|
90
|
+
<button type="button" class="fd__action" aria-pressed={d.done ? 'true' : 'false'} data-act="dep" data-done-label="Checked out" data-label="Check out">
|
|
91
|
+
<span>{d.done ? 'Checked out' : 'Check out'}</span>
|
|
92
|
+
<span class="sr-only"> {d.guest}</span>
|
|
93
|
+
</button>
|
|
94
|
+
</li>
|
|
95
|
+
))}
|
|
96
|
+
</ul>
|
|
97
|
+
<ul class="fd__list" role="tabpanel" id="fd-panel-inh" aria-labelledby="fd-tab-inh" data-panel="inh" hidden>
|
|
98
|
+
{inHouse.map((g) => (
|
|
99
|
+
<li class="fd__row">
|
|
100
|
+
<span class="fd__avatar" style={`--h: ${hueFrom(g.guest)}`} aria-hidden="true">{initials(g.guest)}</span>
|
|
101
|
+
<span class="fd__who"><strong>{g.guest}</strong><small>Room {g.room}</small></span>
|
|
102
|
+
<span class="fd__time">until {g.until}</span>
|
|
103
|
+
</li>
|
|
104
|
+
))}
|
|
105
|
+
</ul>
|
|
106
|
+
</section>
|
|
107
|
+
|
|
108
|
+
<script>
|
|
109
|
+
document.querySelectorAll<HTMLElement>('[data-frontdesk]:not([data-bound])').forEach((fd) => {
|
|
110
|
+
fd.dataset.bound = '';
|
|
111
|
+
const tabs = Array.from(fd.querySelectorAll<HTMLButtonElement>('[role="tab"]'));
|
|
112
|
+
const progress = fd.querySelector<HTMLElement>('[data-progress]')!;
|
|
113
|
+
const words: Record<string, string> = { arr: 'checked in', dep: 'checked out' };
|
|
114
|
+
let current = 'arr';
|
|
115
|
+
const update = () => {
|
|
116
|
+
const panel = fd.querySelector<HTMLElement>(`[data-panel="${current}"]`)!;
|
|
117
|
+
const btns = Array.from(panel.querySelectorAll<HTMLButtonElement>('[data-act]'));
|
|
118
|
+
progress.textContent = btns.length ? `${btns.filter((b) => b.getAttribute('aria-pressed') === 'true').length}/${btns.length} ${words[current]}` : `${panel.children.length} guests in-house`;
|
|
119
|
+
};
|
|
120
|
+
const select = (i: number, focus = false) => {
|
|
121
|
+
tabs.forEach((t, n) => {
|
|
122
|
+
t.setAttribute('aria-selected', String(n === i));
|
|
123
|
+
t.tabIndex = n === i ? 0 : -1;
|
|
124
|
+
fd.querySelector<HTMLElement>(`[data-panel="${t.dataset.tab}"]`)!.hidden = n !== i;
|
|
125
|
+
});
|
|
126
|
+
current = tabs[i].dataset.tab || 'arr';
|
|
127
|
+
if (focus) tabs[i].focus();
|
|
128
|
+
update();
|
|
129
|
+
};
|
|
130
|
+
tabs.forEach((t, i) => {
|
|
131
|
+
t.addEventListener('click', () => select(i));
|
|
132
|
+
t.addEventListener('keydown', (e) => {
|
|
133
|
+
const d = e.key === 'ArrowRight' ? 1 : e.key === 'ArrowLeft' ? -1 : 0;
|
|
134
|
+
if (d) { e.preventDefault(); select((i + d + tabs.length) % tabs.length, true); }
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
fd.querySelectorAll<HTMLButtonElement>('[data-act]').forEach((b) => b.addEventListener('click', () => {
|
|
138
|
+
const done = b.getAttribute('aria-pressed') !== 'true';
|
|
139
|
+
b.setAttribute('aria-pressed', String(done));
|
|
140
|
+
b.firstElementChild!.textContent = done ? b.dataset.doneLabel! : b.dataset.label!;
|
|
141
|
+
update();
|
|
142
|
+
}));
|
|
143
|
+
update();
|
|
144
|
+
});
|
|
145
|
+
</script>
|
|
146
|
+
|
|
147
|
+
<style>
|
|
148
|
+
.fd { container-type: inline-size; display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--sp-3, 0.75rem); min-width: 0; }
|
|
149
|
+
.fd--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); }
|
|
150
|
+
.fd__head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--sp-3, 0.75rem); }
|
|
151
|
+
.fd__title { margin: 0; font-size: 1rem; font-weight: 700; color: var(--c-text-1, #fff); }
|
|
152
|
+
.fd__progress { margin: 0; font-size: 0.78rem; color: var(--c-text-3, #737373); }
|
|
153
|
+
.fd__tabs { display: flex; gap: 4px; padding: 3px; border-radius: 10px; background: rgb(var(--ink-rgb, 255 255 255) / 0.04); }
|
|
154
|
+
.fd__tabs button { flex: 1; min-height: 34px; border: 0; border-radius: 7px; background: transparent; color: var(--c-text-2, #a3a3a3); font: inherit; font-size: 0.8rem; font-weight: 600; cursor: pointer; }
|
|
155
|
+
.fd__tabs button span { padding: 0 6px; border-radius: 999px; background: rgb(var(--ink-rgb, 255 255 255) / 0.08); font-size: 0.7rem; }
|
|
156
|
+
.fd__tabs [aria-selected='true'] { background: var(--c-bg-1, #0a0a0a); color: var(--c-text-1, #fff); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2), 0 0 0 1px var(--c-border, rgba(255, 255, 255, 0.08)); }
|
|
157
|
+
.fd__list { display: grid; margin: 0; padding: 0; list-style: none; }
|
|
158
|
+
.fd__list[hidden] { display: none; }
|
|
159
|
+
.fd__row { display: grid; grid-template-columns: 36px minmax(0, 1fr) auto auto; align-items: center; gap: var(--sp-3, 0.75rem); padding: var(--sp-3, 0.75rem) 0; border-bottom: 1px solid var(--c-border, rgba(255, 255, 255, 0.06)); }
|
|
160
|
+
.fd__row:last-child { border-bottom: 0; }
|
|
161
|
+
.fd__avatar { display: grid; place-items: center; width: 36px; height: 36px; border-radius: 50%; background: hsl(var(--h) 50% 45%); color: #fff; font-size: 0.72rem; font-weight: 700; }
|
|
162
|
+
.fd__who { display: grid; min-width: 0; }
|
|
163
|
+
.fd__who strong { display: flex; align-items: center; gap: 6px; font-size: 0.88rem; color: var(--c-text-1, #fff); }
|
|
164
|
+
.fd__who small { font-size: 0.75rem; color: var(--c-text-3, #737373); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
165
|
+
.fd__vip { padding: 1px 6px; border-radius: 6px; background: color-mix(in srgb, var(--c-warning, #fbbf24) 18%, transparent); color: var(--c-warning, #fbbf24); font-size: 0.62rem; font-style: normal; font-weight: 800; letter-spacing: 0.06em; }
|
|
166
|
+
.fd__time { font-size: 0.8rem; color: var(--c-text-2, #a3a3a3); font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
167
|
+
.fd__action { min-height: 34px; padding: 0 0.8rem; border-radius: 8px; border: 1px solid var(--app-accent, var(--c-primary-light, #d4a017)); background: transparent; color: var(--app-accent, var(--c-primary-light, #d4a017)); font: inherit; font-size: 0.78rem; font-weight: 700; white-space: nowrap; cursor: pointer; }
|
|
168
|
+
.fd__action:hover { background: color-mix(in srgb, var(--app-accent, var(--c-primary-light, #d4a017)) 12%, transparent); }
|
|
169
|
+
.fd__action[aria-pressed='true'] { border-color: transparent; background: color-mix(in srgb, var(--c-success, #34d399) 14%, transparent); color: var(--c-success, #34d399); }
|
|
170
|
+
.fd :is(button):focus-visible { outline: 2px solid var(--app-accent, var(--c-primary-light, #d4a017)); outline-offset: 2px; }
|
|
171
|
+
.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
|
|
172
|
+
@container (max-width: 420px) {
|
|
173
|
+
.fd__row { grid-template-columns: 36px minmax(0, 1fr) auto; }
|
|
174
|
+
.fd__time { display: none; }
|
|
175
|
+
}
|
|
176
|
+
</style>
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* AuthCard — Sign-in / sign-up split screen: form card (email, password with show/hide, remember
|
|
4
|
+
* me, SSO buttons) next to a brand panel with a quote and key figures.
|
|
5
|
+
*
|
|
6
|
+
* @param {string} mode - 'signin' or 'signup'. Default 'signin'.
|
|
7
|
+
* @param {string} brand - Product name. Default 'Lobby'.
|
|
8
|
+
* @param {string} title - Heading. Defaults by mode.
|
|
9
|
+
* @param {string} subtitle - Supporting text.
|
|
10
|
+
* @param {string} action - Form action. With '#' it validates and redirects to `successHref`. Default '#'.
|
|
11
|
+
* @param {string} successHref - Where to go after a demo sign-in. Default '#'.
|
|
12
|
+
* @param {string[]} providers - SSO buttons. Default ['Google', 'Microsoft'].
|
|
13
|
+
* @param {string} switchHref - Link to the other mode. Default '#'.
|
|
14
|
+
* @param {{quote: string, author: string, role?: string}} testimonial - Brand panel quote (optional).
|
|
15
|
+
* @param {Array<{value: string, label: string}>} stats - Brand panel figures.
|
|
16
|
+
* @param {string} variant - Layout (split|centered). Default 'split'.
|
|
17
|
+
* @param {string} className - Extra class.
|
|
18
|
+
*
|
|
19
|
+
* Accessibility: one h1; labelled fields with autocomplete (email, current-password /
|
|
20
|
+
* new-password); errors are linked with aria-describedby and announced; the show-password
|
|
21
|
+
* button is a toggle with aria-pressed.
|
|
22
|
+
*/
|
|
23
|
+
interface Props {
|
|
24
|
+
mode?: 'signin' | 'signup';
|
|
25
|
+
brand?: string;
|
|
26
|
+
title?: string;
|
|
27
|
+
subtitle?: string;
|
|
28
|
+
action?: string;
|
|
29
|
+
successHref?: string;
|
|
30
|
+
providers?: string[];
|
|
31
|
+
switchHref?: string;
|
|
32
|
+
testimonial?: { quote: string; author: string; role?: string };
|
|
33
|
+
stats?: Array<{ value: string; label: string }>;
|
|
34
|
+
variant?: 'split' | 'centered';
|
|
35
|
+
className?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const {
|
|
39
|
+
mode = 'signin',
|
|
40
|
+
brand = 'Lobby',
|
|
41
|
+
title = mode === 'signin' ? 'Welcome back' : 'Create your workspace',
|
|
42
|
+
subtitle = mode === 'signin' ? 'Sign in to manage your property.' : 'Start your 14-day free trial — no card required.',
|
|
43
|
+
action = '#',
|
|
44
|
+
successHref = '#',
|
|
45
|
+
providers = ['Google', 'Microsoft'],
|
|
46
|
+
switchHref = '#',
|
|
47
|
+
testimonial = { quote: 'We replaced three tools with Lobby. Check-in takes half the time and our occupancy is up 9% this season.', author: 'Lucía Ortega', role: 'GM, Hotel Mirador' },
|
|
48
|
+
stats = [{ value: '2,400+', label: 'properties' }, { value: '38%', label: 'less admin time' }, { value: '4.9/5', label: 'support rating' }],
|
|
49
|
+
variant = 'split',
|
|
50
|
+
className = '',
|
|
51
|
+
} = Astro.props;
|
|
52
|
+
|
|
53
|
+
const isIn = mode === 'signin';
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
<div class="auth-cq">
|
|
57
|
+
<section class:list={['auth', `auth--${variant}`, className]}>
|
|
58
|
+
<div class="auth__form-side">
|
|
59
|
+
<form class="auth__card" action={action} method="post" novalidate data-auth data-success={successHref}>
|
|
60
|
+
<p class="auth__brand"><span aria-hidden="true">{brand.slice(0, 1)}</span>{brand}</p>
|
|
61
|
+
<h1 class="auth__title">{title}</h1>
|
|
62
|
+
{subtitle && <p class="auth__sub">{subtitle}</p>}
|
|
63
|
+
|
|
64
|
+
{providers.length > 0 && (
|
|
65
|
+
<>
|
|
66
|
+
<div class="auth__sso">
|
|
67
|
+
{providers.map((p) => <button type="button" class="auth__sso-btn">Continue with {p}</button>)}
|
|
68
|
+
</div>
|
|
69
|
+
<p class="auth__or"><span>or with email</span></p>
|
|
70
|
+
</>
|
|
71
|
+
)}
|
|
72
|
+
|
|
73
|
+
{!isIn && (
|
|
74
|
+
<div class="auth__field">
|
|
75
|
+
<label for="auth-name">Full name</label>
|
|
76
|
+
<input id="auth-name" name="name" type="text" autocomplete="name" required />
|
|
77
|
+
</div>
|
|
78
|
+
)}
|
|
79
|
+
<div class="auth__field">
|
|
80
|
+
<label for="auth-email">Work email</label>
|
|
81
|
+
<input id="auth-email" name="email" type="email" autocomplete="email" required aria-describedby="auth-error" />
|
|
82
|
+
</div>
|
|
83
|
+
<div class="auth__field">
|
|
84
|
+
<div class="auth__label-row">
|
|
85
|
+
<label for="auth-pass">Password</label>
|
|
86
|
+
{isIn && <a href="#">Forgot password?</a>}
|
|
87
|
+
</div>
|
|
88
|
+
<div class="auth__pass">
|
|
89
|
+
<input id="auth-pass" name="password" type="password" autocomplete={isIn ? 'current-password' : 'new-password'} required minlength={8} aria-describedby="auth-error" />
|
|
90
|
+
<button type="button" class="auth__eye" aria-pressed="false" aria-label="Show password" data-eye>
|
|
91
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7S2 12 2 12z"/><circle cx="12" cy="12" r="3"/></svg>
|
|
92
|
+
</button>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
{isIn && (
|
|
96
|
+
<label class="auth__remember"><input type="checkbox" name="remember" checked /> Keep me signed in</label>
|
|
97
|
+
)}
|
|
98
|
+
<p class="auth__error" id="auth-error" role="alert" data-error></p>
|
|
99
|
+
<button type="submit" class="auth__submit" data-submit>{isIn ? 'Sign in' : 'Create account'}</button>
|
|
100
|
+
<p class="auth__switch">{isIn ? 'New to ' + brand + '?' : 'Already have an account?'} <a href={switchHref}>{isIn ? 'Start a free trial' : 'Sign in'}</a></p>
|
|
101
|
+
</form>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
{variant === 'split' && (
|
|
105
|
+
<aside class="auth__brand-side" aria-label={`About ${brand}`}>
|
|
106
|
+
{testimonial && (
|
|
107
|
+
<figure class="auth__quote">
|
|
108
|
+
<blockquote><p>“{testimonial.quote}”</p></blockquote>
|
|
109
|
+
<figcaption><strong>{testimonial.author}</strong>{testimonial.role && <span> · {testimonial.role}</span>}</figcaption>
|
|
110
|
+
</figure>
|
|
111
|
+
)}
|
|
112
|
+
{stats.length > 0 && (
|
|
113
|
+
<dl class="auth__stats">{stats.map((s) => <div><dt>{s.label}</dt><dd>{s.value}</dd></div>)}</dl>
|
|
114
|
+
)}
|
|
115
|
+
</aside>
|
|
116
|
+
)}
|
|
117
|
+
</section>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
<script>
|
|
121
|
+
document.querySelectorAll<HTMLFormElement>('form[data-auth]:not([data-bound])').forEach((form) => {
|
|
122
|
+
form.dataset.bound = '';
|
|
123
|
+
const eye = form.querySelector<HTMLButtonElement>('[data-eye]');
|
|
124
|
+
const pass = form.querySelector<HTMLInputElement>('#auth-pass');
|
|
125
|
+
const error = form.querySelector<HTMLElement>('[data-error]')!;
|
|
126
|
+
eye?.addEventListener('click', () => {
|
|
127
|
+
const show = eye.getAttribute('aria-pressed') !== 'true';
|
|
128
|
+
eye.setAttribute('aria-pressed', String(show));
|
|
129
|
+
eye.setAttribute('aria-label', show ? 'Hide password' : 'Show password');
|
|
130
|
+
if (pass) pass.type = show ? 'text' : 'password';
|
|
131
|
+
});
|
|
132
|
+
form.addEventListener('submit', (e) => {
|
|
133
|
+
form.querySelectorAll('[aria-invalid]').forEach((n) => n.removeAttribute('aria-invalid'));
|
|
134
|
+
const bad = Array.from(form.querySelectorAll<HTMLInputElement>('input[required]')).filter((i) => !i.checkValidity());
|
|
135
|
+
if (bad.length) {
|
|
136
|
+
e.preventDefault();
|
|
137
|
+
bad.forEach((i) => i.setAttribute('aria-invalid', 'true'));
|
|
138
|
+
error.textContent = bad[0].type === 'password' ? 'Your password needs at least 8 characters.' : 'Please check your email address.';
|
|
139
|
+
bad[0].focus();
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (form.getAttribute('action') === '#') {
|
|
143
|
+
e.preventDefault();
|
|
144
|
+
const btn = form.querySelector<HTMLButtonElement>('[data-submit]')!;
|
|
145
|
+
btn.disabled = true;
|
|
146
|
+
btn.textContent = 'Signing in…';
|
|
147
|
+
window.setTimeout(() => { window.location.href = form.dataset.success || '#'; }, 700);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
</script>
|
|
152
|
+
|
|
153
|
+
<style>
|
|
154
|
+
.auth-cq { container-type: inline-size; }
|
|
155
|
+
.auth { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); min-height: min(100dvh, 860px); color: var(--c-text-2, #a3a3a3); }
|
|
156
|
+
.auth--centered { grid-template-columns: 1fr; }
|
|
157
|
+
.auth__form-side { display: grid; place-items: center; padding: clamp(1.5rem, 5vw, 4rem) var(--sp-5, 1.25rem); }
|
|
158
|
+
.auth__card { display: grid; gap: var(--sp-4, 1rem); width: 100%; max-width: 400px; }
|
|
159
|
+
.auth__brand { display: flex; align-items: center; gap: var(--sp-2, 0.5rem); margin: 0 0 var(--sp-4, 1rem); font-weight: 800; font-size: 1.05rem; color: var(--c-text-1, #fff); }
|
|
160
|
+
.auth__brand span { display: grid; place-items: center; width: 32px; height: 32px; border-radius: 9px; background: var(--app-accent, var(--c-primary-light, #d4a017)); color: var(--c-on-primary, #050505); }
|
|
161
|
+
.auth__title { margin: 0; font-size: 1.8rem; font-weight: 800; letter-spacing: -0.03em; color: var(--c-text-1, #fff); }
|
|
162
|
+
.auth__sub { margin: -8px 0 0; font-size: 0.92rem; }
|
|
163
|
+
.auth__sso { display: grid; gap: var(--sp-2, 0.5rem); }
|
|
164
|
+
.auth__sso-btn { min-height: 44px; border-radius: 10px; border: 1px solid var(--c-border, rgba(255, 255, 255, 0.12)); background: var(--c-bg-1, #0a0a0a); color: var(--c-text-1, #fff); font: inherit; font-size: 0.88rem; font-weight: 600; cursor: pointer; }
|
|
165
|
+
.auth__sso-btn:hover { background: rgb(var(--ink-rgb, 255 255 255) / 0.05); }
|
|
166
|
+
.auth__or { display: flex; align-items: center; gap: var(--sp-3, 0.75rem); margin: 0; font-size: 0.75rem; color: var(--c-text-3, #737373); }
|
|
167
|
+
.auth__or::before, .auth__or::after { content: ''; flex: 1; height: 1px; background: var(--c-border, rgba(255, 255, 255, 0.1)); }
|
|
168
|
+
.auth__field { display: grid; gap: 6px; }
|
|
169
|
+
.auth__field label, .auth__label-row label { font-size: 0.82rem; font-weight: 600; color: var(--c-text-1, #fff); }
|
|
170
|
+
.auth__label-row { display: flex; justify-content: space-between; align-items: baseline; }
|
|
171
|
+
.auth__label-row a, .auth__switch a { font-size: 0.8rem; font-weight: 600; color: var(--app-accent, var(--c-primary-light, #d4a017)); }
|
|
172
|
+
.auth__field input { width: 100%; min-height: 46px; padding: 0 0.85rem; border-radius: 10px; border: 1px solid var(--c-border, rgba(255, 255, 255, 0.14)); background: var(--c-bg-1, #0a0a0a); color: var(--c-text-1, #fff); font: inherit; font-size: 0.95rem; }
|
|
173
|
+
.auth__field input:focus { outline: none; border-color: var(--app-accent, var(--c-primary-light, #d4a017)); box-shadow: 0 0 0 3px color-mix(in srgb, var(--app-accent, var(--c-primary-light, #d4a017)) 20%, transparent); }
|
|
174
|
+
.auth__field input[aria-invalid='true'] { border-color: var(--c-danger, #f87171); }
|
|
175
|
+
.auth__pass { position: relative; }
|
|
176
|
+
.auth__pass input { padding-right: 44px; }
|
|
177
|
+
.auth__eye { position: absolute; right: 4px; top: 50%; transform: translateY(-50%); display: grid; place-items: center; width: 38px; height: 38px; border: 0; border-radius: 8px; background: transparent; color: var(--c-text-3, #737373); cursor: pointer; }
|
|
178
|
+
.auth__eye[aria-pressed='true'] { color: var(--app-accent, var(--c-primary-light, #d4a017)); }
|
|
179
|
+
.auth__remember { display: flex; align-items: center; gap: var(--sp-2, 0.5rem); font-size: 0.85rem; cursor: pointer; }
|
|
180
|
+
.auth__remember input { width: 16px; height: 16px; accent-color: var(--app-accent, var(--c-primary-light, #d4a017)); }
|
|
181
|
+
.auth__error { min-height: 1.2em; margin: -6px 0 0; font-size: 0.82rem; color: var(--c-danger, #f87171); }
|
|
182
|
+
.auth__error:empty { display: none; }
|
|
183
|
+
.auth__submit { min-height: 48px; border: 0; border-radius: 10px; background: var(--app-accent, var(--c-primary-light, #d4a017)); color: var(--c-on-primary, #050505); font: inherit; font-size: 0.95rem; font-weight: 700; cursor: pointer; }
|
|
184
|
+
.auth__submit:disabled { opacity: 0.7; cursor: progress; }
|
|
185
|
+
.auth__switch { margin: 0; font-size: 0.85rem; text-align: center; }
|
|
186
|
+
.auth :is(a, button, input):focus-visible { outline: 2px solid var(--app-accent, var(--c-primary-light, #d4a017)); outline-offset: 2px; }
|
|
187
|
+
|
|
188
|
+
.auth__brand-side {
|
|
189
|
+
display: flex;
|
|
190
|
+
flex-direction: column;
|
|
191
|
+
justify-content: flex-end;
|
|
192
|
+
gap: var(--sp-8, 2rem);
|
|
193
|
+
padding: clamp(2rem, 5vw, 4rem);
|
|
194
|
+
background:
|
|
195
|
+
radial-gradient(circle at 20% 20%, color-mix(in srgb, var(--app-accent, var(--c-primary-light, #d4a017)) 45%, transparent), transparent 55%),
|
|
196
|
+
radial-gradient(circle at 90% 80%, color-mix(in srgb, var(--chart-3, #199e70) 35%, transparent), transparent 50%),
|
|
197
|
+
#0b1020;
|
|
198
|
+
color: #fff;
|
|
199
|
+
}
|
|
200
|
+
.auth__quote { margin: 0; }
|
|
201
|
+
.auth__quote p { margin: 0; font-size: clamp(1.3rem, 2.4vw, 1.9rem); font-weight: 600; line-height: 1.35; letter-spacing: -0.02em; }
|
|
202
|
+
.auth__quote figcaption { margin-top: var(--sp-4, 1rem); font-size: 0.9rem; color: rgba(255, 255, 255, 0.75); }
|
|
203
|
+
.auth__quote strong { color: #fff; }
|
|
204
|
+
.auth__stats { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--sp-4, 1rem); margin: 0; padding-top: var(--sp-6, 1.5rem); border-top: 1px solid rgba(255, 255, 255, 0.2); }
|
|
205
|
+
.auth__stats div { display: flex; flex-direction: column-reverse; }
|
|
206
|
+
.auth__stats dt { font-size: 0.8rem; color: rgba(255, 255, 255, 0.75); }
|
|
207
|
+
.auth__stats dd { margin: 0; font-size: 1.5rem; font-weight: 800; }
|
|
208
|
+
|
|
209
|
+
@container (max-width: 860px) {
|
|
210
|
+
.auth { grid-template-columns: 1fr; }
|
|
211
|
+
.auth__brand-side { display: none; }
|
|
212
|
+
}
|
|
213
|
+
</style>
|