astro-component-kit 0.3.1 → 0.4.1
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 +133 -36
- package/package.json +17 -4
- package/src/components/index.ts +18 -202
- 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/buttons/NeonBorderButton.astro +162 -0
- package/src/components/lib/buttons/RippleButton.astro +199 -0
- package/src/components/lib/buttons/SlideRevealButton.astro +159 -0
- package/src/components/lib/buttons/index.ts +4 -0
- package/src/components/lib/cards/GithubRepoCard.astro +176 -0
- package/src/components/lib/cards/PricingCompareMiniCard.astro +196 -0
- package/src/components/lib/cards/StatsTrendCard.astro +149 -0
- package/src/components/lib/cards/index.ts +3 -0
- package/src/components/lib/charts/ActivityRings.astro +105 -0
- package/src/components/lib/charts/AreaChart.astro +4 -4
- package/src/components/lib/charts/BarChart.astro +3 -3
- package/src/components/lib/charts/BoxPlotChart.astro +109 -0
- package/src/components/lib/charts/BubbleChart.astro +101 -0
- package/src/components/lib/charts/BulletChart.astro +160 -0
- package/src/components/lib/charts/BumpChart.astro +92 -0
- package/src/components/lib/charts/CandlestickChart.astro +105 -0
- package/src/components/lib/charts/ChartFrame.astro +373 -0
- package/src/components/lib/charts/DivergingBarChart.astro +98 -0
- package/src/components/lib/charts/DonutChart.astro +2 -2
- package/src/components/lib/charts/DumbbellChart.astro +92 -0
- package/src/components/lib/charts/ForecastLineChart.astro +125 -0
- package/src/components/lib/charts/FunnelChart.astro +122 -0
- package/src/components/lib/charts/GaugeChart.astro +111 -0
- package/src/components/lib/charts/GoalProgressChart.astro +97 -0
- package/src/components/lib/charts/GroupedBarChart.astro +125 -0
- package/src/components/lib/charts/HeatmapChart.astro +131 -0
- package/src/components/lib/charts/HistogramChart.astro +111 -0
- package/src/components/lib/charts/HorizontalBarChart.astro +155 -0
- package/src/components/lib/charts/KpiStatTile.astro +116 -0
- package/src/components/lib/charts/KpiTrendGrid.astro +60 -0
- package/src/components/lib/charts/LineChart.astro +160 -0
- package/src/components/lib/charts/LinearProgressGauge.astro +136 -0
- package/src/components/lib/charts/LollipopChart.astro +100 -0
- package/src/components/lib/charts/MeterGauge.astro +122 -0
- package/src/components/lib/charts/MetricDeltaTable.astro +125 -0
- package/src/components/lib/charts/PieChart.astro +135 -0
- package/src/components/lib/charts/PolarAreaChart.astro +85 -0
- package/src/components/lib/charts/PopulationPyramid.astro +122 -0
- package/src/components/lib/charts/PunchCardChart.astro +89 -0
- package/src/components/lib/charts/RadarChart.astro +94 -0
- package/src/components/lib/charts/RadarMetricChart.astro +168 -0
- package/src/components/lib/charts/RadialBarChart.astro +124 -0
- package/src/components/lib/charts/RangeAreaChart.astro +113 -0
- package/src/components/lib/charts/RetentionCohortTable.astro +126 -0
- package/src/components/lib/charts/SankeyChart.astro +128 -0
- package/src/components/lib/charts/ScatterPlot.astro +139 -0
- package/src/components/lib/charts/SegmentedBarChart.astro +119 -0
- package/src/components/lib/charts/SlopeChart.astro +105 -0
- package/src/components/lib/charts/SparklineChart.astro +87 -0
- package/src/components/lib/charts/StackedAreaChart.astro +130 -0
- package/src/components/lib/charts/StackedBarChart.astro +136 -0
- package/src/components/lib/charts/SunburstChart.astro +103 -0
- package/src/components/lib/charts/TreemapChart.astro +94 -0
- package/src/components/lib/charts/WaffleChart.astro +121 -0
- package/src/components/lib/charts/WaterfallChart.astro +118 -0
- package/src/components/lib/charts/chart-tooltip.ts +75 -0
- package/src/components/lib/charts/chart-utils.ts +271 -0
- package/src/components/lib/charts/index.ts +46 -0
- package/src/components/lib/data/ActivityHeatmapGrid.astro +157 -0
- package/src/components/lib/data/MetricComparisonRow.astro +109 -0
- package/src/components/lib/data/StatusTimelineFeed.astro +168 -0
- package/src/components/lib/data/index.ts +3 -0
- 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/feedback/ProgressRingCounter.astro +118 -0
- package/src/components/lib/feedback/ToastNotificationGroup.astro +173 -0
- package/src/components/lib/feedback/index.ts +2 -0
- package/src/components/lib/forms/AnimatedInput.astro +4 -2
- package/src/components/lib/forms/CustomRadioGroup.astro +3 -1
- package/src/components/lib/forms/GlassInlineToggleGroup.astro +161 -0
- package/src/components/lib/forms/PasswordStrengthInput.astro +242 -0
- package/src/components/lib/forms/PhoneCountryInput.astro +145 -0
- package/src/components/lib/forms/TagsAutocompleteInput.astro +249 -0
- package/src/components/lib/forms/index.ts +5 -0
- 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/DiagonalSectionDivider.astro +69 -0
- 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 +127 -0
- 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/layout/index.ts +3 -1
- package/src/components/lib/navigation/QuickJumpDock.astro +125 -0
- package/src/components/lib/navigation/StepBreadcrumbBar.astro +151 -0
- package/src/components/lib/navigation/SubNavbarPills.astro +102 -0
- package/src/components/lib/navigation/index.ts +3 -0
- package/src/components/lib/overlays/CookiePreferencesDrawer.astro +226 -0
- package/src/components/lib/overlays/FloatingFeedbackWidget.astro +203 -0
- package/src/components/lib/overlays/ImageCompareSlider.astro +172 -0
- package/src/components/lib/overlays/index.ts +3 -0
- 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 +133 -0
- 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 +141 -0
- package/src/components/lib/sections/ChangelogSection.astro +151 -0
- package/src/components/lib/sections/ContactGlassForm.astro +10 -8
- package/src/components/lib/sections/ContactSplitMap.astro +141 -0
- 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 +165 -0
- package/src/components/lib/sections/CtaSplitImage.astro +149 -0
- 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 +121 -0
- package/src/components/lib/sections/FeatureAlternatingRows.astro +192 -0
- package/src/components/lib/sections/FeatureBentoGrid.astro +13 -11
- package/src/components/lib/sections/FeatureIconGrid.astro +155 -0
- package/src/components/lib/sections/FeatureTabsShowcase.astro +182 -0
- 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 +157 -0
- package/src/components/lib/sections/FormsShowcase.astro +22 -20
- package/src/components/lib/sections/HeroDashboardPreview.astro +182 -0
- 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 +175 -0
- package/src/components/lib/sections/HeroShowcaseSplit.astro +306 -0
- package/src/components/lib/sections/HeroSplit.astro +13 -11
- package/src/components/lib/sections/HeroTerminal.astro +253 -0
- package/src/components/lib/sections/HeroWaitlist.astro +231 -0
- package/src/components/lib/sections/HowItWorksSteps.astro +180 -0
- package/src/components/lib/sections/IntegrationsGrid.astro +167 -0
- package/src/components/lib/sections/LayoutShowcase.astro +21 -19
- package/src/components/lib/sections/LogoCloudSection.astro +96 -0
- package/src/components/lib/sections/LogoWallGrid.astro +93 -0
- 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 +159 -0
- package/src/components/lib/sections/PricingTierCards.astro +11 -9
- package/src/components/lib/sections/PricingToggleCards.astro +171 -0
- 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 +124 -0
- package/src/components/lib/sections/StatsDashboardSection.astro +85 -0
- 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 +167 -0
- package/src/components/lib/sections/TestimonialFeatured.astro +122 -0
- package/src/components/lib/sections/TestimonialGridSection.astro +188 -0
- 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 +39 -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/components/lib/typography/AnimatedCounterText.astro +102 -0
- package/src/components/lib/typography/CurvedPathHeading.astro +86 -0
- package/src/components/lib/typography/GradientTextMarquee.astro +105 -0
- package/src/components/lib/typography/HackerDecodeText.astro +85 -0
- package/src/components/lib/typography/index.ts +4 -0
- package/src/styles/global.css +113 -1
|
@@ -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';
|