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,207 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* RoomRack — Booking timeline ("tape chart"): rooms as rows, days as columns and reservations as
|
|
4
|
+
* coloured bars by status, with a today marker, room-type groups and a detail panel on click.
|
|
5
|
+
*
|
|
6
|
+
* @param {string[]} days - Column labels, e.g. ['Mon 3', 'Tue 4', …].
|
|
7
|
+
* @param {number} today - Index of today's column (-1 hides the marker). Default 1.
|
|
8
|
+
* @param {Array<{id: string, name: string, type?: string}>} rooms - Rows.
|
|
9
|
+
* @param {Array<{room: string, guest: string, start: number, nights: number, status: 'confirmed'|'checked-in'|'pending'|'blocked', ref?: string, channel?: string}>} bookings - Bars; `start` is a day index.
|
|
10
|
+
* @param {string} title - Card title. Default 'Room rack'.
|
|
11
|
+
* @param {number} dayWidth - Column width in px. Default 64.
|
|
12
|
+
* @param {string} variant - Style (card|plain). Default 'card'.
|
|
13
|
+
* @param {string} className - Extra class.
|
|
14
|
+
*
|
|
15
|
+
* Accessibility: each booking is a button labelled with guest, room, dates and status; the detail
|
|
16
|
+
* panel is a polite live region; the grid scrolls horizontally with the keyboard.
|
|
17
|
+
*/
|
|
18
|
+
interface Booking { room: string; guest: string; start: number; nights: number; status: 'confirmed' | 'checked-in' | 'pending' | 'blocked'; ref?: string; channel?: string }
|
|
19
|
+
interface Props {
|
|
20
|
+
days?: string[];
|
|
21
|
+
today?: number;
|
|
22
|
+
rooms?: Array<{ id: string; name: string; type?: string }>;
|
|
23
|
+
bookings?: Booking[];
|
|
24
|
+
title?: string;
|
|
25
|
+
dayWidth?: number;
|
|
26
|
+
variant?: 'card' | 'plain';
|
|
27
|
+
className?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const {
|
|
31
|
+
days = ['Mon 3', 'Tue 4', 'Wed 5', 'Thu 6', 'Fri 7', 'Sat 8', 'Sun 9', 'Mon 10', 'Tue 11', 'Wed 12'],
|
|
32
|
+
today = 1,
|
|
33
|
+
rooms = [
|
|
34
|
+
{ id: '101', name: '101', type: 'Standard' },
|
|
35
|
+
{ id: '102', name: '102', type: 'Standard' },
|
|
36
|
+
{ id: '201', name: '201', type: 'Deluxe' },
|
|
37
|
+
{ id: '202', name: '202', type: 'Deluxe' },
|
|
38
|
+
{ id: '301', name: '301', type: 'Suite' },
|
|
39
|
+
],
|
|
40
|
+
bookings = [
|
|
41
|
+
{ room: '101', guest: 'Elena Duarte', start: 0, nights: 3, status: 'checked-in', ref: 'MR-2830', channel: 'Booking.com' },
|
|
42
|
+
{ room: '101', guest: 'Tom Becker', start: 4, nights: 2, status: 'confirmed', ref: 'MR-2852' },
|
|
43
|
+
{ room: '102', guest: 'Aiko Tanaka', start: 1, nights: 5, status: 'confirmed', ref: 'MR-2839', channel: 'Expedia' },
|
|
44
|
+
{ room: '201', guest: 'Maintenance', start: 2, nights: 2, status: 'blocked' },
|
|
45
|
+
{ room: '202', guest: 'James Whitfield', start: 0, nights: 2, status: 'checked-in', ref: 'MR-2840' },
|
|
46
|
+
{ room: '202', guest: 'Sofía Ramos', start: 3, nights: 4, status: 'pending', ref: 'MR-2861' },
|
|
47
|
+
{ room: '301', guest: 'The Okafor family', start: 1, nights: 6, status: 'confirmed', ref: 'MR-2844', channel: 'Direct' },
|
|
48
|
+
],
|
|
49
|
+
title = 'Room rack',
|
|
50
|
+
dayWidth = 64,
|
|
51
|
+
variant = 'card',
|
|
52
|
+
className = '',
|
|
53
|
+
} = Astro.props;
|
|
54
|
+
|
|
55
|
+
const labels: Record<Booking['status'], string> = { confirmed: 'Confirmed', 'checked-in': 'Checked in', pending: 'Pending', blocked: 'Blocked' };
|
|
56
|
+
const groups = Array.from(new Set(rooms.map((r) => r.type ?? ''))).map((type) => ({ type, rooms: rooms.filter((r) => (r.type ?? '') === type) }));
|
|
57
|
+
const occupied = days.map((_, d) => bookings.filter((b) => b.status !== 'blocked' && d >= b.start && d < b.start + b.nights).length);
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
<section class:list={['rack', `rack--${variant}`, className]} style={`--day-w: ${dayWidth}px; --days: ${days.length}`} data-rack aria-labelledby="rack-title">
|
|
61
|
+
<header class="rack__head">
|
|
62
|
+
<h2 class="rack__title" id="rack-title">{title}</h2>
|
|
63
|
+
<ul class="rack__legend" aria-label="Legend">
|
|
64
|
+
{(Object.keys(labels) as Booking['status'][]).map((s) => <li><i class={`rack__swatch rack__swatch--${s}`} aria-hidden="true"></i>{labels[s]}</li>)}
|
|
65
|
+
</ul>
|
|
66
|
+
</header>
|
|
67
|
+
|
|
68
|
+
<div class="rack__scroll" tabindex="0" role="region" aria-label={`${title}, scroll horizontally`}>
|
|
69
|
+
<div class="rack__grid">
|
|
70
|
+
<div class="rack__corner">Room</div>
|
|
71
|
+
{days.map((d, i) => (
|
|
72
|
+
<div class:list={['rack__day', { 'is-today': i === today, 'is-weekend': /^(Sat|Sun|Sáb|Dom)/i.test(d) }]}>
|
|
73
|
+
<span>{d}</span>
|
|
74
|
+
<small>{Math.round((occupied[i] / Math.max(1, rooms.length)) * 100)}%</small>
|
|
75
|
+
</div>
|
|
76
|
+
))}
|
|
77
|
+
|
|
78
|
+
{groups.map((g) => (
|
|
79
|
+
<>
|
|
80
|
+
{g.type && <div class="rack__group" style={`grid-column: 1 / span ${days.length + 1}`}>{g.type} <span>{g.rooms.length}</span></div>}
|
|
81
|
+
{g.rooms.map((room) => (
|
|
82
|
+
<>
|
|
83
|
+
<div class="rack__room">{room.name}</div>
|
|
84
|
+
<div class="rack__lane" style={`grid-column: 2 / span ${days.length}`}>
|
|
85
|
+
{today >= 0 && <span class="rack__now" style={`left: calc(${today} * var(--day-w) + var(--day-w) / 2)`} aria-hidden="true"></span>}
|
|
86
|
+
{bookings.filter((b) => b.room === room.id).map((b) => (
|
|
87
|
+
<button
|
|
88
|
+
type="button"
|
|
89
|
+
class:list={['rack__bar', `rack__bar--${b.status}`]}
|
|
90
|
+
style={`left: calc(${b.start} * var(--day-w) + 3px); width: calc(${Math.min(b.nights, days.length - b.start)} * var(--day-w) - 6px)`}
|
|
91
|
+
aria-label={`${b.guest}, room ${room.name}, ${days[b.start]} for ${b.nights} ${b.nights === 1 ? 'night' : 'nights'}, ${labels[b.status]}`}
|
|
92
|
+
data-detail={JSON.stringify({ guest: b.guest, room: room.name, type: room.type, from: days[b.start], nights: b.nights, status: labels[b.status], ref: b.ref, channel: b.channel })}
|
|
93
|
+
>
|
|
94
|
+
<span>{b.guest}</span>
|
|
95
|
+
</button>
|
|
96
|
+
))}
|
|
97
|
+
</div>
|
|
98
|
+
</>
|
|
99
|
+
))}
|
|
100
|
+
</>
|
|
101
|
+
))}
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<div class="rack__detail" aria-live="polite" data-detail-out>
|
|
106
|
+
<p class="rack__hint">Select a booking to see its details.</p>
|
|
107
|
+
</div>
|
|
108
|
+
</section>
|
|
109
|
+
|
|
110
|
+
<script>
|
|
111
|
+
document.querySelectorAll<HTMLElement>('[data-rack]:not([data-bound])').forEach((rack) => {
|
|
112
|
+
rack.dataset.bound = '';
|
|
113
|
+
const out = rack.querySelector<HTMLElement>('[data-detail-out]')!;
|
|
114
|
+
const bars = Array.from(rack.querySelectorAll<HTMLButtonElement>('.rack__bar'));
|
|
115
|
+
bars.forEach((bar) => bar.addEventListener('click', () => {
|
|
116
|
+
bars.forEach((b) => b.removeAttribute('aria-pressed'));
|
|
117
|
+
bar.setAttribute('aria-pressed', 'true');
|
|
118
|
+
const d = JSON.parse(bar.dataset.detail || '{}');
|
|
119
|
+
const dl = document.createElement('dl');
|
|
120
|
+
dl.className = 'rack__dl';
|
|
121
|
+
const rows: Array<[string, string | undefined]> = [
|
|
122
|
+
['Guest', d.guest], ['Room', `${d.room}${d.type ? ` · ${d.type}` : ''}`], ['Arrival', d.from],
|
|
123
|
+
['Nights', String(d.nights)], ['Status', d.status], ['Booking', d.ref], ['Channel', d.channel],
|
|
124
|
+
];
|
|
125
|
+
rows.filter(([, v]) => v).forEach(([k, v]) => {
|
|
126
|
+
const wrap = document.createElement('div');
|
|
127
|
+
const dt = document.createElement('dt'); dt.textContent = k;
|
|
128
|
+
const dd = document.createElement('dd'); dd.textContent = v!;
|
|
129
|
+
wrap.append(dt, dd);
|
|
130
|
+
dl.append(wrap);
|
|
131
|
+
});
|
|
132
|
+
out.replaceChildren(dl);
|
|
133
|
+
}));
|
|
134
|
+
});
|
|
135
|
+
</script>
|
|
136
|
+
|
|
137
|
+
<style>
|
|
138
|
+
.rack { container-type: inline-size; display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--sp-3, 0.75rem); min-width: 0; }
|
|
139
|
+
.rack--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); }
|
|
140
|
+
.rack__head { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--sp-3, 0.75rem); }
|
|
141
|
+
.rack__title { margin: 0; font-size: 1rem; font-weight: 700; color: var(--c-text-1, #fff); }
|
|
142
|
+
.rack__legend { display: flex; flex-wrap: wrap; gap: var(--sp-3, 0.75rem); margin: 0; padding: 0; list-style: none; font-size: 0.75rem; color: var(--c-text-2, #a3a3a3); }
|
|
143
|
+
.rack__legend li { display: inline-flex; align-items: center; gap: 6px; }
|
|
144
|
+
.rack__swatch { width: 10px; height: 10px; border-radius: 3px; }
|
|
145
|
+
|
|
146
|
+
.rack__scroll { overflow-x: auto; border-radius: 10px; border: 1px solid var(--c-border, rgba(255, 255, 255, 0.08)); }
|
|
147
|
+
.rack__grid { display: grid; grid-template-columns: 80px repeat(var(--days), var(--day-w)); min-width: max-content; }
|
|
148
|
+
.rack__corner, .rack__day { position: sticky; top: 0; z-index: 2; display: grid; align-content: center; height: 48px; padding: 0 8px; background: var(--c-bg-1, #0a0a0a); border-bottom: 1px solid var(--c-border, rgba(255, 255, 255, 0.08)); font-size: 0.72rem; color: var(--c-text-3, #737373); }
|
|
149
|
+
.rack__corner { left: 0; z-index: 3; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; }
|
|
150
|
+
.rack__day { text-align: center; border-left: 1px solid var(--c-border, rgba(255, 255, 255, 0.05)); }
|
|
151
|
+
.rack__day span { font-weight: 700; color: var(--c-text-2, #a3a3a3); }
|
|
152
|
+
.rack__day small { font-size: 0.66rem; }
|
|
153
|
+
.rack__day.is-weekend { background: color-mix(in srgb, var(--c-bg-1, #0a0a0a) 92%, var(--c-text-1, #fff)); }
|
|
154
|
+
.rack__day.is-today span { color: var(--app-accent, var(--c-primary-light, #d4a017)); }
|
|
155
|
+
.rack__group { padding: 6px 10px; background: rgb(var(--ink-rgb, 255 255 255) / 0.03); border-bottom: 1px solid var(--c-border, rgba(255, 255, 255, 0.06)); font-size: 0.7rem; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; color: var(--c-text-2, #a3a3a3); }
|
|
156
|
+
.rack__group span { color: var(--c-text-3, #737373); }
|
|
157
|
+
.rack__room { position: sticky; left: 0; z-index: 1; display: flex; align-items: center; height: 48px; padding: 0 12px; background: var(--c-bg-1, #0a0a0a); border-bottom: 1px solid var(--c-border, rgba(255, 255, 255, 0.06)); border-right: 1px solid var(--c-border, rgba(255, 255, 255, 0.08)); font-size: 0.85rem; font-weight: 700; color: var(--c-text-1, #fff); font-variant-numeric: tabular-nums; }
|
|
158
|
+
.rack__lane {
|
|
159
|
+
position: relative;
|
|
160
|
+
height: 48px;
|
|
161
|
+
border-bottom: 1px solid var(--c-border, rgba(255, 255, 255, 0.06));
|
|
162
|
+
background-image: linear-gradient(to right, var(--c-border, rgba(255, 255, 255, 0.05)) 1px, transparent 1px);
|
|
163
|
+
background-size: var(--day-w) 100%;
|
|
164
|
+
}
|
|
165
|
+
.rack__now { position: absolute; top: 0; bottom: 0; width: 2px; margin-left: -1px; background: var(--app-accent, var(--c-primary-light, #d4a017)); opacity: 0.6; z-index: 1; }
|
|
166
|
+
|
|
167
|
+
.rack__bar {
|
|
168
|
+
--tone: var(--app-accent, var(--c-primary-light, #d4a017));
|
|
169
|
+
position: absolute;
|
|
170
|
+
top: 8px;
|
|
171
|
+
bottom: 8px;
|
|
172
|
+
z-index: 2;
|
|
173
|
+
display: flex;
|
|
174
|
+
align-items: center;
|
|
175
|
+
padding: 0 10px;
|
|
176
|
+
border: 0;
|
|
177
|
+
border-left: 3px solid var(--tone);
|
|
178
|
+
border-radius: 7px;
|
|
179
|
+
background: color-mix(in srgb, var(--tone) 22%, var(--c-bg-1, #0a0a0a));
|
|
180
|
+
color: var(--c-text-1, #fff);
|
|
181
|
+
font: inherit;
|
|
182
|
+
font-size: 0.75rem;
|
|
183
|
+
font-weight: 600;
|
|
184
|
+
text-align: left;
|
|
185
|
+
overflow: hidden;
|
|
186
|
+
cursor: pointer;
|
|
187
|
+
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
|
188
|
+
}
|
|
189
|
+
.rack__bar span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
190
|
+
.rack__bar:hover, .rack__bar[aria-pressed='true'] { transform: translateY(-1px); box-shadow: 0 6px 16px -6px color-mix(in srgb, var(--tone) 60%, transparent); }
|
|
191
|
+
.rack__bar[aria-pressed='true'] { outline: 2px solid var(--tone); outline-offset: 1px; }
|
|
192
|
+
.rack__bar:focus-visible { outline: 2px solid var(--c-text-1, #fff); outline-offset: 2px; }
|
|
193
|
+
.rack__bar--checked-in, .rack__swatch--checked-in { --tone: var(--c-success, #34d399); }
|
|
194
|
+
.rack__bar--pending, .rack__swatch--pending { --tone: var(--c-warning, #fbbf24); }
|
|
195
|
+
.rack__bar--blocked, .rack__swatch--blocked { --tone: var(--c-text-3, #737373); }
|
|
196
|
+
.rack__bar--blocked { background: repeating-linear-gradient(135deg, rgb(var(--ink-rgb, 255 255 255) / 0.06) 0 6px, transparent 6px 12px); color: var(--c-text-2, #a3a3a3); }
|
|
197
|
+
.rack__swatch { background: var(--tone, var(--app-accent, var(--c-primary-light, #d4a017))); }
|
|
198
|
+
.rack__swatch--confirmed { --tone: var(--app-accent, var(--c-primary-light, #d4a017)); }
|
|
199
|
+
.rack__scroll:focus-visible { outline: 2px solid var(--app-accent, var(--c-primary-light, #d4a017)); outline-offset: 2px; }
|
|
200
|
+
|
|
201
|
+
.rack__detail { min-height: 44px; padding: var(--sp-3, 0.75rem) var(--sp-4, 1rem); border-radius: 10px; background: rgb(var(--ink-rgb, 255 255 255) / 0.03); }
|
|
202
|
+
.rack__hint { margin: 0; font-size: 0.82rem; color: var(--c-text-3, #737373); }
|
|
203
|
+
.rack__detail :global(.rack__dl) { display: flex; flex-wrap: wrap; gap: var(--sp-2, 0.5rem) var(--sp-6, 1.5rem); margin: 0; }
|
|
204
|
+
.rack__detail :global(.rack__dl dt) { font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.06em; color: var(--c-text-3, #737373); }
|
|
205
|
+
.rack__detail :global(.rack__dl dd) { margin: 0; font-size: 0.88rem; font-weight: 600; color: var(--c-text-1, #fff); }
|
|
206
|
+
@media (prefers-reduced-motion: reduce) { .rack__bar { transition: none; } }
|
|
207
|
+
</style>
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* SettingsSection — Settings page block: title and description on the left, fields in a card on
|
|
4
|
+
* the right, with an optional footer holding Save / Cancel and a "saved" confirmation.
|
|
5
|
+
*
|
|
6
|
+
* @param {string} title - Section title. Default 'Profile'.
|
|
7
|
+
* @param {string} description - Help text under the title.
|
|
8
|
+
* @param {boolean} form - Wrap the fields in a <form> with Save/Cancel. Default true.
|
|
9
|
+
* @param {string} saveLabel - Save button label. Default 'Save changes'.
|
|
10
|
+
* @param {string} savedText - Confirmation after saving. Default 'Changes saved'.
|
|
11
|
+
* @param {string} id - Base id. Default 'settings-section'.
|
|
12
|
+
* @param {string} variant - Layout (split|stacked). Default 'split'.
|
|
13
|
+
* @param {string} className - Extra class.
|
|
14
|
+
*
|
|
15
|
+
* Slots: default (fields — SwitchRow, AnimatedInput, ModernSelect…), `footer-start` (extra text).
|
|
16
|
+
*
|
|
17
|
+
* Accessibility: a labelled section; the form validates natively and announces the saved state
|
|
18
|
+
* in a polite live region. Emits `app:settings-save` with the FormData.
|
|
19
|
+
*/
|
|
20
|
+
interface Props {
|
|
21
|
+
title?: string;
|
|
22
|
+
description?: string;
|
|
23
|
+
form?: boolean;
|
|
24
|
+
saveLabel?: string;
|
|
25
|
+
savedText?: string;
|
|
26
|
+
id?: string;
|
|
27
|
+
variant?: 'split' | 'stacked';
|
|
28
|
+
className?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const {
|
|
32
|
+
title = 'Profile',
|
|
33
|
+
description = 'This information is visible to your team.',
|
|
34
|
+
form = true,
|
|
35
|
+
saveLabel = 'Save changes',
|
|
36
|
+
savedText = 'Changes saved',
|
|
37
|
+
id = 'settings-section',
|
|
38
|
+
variant = 'split',
|
|
39
|
+
className = '',
|
|
40
|
+
} = Astro.props;
|
|
41
|
+
|
|
42
|
+
const Body = form ? 'form' : 'div';
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
<section class:list={['set', `set--${variant}`, className]} aria-labelledby={`${id}-title`}>
|
|
46
|
+
<div class="set__intro">
|
|
47
|
+
<h2 class="set__title" id={`${id}-title`}>{title}</h2>
|
|
48
|
+
{description && <p class="set__desc">{description}</p>}
|
|
49
|
+
</div>
|
|
50
|
+
<Body class="set__card" data-settings-form={form ? '' : undefined} novalidate={form ? true : undefined}>
|
|
51
|
+
<div class="set__fields"><slot /></div>
|
|
52
|
+
{form && (
|
|
53
|
+
<div class="set__foot">
|
|
54
|
+
<slot name="footer-start" />
|
|
55
|
+
<p class="set__saved" aria-live="polite" data-saved data-text={savedText}></p>
|
|
56
|
+
<button type="reset" class="set__btn">Cancel</button>
|
|
57
|
+
<button type="submit" class="set__btn set__btn--primary">{saveLabel}</button>
|
|
58
|
+
</div>
|
|
59
|
+
)}
|
|
60
|
+
</Body>
|
|
61
|
+
</section>
|
|
62
|
+
|
|
63
|
+
<script>
|
|
64
|
+
document.querySelectorAll<HTMLFormElement>('form[data-settings-form]:not([data-bound])').forEach((f) => {
|
|
65
|
+
f.dataset.bound = '';
|
|
66
|
+
const saved = f.querySelector<HTMLElement>('[data-saved]');
|
|
67
|
+
f.addEventListener('submit', (e) => {
|
|
68
|
+
e.preventDefault();
|
|
69
|
+
if (!f.checkValidity()) {
|
|
70
|
+
f.querySelector<HTMLElement>(':invalid')?.focus();
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (saved) {
|
|
74
|
+
saved.textContent = `✓ ${saved.dataset.text}`;
|
|
75
|
+
window.setTimeout(() => { saved.textContent = ''; }, 2600);
|
|
76
|
+
}
|
|
77
|
+
f.dispatchEvent(new CustomEvent('app:settings-save', { bubbles: true, detail: new FormData(f) }));
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
</script>
|
|
81
|
+
|
|
82
|
+
<style>
|
|
83
|
+
.set { container-type: inline-size; display: grid; grid-template-columns: minmax(0, 0.8fr) minmax(0, 2fr); gap: var(--sp-6, 1.5rem); padding-block: var(--sp-6, 1.5rem); border-bottom: 1px solid var(--c-border, rgba(255, 255, 255, 0.08)); }
|
|
84
|
+
.set--stacked { grid-template-columns: 1fr; }
|
|
85
|
+
.set__title { margin: 0; font-size: 1rem; font-weight: 700; color: var(--c-text-1, #fff); }
|
|
86
|
+
.set__desc { margin: 4px 0 0; font-size: 0.85rem; line-height: 1.55; color: var(--c-text-2, #a3a3a3); }
|
|
87
|
+
.set__card { border-radius: var(--r-lg, 16px); border: 1px solid var(--c-border, rgba(255, 255, 255, 0.08)); background: var(--c-bg-1, #0a0a0a); overflow: hidden; }
|
|
88
|
+
.set__fields { display: grid; gap: var(--sp-4, 1rem); padding: var(--sp-5, 1.25rem); }
|
|
89
|
+
.set__foot { display: flex; align-items: center; justify-content: flex-end; gap: var(--sp-2, 0.5rem); padding: var(--sp-3, 0.75rem) var(--sp-5, 1.25rem); border-top: 1px solid var(--c-border, rgba(255, 255, 255, 0.08)); background: rgb(var(--ink-rgb, 255 255 255) / 0.02); }
|
|
90
|
+
.set__saved { margin: 0 auto 0 0; font-size: 0.8rem; font-weight: 600; color: var(--c-success, #34d399); }
|
|
91
|
+
.set__btn { min-height: 38px; padding: 0 0.9rem; 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; cursor: pointer; }
|
|
92
|
+
.set__btn--primary { border-color: transparent; background: var(--app-accent, var(--c-primary-light, #d4a017)); color: var(--c-on-primary, #050505); }
|
|
93
|
+
.set__btn:focus-visible { outline: 2px solid var(--app-accent, var(--c-primary-light, #d4a017)); outline-offset: 2px; }
|
|
94
|
+
.set :global(.anim-input), .set :global(.custom-select) { background: var(--c-bg, #050505); }
|
|
95
|
+
@container (max-width: 640px) { .set__intro, .set__card { grid-column: 1 / -1; } }
|
|
96
|
+
</style>
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* SwitchRow — Settings row with label, description and an accessible on/off switch.
|
|
4
|
+
*
|
|
5
|
+
* @param {string} label - Setting name. Default 'Email notifications'.
|
|
6
|
+
* @param {string} description - Help text.
|
|
7
|
+
* @param {string} name - Form field name (submitted as "on" when checked). Default 'setting'.
|
|
8
|
+
* @param {boolean} checked - Initial state. Default true.
|
|
9
|
+
* @param {boolean} disabled - Disable the switch. Default false.
|
|
10
|
+
* @param {string} id - Input id (unique). Defaults to `sw-${name}`.
|
|
11
|
+
* @param {string} size - Switch size (sm|md). Default 'md'.
|
|
12
|
+
* @param {string} className - Extra class.
|
|
13
|
+
*
|
|
14
|
+
* Accessibility: native checkbox with role="switch", associated label and description
|
|
15
|
+
* (aria-describedby), visible focus ring, 44px touch target.
|
|
16
|
+
*/
|
|
17
|
+
interface Props {
|
|
18
|
+
label?: string;
|
|
19
|
+
description?: string;
|
|
20
|
+
name?: string;
|
|
21
|
+
checked?: boolean;
|
|
22
|
+
disabled?: boolean;
|
|
23
|
+
id?: string;
|
|
24
|
+
size?: 'sm' | 'md';
|
|
25
|
+
className?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const {
|
|
29
|
+
label = 'Email notifications',
|
|
30
|
+
description = 'Get an email for new bookings and cancellations.',
|
|
31
|
+
name = 'setting',
|
|
32
|
+
checked = true,
|
|
33
|
+
disabled = false,
|
|
34
|
+
id,
|
|
35
|
+
size = 'md',
|
|
36
|
+
className = '',
|
|
37
|
+
} = Astro.props;
|
|
38
|
+
|
|
39
|
+
const inputId = id ?? `sw-${name}`;
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
<div class:list={['sw-row', `sw-row--${size}`, { 'is-disabled': disabled }, className]}>
|
|
43
|
+
<div class="sw-row__text">
|
|
44
|
+
<label class="sw-row__label" for={inputId}>{label}</label>
|
|
45
|
+
{description && <p class="sw-row__desc" id={`${inputId}-desc`}>{description}</p>}
|
|
46
|
+
</div>
|
|
47
|
+
<span class="sw-row__switch">
|
|
48
|
+
<input type="checkbox" role="switch" id={inputId} name={name} checked={checked} disabled={disabled} aria-describedby={description ? `${inputId}-desc` : undefined} />
|
|
49
|
+
<span class="sw-row__track" aria-hidden="true"></span>
|
|
50
|
+
</span>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<style>
|
|
54
|
+
.sw-row { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-4, 1rem); min-height: 44px; }
|
|
55
|
+
.sw-row + :global(.sw-row) { padding-top: var(--sp-4, 1rem); border-top: 1px solid var(--c-border, rgba(255, 255, 255, 0.06)); }
|
|
56
|
+
.sw-row__label { font-size: 0.9rem; font-weight: 600; color: var(--c-text-1, #fff); cursor: pointer; }
|
|
57
|
+
.sw-row__desc { margin: 2px 0 0; font-size: 0.8rem; line-height: 1.5; color: var(--c-text-2, #a3a3a3); }
|
|
58
|
+
.sw-row__switch { position: relative; flex-shrink: 0; width: 44px; height: 26px; }
|
|
59
|
+
.sw-row--sm .sw-row__switch { width: 36px; height: 22px; }
|
|
60
|
+
.sw-row__switch input { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; opacity: 0; cursor: pointer; z-index: 1; }
|
|
61
|
+
.sw-row__track { position: absolute; inset: 0; border-radius: 999px; background: rgb(var(--ink-rgb, 255 255 255) / 0.16); transition: background 0.2s ease; }
|
|
62
|
+
.sw-row__track::after { content: ''; position: absolute; top: 3px; left: 3px; width: 20px; height: 20px; border-radius: 50%; background: #fff; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); transition: transform 0.2s cubic-bezier(0.34, 1.4, 0.64, 1); }
|
|
63
|
+
.sw-row--sm .sw-row__track::after { width: 16px; height: 16px; }
|
|
64
|
+
.sw-row__switch input:checked + .sw-row__track { background: var(--app-accent, var(--c-primary-light, #d4a017)); }
|
|
65
|
+
.sw-row__switch input:checked + .sw-row__track::after { transform: translateX(18px); }
|
|
66
|
+
.sw-row--sm .sw-row__switch input:checked + .sw-row__track::after { transform: translateX(14px); }
|
|
67
|
+
.sw-row__switch input:focus-visible + .sw-row__track { outline: 2px solid var(--app-accent, var(--c-primary-light, #d4a017)); outline-offset: 2px; }
|
|
68
|
+
.sw-row.is-disabled { opacity: 0.5; }
|
|
69
|
+
.sw-row.is-disabled input { cursor: not-allowed; }
|
|
70
|
+
@media (prefers-reduced-motion: reduce) { .sw-row__track, .sw-row__track::after { transition: none; } }
|
|
71
|
+
</style>
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* TeamMembers — Team management list: members with avatar, email, role selector and status,
|
|
4
|
+
* an invite form, and remove with confirmation.
|
|
5
|
+
*
|
|
6
|
+
* @param {string} title - Card title. Default 'Team'.
|
|
7
|
+
* @param {Array<{name: string, email: string, role: string, status?: 'active'|'invited'|'suspended', you?: boolean}>} members - People.
|
|
8
|
+
* @param {string[]} roles - Role options. Default ['Owner', 'Manager', 'Front desk', 'Housekeeping', 'Viewer'].
|
|
9
|
+
* @param {number} seats - Seats in the plan (shows "4 of 10 seats"). Optional.
|
|
10
|
+
* @param {string} variant - Style (card|plain). Default 'card'.
|
|
11
|
+
* @param {string} className - Extra class.
|
|
12
|
+
*
|
|
13
|
+
* Accessibility: real list; role selects and remove buttons are labelled with the member's name;
|
|
14
|
+
* the invite email field validates natively; changes are announced politely.
|
|
15
|
+
* Emits `app:team-invite` `{ email, role }` and `app:team-remove` `{ email }`.
|
|
16
|
+
*/
|
|
17
|
+
import { initials, hueFrom } from './app-icons';
|
|
18
|
+
|
|
19
|
+
interface Member { name: string; email: string; role: string; status?: 'active' | 'invited' | 'suspended'; you?: boolean }
|
|
20
|
+
interface Props {
|
|
21
|
+
title?: string;
|
|
22
|
+
members?: Member[];
|
|
23
|
+
roles?: string[];
|
|
24
|
+
seats?: number;
|
|
25
|
+
variant?: 'card' | 'plain';
|
|
26
|
+
className?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const {
|
|
30
|
+
title = 'Team',
|
|
31
|
+
members = [
|
|
32
|
+
{ name: 'Lucía Ortega', email: 'lucia@mirador.com', role: 'Owner', status: 'active', you: true },
|
|
33
|
+
{ name: 'Daniel Kim', email: 'daniel@mirador.com', role: 'Manager', status: 'active' },
|
|
34
|
+
{ name: 'Rosa Jiménez', email: 'rosa@mirador.com', role: 'Housekeeping', status: 'active' },
|
|
35
|
+
{ name: 'Omar Haddad', email: 'omar@mirador.com', role: 'Front desk', status: 'invited' },
|
|
36
|
+
],
|
|
37
|
+
roles = ['Owner', 'Manager', 'Front desk', 'Housekeeping', 'Viewer'],
|
|
38
|
+
seats = 10,
|
|
39
|
+
variant = 'card',
|
|
40
|
+
className = '',
|
|
41
|
+
} = Astro.props;
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
<section class:list={['team', `team--${variant}`, className]} data-team aria-labelledby="team-title">
|
|
45
|
+
<header class="team__head">
|
|
46
|
+
<div>
|
|
47
|
+
<h2 class="team__title" id="team-title">{title}</h2>
|
|
48
|
+
{seats && <p class="team__seats"><span data-used>{members.length}</span> of {seats} seats used</p>}
|
|
49
|
+
</div>
|
|
50
|
+
</header>
|
|
51
|
+
|
|
52
|
+
<form class="team__invite" novalidate data-invite>
|
|
53
|
+
<label class="sr-only" for="team-email">Email to invite</label>
|
|
54
|
+
<input id="team-email" type="email" name="email" placeholder="name@company.com" required autocomplete="email" />
|
|
55
|
+
<label class="sr-only" for="team-role">Role</label>
|
|
56
|
+
<select id="team-role" name="role">{roles.filter((r) => r !== 'Owner').map((r) => <option>{r}</option>)}</select>
|
|
57
|
+
<button type="submit">Send invite</button>
|
|
58
|
+
</form>
|
|
59
|
+
<p class="team__msg" aria-live="polite" data-msg></p>
|
|
60
|
+
|
|
61
|
+
<ul class="team__list" data-list>
|
|
62
|
+
{members.map((m) => (
|
|
63
|
+
<li class="team__row" data-email={m.email}>
|
|
64
|
+
<span class="team__avatar" style={`--h: ${hueFrom(m.name)}`} aria-hidden="true">{initials(m.name)}</span>
|
|
65
|
+
<span class="team__who">
|
|
66
|
+
<strong>{m.name}{m.you && <em> (you)</em>}</strong>
|
|
67
|
+
<small>{m.email}</small>
|
|
68
|
+
</span>
|
|
69
|
+
<span class:list={['team__status', `team__status--${m.status ?? 'active'}`]}>{m.status === 'invited' ? 'Invited' : m.status === 'suspended' ? 'Suspended' : 'Active'}</span>
|
|
70
|
+
<label class="sr-only" for={`role-${m.email}`}>Role for {m.name}</label>
|
|
71
|
+
<select id={`role-${m.email}`} class="team__role" disabled={m.role === 'Owner'}>
|
|
72
|
+
{roles.map((r) => <option selected={r === m.role}>{r}</option>)}
|
|
73
|
+
</select>
|
|
74
|
+
<button type="button" class="team__remove" aria-label={`Remove ${m.name}`} disabled={m.you} data-remove>
|
|
75
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12"/></svg>
|
|
76
|
+
</button>
|
|
77
|
+
</li>
|
|
78
|
+
))}
|
|
79
|
+
</ul>
|
|
80
|
+
</section>
|
|
81
|
+
|
|
82
|
+
<script>
|
|
83
|
+
import { initials, hueFrom } from './app-icons';
|
|
84
|
+
|
|
85
|
+
document.querySelectorAll<HTMLElement>('[data-team]:not([data-bound])').forEach((team) => {
|
|
86
|
+
team.dataset.bound = '';
|
|
87
|
+
const list = team.querySelector<HTMLUListElement>('[data-list]')!;
|
|
88
|
+
const msg = team.querySelector<HTMLElement>('[data-msg]')!;
|
|
89
|
+
const used = team.querySelector<HTMLElement>('[data-used]');
|
|
90
|
+
const form = team.querySelector<HTMLFormElement>('[data-invite]')!;
|
|
91
|
+
const recount = () => { if (used) used.textContent = String(list.children.length); };
|
|
92
|
+
|
|
93
|
+
const bindRemove = (btn: HTMLButtonElement) => {
|
|
94
|
+
let armed = false;
|
|
95
|
+
btn.addEventListener('click', () => {
|
|
96
|
+
const row = btn.closest<HTMLElement>('.team__row')!;
|
|
97
|
+
const name = row.querySelector('strong')?.textContent?.replace(' (you)', '') ?? '';
|
|
98
|
+
if (!armed) {
|
|
99
|
+
armed = true;
|
|
100
|
+
btn.classList.add('is-armed');
|
|
101
|
+
btn.setAttribute('aria-label', `Confirm removing ${name}`);
|
|
102
|
+
msg.textContent = `Click again to remove ${name}.`;
|
|
103
|
+
window.setTimeout(() => { armed = false; btn.classList.remove('is-armed'); btn.setAttribute('aria-label', `Remove ${name}`); }, 3000);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
team.dispatchEvent(new CustomEvent('app:team-remove', { bubbles: true, detail: { email: row.dataset.email } }));
|
|
107
|
+
const next = (row.nextElementSibling ?? row.previousElementSibling)?.querySelector<HTMLElement>('select, button');
|
|
108
|
+
row.remove();
|
|
109
|
+
msg.textContent = `${name} was removed from the team.`;
|
|
110
|
+
recount();
|
|
111
|
+
next?.focus();
|
|
112
|
+
});
|
|
113
|
+
};
|
|
114
|
+
team.querySelectorAll<HTMLButtonElement>('[data-remove]').forEach(bindRemove);
|
|
115
|
+
|
|
116
|
+
form.addEventListener('submit', (e) => {
|
|
117
|
+
e.preventDefault();
|
|
118
|
+
const input = form.elements.namedItem('email') as HTMLInputElement;
|
|
119
|
+
const role = (form.elements.namedItem('role') as HTMLSelectElement).value;
|
|
120
|
+
if (!input.checkValidity()) { msg.textContent = 'Enter a valid email address.'; input.focus(); return; }
|
|
121
|
+
const email = input.value.trim();
|
|
122
|
+
const name = email.split('@')[0].replace(/[._-]+/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
|
123
|
+
const li = document.createElement('li');
|
|
124
|
+
li.className = 'team__row is-new';
|
|
125
|
+
li.dataset.email = email;
|
|
126
|
+
li.innerHTML = `<span class="team__avatar" style="--h:${hueFrom(name)}" aria-hidden="true">${initials(name)}</span>
|
|
127
|
+
<span class="team__who"><strong></strong><small></small></span>
|
|
128
|
+
<span class="team__status team__status--invited">Invited</span>
|
|
129
|
+
<span class="team__role-static"></span>
|
|
130
|
+
<button type="button" class="team__remove" data-remove><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12"/></svg></button>`;
|
|
131
|
+
li.querySelector('strong')!.textContent = name;
|
|
132
|
+
li.querySelector('small')!.textContent = email;
|
|
133
|
+
li.querySelector('.team__role-static')!.textContent = role;
|
|
134
|
+
li.querySelector('button')!.setAttribute('aria-label', `Remove ${name}`);
|
|
135
|
+
list.append(li);
|
|
136
|
+
bindRemove(li.querySelector<HTMLButtonElement>('[data-remove]')!);
|
|
137
|
+
msg.textContent = `Invitation sent to ${email} as ${role}.`;
|
|
138
|
+
team.dispatchEvent(new CustomEvent('app:team-invite', { bubbles: true, detail: { email, role } }));
|
|
139
|
+
form.reset();
|
|
140
|
+
recount();
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
</script>
|
|
144
|
+
|
|
145
|
+
<style>
|
|
146
|
+
.team { container-type: inline-size; display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--sp-3, 0.75rem); min-width: 0; }
|
|
147
|
+
.team--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); }
|
|
148
|
+
.team__title { margin: 0; font-size: 1rem; font-weight: 700; color: var(--c-text-1, #fff); }
|
|
149
|
+
.team__seats { margin: 2px 0 0; font-size: 0.8rem; color: var(--c-text-3, #737373); }
|
|
150
|
+
.team__invite { display: flex; gap: var(--sp-2, 0.5rem); }
|
|
151
|
+
.team__invite input, .team__invite select, .team__role {
|
|
152
|
+
min-height: 40px;
|
|
153
|
+
padding: 0 0.75rem;
|
|
154
|
+
border-radius: 10px;
|
|
155
|
+
border: 1px solid var(--c-border, rgba(255, 255, 255, 0.12));
|
|
156
|
+
background: var(--c-bg, #050505);
|
|
157
|
+
color: var(--c-text-1, #fff);
|
|
158
|
+
font: inherit;
|
|
159
|
+
font-size: 0.85rem;
|
|
160
|
+
}
|
|
161
|
+
.team__invite input { flex: 1; min-width: 0; }
|
|
162
|
+
.team__invite button { min-height: 40px; padding: 0 1rem; 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.85rem; font-weight: 700; cursor: pointer; white-space: nowrap; }
|
|
163
|
+
.team__msg { min-height: 1.2em; margin: 0; font-size: 0.8rem; color: var(--c-text-2, #a3a3a3); }
|
|
164
|
+
.team__list { display: grid; margin: 0; padding: 0; list-style: none; }
|
|
165
|
+
.team__list :global(.team__row) { display: grid; grid-template-columns: 36px minmax(0, 1fr) auto 150px 40px; align-items: center; gap: var(--sp-3, 0.75rem); padding: var(--sp-3, 0.75rem) 0; border-top: 1px solid var(--c-border, rgba(255, 255, 255, 0.06)); }
|
|
166
|
+
.team__list :global(.team__row.is-new) { animation: team-in 0.3s ease-out; }
|
|
167
|
+
@keyframes team-in { from { opacity: 0; transform: translateY(-4px); } }
|
|
168
|
+
.team__list :global(.team__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; }
|
|
169
|
+
.team__list :global(.team__who) { display: grid; min-width: 0; }
|
|
170
|
+
.team__list :global(.team__who strong) { font-size: 0.88rem; color: var(--c-text-1, #fff); }
|
|
171
|
+
.team__list :global(.team__who em) { font-style: normal; font-weight: 500; color: var(--c-text-3, #737373); }
|
|
172
|
+
.team__list :global(.team__who small) { font-size: 0.76rem; color: var(--c-text-3, #737373); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
173
|
+
.team__list :global(.team__status) { padding: 2px 8px; border-radius: 999px; font-size: 0.72rem; font-weight: 700; background: color-mix(in srgb, var(--c-success, #34d399) 14%, transparent); color: var(--c-success, #34d399); }
|
|
174
|
+
.team__list :global(.team__status--invited) { background: color-mix(in srgb, var(--c-warning, #fbbf24) 14%, transparent); color: var(--c-warning, #fbbf24); }
|
|
175
|
+
.team__list :global(.team__status--suspended) { background: color-mix(in srgb, var(--c-danger, #f87171) 14%, transparent); color: var(--c-danger, #f87171); }
|
|
176
|
+
.team__list :global(.team__role-static) { font-size: 0.85rem; color: var(--c-text-2, #a3a3a3); padding-left: 0.75rem; }
|
|
177
|
+
.team__role:disabled { opacity: 0.6; }
|
|
178
|
+
.team__list :global(.team__remove) { display: grid; place-items: center; width: 36px; height: 36px; border: 0; border-radius: 8px; background: transparent; color: var(--c-text-3, #737373); cursor: pointer; }
|
|
179
|
+
.team__list :global(.team__remove:hover) { background: color-mix(in srgb, var(--c-danger, #f87171) 12%, transparent); color: var(--c-danger, #f87171); }
|
|
180
|
+
.team__list :global(.team__remove.is-armed) { background: var(--c-danger, #f87171); color: #fff; }
|
|
181
|
+
.team__list :global(.team__remove:disabled) { visibility: hidden; }
|
|
182
|
+
.team :is(input, select, button):focus-visible, .team__list :global(button:focus-visible) { outline: 2px solid var(--app-accent, var(--c-primary-light, #d4a017)); outline-offset: 2px; }
|
|
183
|
+
.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
|
|
184
|
+
@container (max-width: 620px) {
|
|
185
|
+
.team__invite { flex-wrap: wrap; }
|
|
186
|
+
.team__list :global(.team__row) { grid-template-columns: 36px minmax(0, 1fr) 40px; }
|
|
187
|
+
.team__list :global(.team__status) { display: none; }
|
|
188
|
+
.team__role, .team__list :global(.team__role-static) { grid-column: 2; }
|
|
189
|
+
}
|
|
190
|
+
</style>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// src/components/lib/app/app-icons.ts
|
|
2
|
+
// Stroke icons (24×24, stroke = currentColor) shared by the App & Dashboard components.
|
|
3
|
+
|
|
4
|
+
export const appIcons = {
|
|
5
|
+
dashboard: '<rect x="3" y="3" width="7" height="9" rx="1.5"/><rect x="14" y="3" width="7" height="5" rx="1.5"/><rect x="14" y="12" width="7" height="9" rx="1.5"/><rect x="3" y="16" width="7" height="5" rx="1.5"/>',
|
|
6
|
+
calendar: '<rect x="3" y="5" width="18" height="16" rx="2"/><path d="M3 10h18M8 3v4M16 3v4"/>',
|
|
7
|
+
bed: '<path d="M3 18V8M21 18v-5a3 3 0 0 0-3-3h-7v8M3 14h18"/><circle cx="7" cy="11" r="1.6"/>',
|
|
8
|
+
users: '<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.9M16 3.1a4 4 0 0 1 0 7.8"/>',
|
|
9
|
+
chart: '<path d="M3 3v18h18"/><path d="m7 15 4-4 3 3 5-6"/>',
|
|
10
|
+
sparkle: '<path d="M12 3l1.9 5.8L20 10l-6.1 1.2L12 17l-1.9-5.8L4 10l6.1-1.2z"/>',
|
|
11
|
+
settings: '<circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1.1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.5-1.1 1.7 1.7 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.8.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8V9a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1z"/>',
|
|
12
|
+
bell: '<path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M10.3 21a1.9 1.9 0 0 0 3.4 0"/>',
|
|
13
|
+
search: '<circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/>',
|
|
14
|
+
wallet: '<rect x="2" y="6" width="20" height="14" rx="2"/><path d="M2 10h20M16 15h2"/>',
|
|
15
|
+
broom: '<path d="M14 4 20 10M9 9l6 6M4 20l5-5M9 15l-2 5 5-2"/><path d="m11 13 7-7"/>',
|
|
16
|
+
door: '<path d="M14 3H6v18h8"/><path d="M14 3l5 2v14l-5 2z"/><path d="M11 12h.01"/>',
|
|
17
|
+
plane: '<path d="M2 16l20-7-20-7 4 7z"/><path d="M6 9h8"/>',
|
|
18
|
+
logout: '<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4M16 17l5-5-5-5M21 12H9"/>',
|
|
19
|
+
plus: '<path d="M12 5v14M5 12h14"/>',
|
|
20
|
+
download: '<path d="M12 3v12M7 10l5 5 5-5M5 21h14"/>',
|
|
21
|
+
filter: '<path d="M3 5h18l-7 8v6l-4 2v-8z"/>',
|
|
22
|
+
more: '<circle cx="5" cy="12" r="1"/><circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/>',
|
|
23
|
+
check: '<path d="M20 6 9 17l-5-5"/>',
|
|
24
|
+
x: '<path d="M18 6 6 18M6 6l12 12"/>',
|
|
25
|
+
star: '<path d="M12 3l2.7 5.6 6.1.9-4.4 4.3 1 6.1L12 17l-5.4 2.9 1-6.1L3.2 9.5l6.1-.9z"/>',
|
|
26
|
+
mail: '<rect x="3" y="5" width="18" height="14" rx="2"/><path d="m3 7 9 6 9-6"/>',
|
|
27
|
+
phone: '<path d="M22 16.9v3a2 2 0 0 1-2.2 2 19.8 19.8 0 0 1-8.6-3.1 19.5 19.5 0 0 1-6-6A19.8 19.8 0 0 1 2.1 4.2 2 2 0 0 1 4.1 2h3a2 2 0 0 1 2 1.7c.1.9.4 1.8.7 2.7a2 2 0 0 1-.5 2.1L8 9.8a16 16 0 0 0 6 6l1.3-1.3a2 2 0 0 1 2.1-.5c.9.3 1.8.6 2.7.7a2 2 0 0 1 1.7 2z"/>',
|
|
28
|
+
key: '<circle cx="7.5" cy="15.5" r="4.5"/><path d="m21 2-9.6 9.6M15.5 7.5l3 3L22 7l-3-3"/>',
|
|
29
|
+
menu: '<path d="M4 6h16M4 12h16M4 18h16"/>',
|
|
30
|
+
chevron: '<path d="m9 18 6-6-6-6"/>',
|
|
31
|
+
sun: '<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/>',
|
|
32
|
+
moon: '<path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/>',
|
|
33
|
+
building: '<rect x="4" y="2" width="16" height="20" rx="2"/><path d="M9 22v-4h6v4M8 6h.01M12 6h.01M16 6h.01M8 10h.01M12 10h.01M16 10h.01M8 14h.01M12 14h.01M16 14h.01"/>',
|
|
34
|
+
} as const;
|
|
35
|
+
|
|
36
|
+
export type AppIcon = keyof typeof appIcons;
|
|
37
|
+
|
|
38
|
+
export function appIcon(name?: string): string {
|
|
39
|
+
return appIcons[(name as AppIcon) ?? 'dashboard'] ?? appIcons.dashboard;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Initials for avatar fallbacks: "Lucía Ortega" → "LO". */
|
|
43
|
+
export function initials(name: string): string {
|
|
44
|
+
return name.split(/\s+/).filter(Boolean).slice(0, 2).map((p) => p[0]!.toUpperCase()).join('');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Stable hue (0–360) from a string, for avatar backgrounds. */
|
|
48
|
+
export function hueFrom(text: string): number {
|
|
49
|
+
let h = 0;
|
|
50
|
+
for (let i = 0; i < text.length; i++) h = (h * 31 + text.charCodeAt(i)) % 360;
|
|
51
|
+
return h;
|
|
52
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// src/components/lib/app/index.ts
|
|
2
|
+
// Barrel export for App & Dashboard components (SaaS shells, tables, boards, settings).
|
|
3
|
+
|
|
4
|
+
export { default as AppSidebar } from './AppSidebar.astro';
|
|
5
|
+
export { default as AppTopbar } from './AppTopbar.astro';
|
|
6
|
+
export { default as PageHeader } from './PageHeader.astro';
|
|
7
|
+
export { default as DataTable } from './DataTable.astro';
|
|
8
|
+
export { default as RoomRack } from './RoomRack.astro';
|
|
9
|
+
export { default as HousekeepingBoard } from './HousekeepingBoard.astro';
|
|
10
|
+
export { default as ArrivalsBoard } from './ArrivalsBoard.astro';
|
|
11
|
+
export { default as GuestProfileCard } from './GuestProfileCard.astro';
|
|
12
|
+
export { default as SettingsSection } from './SettingsSection.astro';
|
|
13
|
+
export { default as SwitchRow } from './SwitchRow.astro';
|
|
14
|
+
export { default as TeamMembers } from './TeamMembers.astro';
|
|
15
|
+
export { default as PlanUsageCard } from './PlanUsageCard.astro';
|
|
16
|
+
export { default as AuthCard } from './AuthCard.astro';
|
|
17
|
+
|
|
18
|
+
export { appIcons, appIcon, initials, hueFrom } from './app-icons';
|
|
19
|
+
export type { AppIcon } from './app-icons';
|