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,189 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* HousekeepingBoard — Room status board grouped by floor: clean, dirty, inspecting, occupied and
|
|
4
|
+
* out of order, with live counters, status filter and a quick "next status" action per room.
|
|
5
|
+
*
|
|
6
|
+
* @param {Array<{number: string, floor: string, type?: string, status: Status, guest?: string, note?: string, assignee?: string}>} rooms - Rooms to show.
|
|
7
|
+
* @param {string} title - Card title. Default 'Housekeeping'.
|
|
8
|
+
* @param {boolean} interactive - Clicking a tile advances dirty → inspecting → clean (demo/workflow). Default true.
|
|
9
|
+
* @param {string} variant - Style (card|plain). Default 'card'.
|
|
10
|
+
* @param {string} size - Tile size (sm|md). Default 'md'.
|
|
11
|
+
* @param {string} className - Extra class.
|
|
12
|
+
*
|
|
13
|
+
* Statuses: clean · dirty · inspecting · occupied · ooo (out of order).
|
|
14
|
+
* Emits `app:room-status` with `{ room, status }` on change.
|
|
15
|
+
*
|
|
16
|
+
* Accessibility: status is written in text on every tile (not colour only); tiles are buttons
|
|
17
|
+
* with full labels; counters live in a polite region; the filter is a set of toggle buttons.
|
|
18
|
+
*/
|
|
19
|
+
type Status = 'clean' | 'dirty' | 'inspecting' | 'occupied' | 'ooo';
|
|
20
|
+
interface Room { number: string; floor: string; type?: string; status: Status; guest?: string; note?: string; assignee?: string }
|
|
21
|
+
interface Props {
|
|
22
|
+
rooms?: Room[];
|
|
23
|
+
title?: string;
|
|
24
|
+
interactive?: boolean;
|
|
25
|
+
variant?: 'card' | 'plain';
|
|
26
|
+
size?: 'sm' | 'md';
|
|
27
|
+
className?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const {
|
|
31
|
+
rooms = [
|
|
32
|
+
{ number: '101', floor: 'Floor 1', type: 'Standard', status: 'clean' },
|
|
33
|
+
{ number: '102', floor: 'Floor 1', type: 'Standard', status: 'occupied', guest: 'A. Tanaka' },
|
|
34
|
+
{ number: '103', floor: 'Floor 1', type: 'Standard', status: 'dirty', assignee: 'Rosa' },
|
|
35
|
+
{ number: '104', floor: 'Floor 1', type: 'Twin', status: 'inspecting', assignee: 'Luis' },
|
|
36
|
+
{ number: '201', floor: 'Floor 2', type: 'Deluxe', status: 'ooo', note: 'A/C repair' },
|
|
37
|
+
{ number: '202', floor: 'Floor 2', type: 'Deluxe', status: 'occupied', guest: 'J. Whitfield' },
|
|
38
|
+
{ number: '203', floor: 'Floor 2', type: 'Deluxe', status: 'dirty', assignee: 'Rosa' },
|
|
39
|
+
{ number: '204', floor: 'Floor 2', type: 'Deluxe', status: 'clean' },
|
|
40
|
+
],
|
|
41
|
+
title = 'Housekeeping',
|
|
42
|
+
interactive = true,
|
|
43
|
+
variant = 'card',
|
|
44
|
+
size = 'md',
|
|
45
|
+
className = '',
|
|
46
|
+
} = Astro.props;
|
|
47
|
+
|
|
48
|
+
const labels: Record<Status, string> = { clean: 'Clean', dirty: 'Dirty', inspecting: 'Inspecting', occupied: 'Occupied', ooo: 'Out of order' };
|
|
49
|
+
const order: Status[] = ['clean', 'dirty', 'inspecting', 'occupied', 'ooo'];
|
|
50
|
+
const floors = Array.from(new Set(rooms.map((r) => r.floor)));
|
|
51
|
+
const counts = Object.fromEntries(order.map((s) => [s, rooms.filter((r) => r.status === s).length]));
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
<section class:list={['hk', `hk--${variant}`, `hk--${size}`, className]} data-hk data-interactive={interactive ? '' : undefined} aria-labelledby="hk-title">
|
|
55
|
+
<header class="hk__head">
|
|
56
|
+
<h2 class="hk__title" id="hk-title">{title}</h2>
|
|
57
|
+
<div class="hk__filters" role="group" aria-label="Filter rooms by status">
|
|
58
|
+
<button type="button" class="hk__filter" aria-pressed="true" data-f="">All <span data-c="">{rooms.length}</span></button>
|
|
59
|
+
{order.map((s) => (
|
|
60
|
+
<button type="button" class:list={['hk__filter', `hk__filter--${s}`]} aria-pressed="false" data-f={s}>
|
|
61
|
+
<i aria-hidden="true"></i>{labels[s]} <span data-c={s}>{counts[s]}</span>
|
|
62
|
+
</button>
|
|
63
|
+
))}
|
|
64
|
+
</div>
|
|
65
|
+
</header>
|
|
66
|
+
|
|
67
|
+
<p class="sr-only" aria-live="polite" data-announce></p>
|
|
68
|
+
|
|
69
|
+
{floors.map((floor) => (
|
|
70
|
+
<div class="hk__floor">
|
|
71
|
+
<h3 class="hk__floor-title">{floor}</h3>
|
|
72
|
+
<ul class="hk__grid">
|
|
73
|
+
{rooms.filter((r) => r.floor === floor).map((r) => (
|
|
74
|
+
<li data-status={r.status}>
|
|
75
|
+
<button
|
|
76
|
+
type="button"
|
|
77
|
+
class:list={['hk__room', `hk__room--${r.status}`]}
|
|
78
|
+
data-room={r.number}
|
|
79
|
+
data-status={r.status}
|
|
80
|
+
aria-label={`Room ${r.number}, ${labels[r.status]}${r.guest ? `, ${r.guest}` : ''}${r.assignee ? `, assigned to ${r.assignee}` : ''}${interactive && (r.status === 'dirty' || r.status === 'inspecting') ? '. Activate to advance status' : ''}`}
|
|
81
|
+
>
|
|
82
|
+
<span class="hk__num">{r.number}</span>
|
|
83
|
+
{r.type && <span class="hk__type">{r.type}</span>}
|
|
84
|
+
<span class="hk__state"><i aria-hidden="true"></i><span data-label>{labels[r.status]}</span></span>
|
|
85
|
+
{(r.guest || r.note || r.assignee) && <span class="hk__meta">{r.guest ?? r.note ?? `→ ${r.assignee}`}</span>}
|
|
86
|
+
</button>
|
|
87
|
+
</li>
|
|
88
|
+
))}
|
|
89
|
+
</ul>
|
|
90
|
+
</div>
|
|
91
|
+
))}
|
|
92
|
+
</section>
|
|
93
|
+
|
|
94
|
+
<script>
|
|
95
|
+
const LABELS: Record<string, string> = { clean: 'Clean', dirty: 'Dirty', inspecting: 'Inspecting', occupied: 'Occupied', ooo: 'Out of order' };
|
|
96
|
+
const NEXT: Record<string, string> = { dirty: 'inspecting', inspecting: 'clean' };
|
|
97
|
+
|
|
98
|
+
document.querySelectorAll<HTMLElement>('[data-hk]:not([data-bound])').forEach((hk) => {
|
|
99
|
+
hk.dataset.bound = '';
|
|
100
|
+
const filters = Array.from(hk.querySelectorAll<HTMLButtonElement>('[data-f]'));
|
|
101
|
+
const announce = hk.querySelector<HTMLElement>('[data-announce]')!;
|
|
102
|
+
let active = '';
|
|
103
|
+
|
|
104
|
+
const recount = () => {
|
|
105
|
+
const items = Array.from(hk.querySelectorAll<HTMLElement>('li[data-status]'));
|
|
106
|
+
hk.querySelectorAll<HTMLElement>('[data-c]').forEach((c) => {
|
|
107
|
+
const s = c.dataset.c;
|
|
108
|
+
c.textContent = String(s ? items.filter((i) => i.dataset.status === s).length : items.length);
|
|
109
|
+
});
|
|
110
|
+
items.forEach((i) => { i.hidden = Boolean(active) && i.dataset.status !== active; });
|
|
111
|
+
hk.querySelectorAll<HTMLElement>('.hk__floor').forEach((f) => { f.hidden = !f.querySelector('li:not([hidden])'); });
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
filters.forEach((f) => f.addEventListener('click', () => {
|
|
115
|
+
active = f.dataset.f || '';
|
|
116
|
+
filters.forEach((x) => x.setAttribute('aria-pressed', String(x === f)));
|
|
117
|
+
recount();
|
|
118
|
+
}));
|
|
119
|
+
|
|
120
|
+
if (hk.hasAttribute('data-interactive')) {
|
|
121
|
+
hk.querySelectorAll<HTMLButtonElement>('.hk__room').forEach((btn) => btn.addEventListener('click', () => {
|
|
122
|
+
const next = NEXT[btn.dataset.status || ''];
|
|
123
|
+
if (!next) return;
|
|
124
|
+
btn.classList.remove(`hk__room--${btn.dataset.status}`);
|
|
125
|
+
btn.classList.add(`hk__room--${next}`);
|
|
126
|
+
btn.dataset.status = next;
|
|
127
|
+
btn.closest('li')!.dataset.status = next;
|
|
128
|
+
btn.querySelector('[data-label]')!.textContent = LABELS[next];
|
|
129
|
+
btn.setAttribute('aria-label', btn.getAttribute('aria-label')!.replace(/, (Dirty|Inspecting|Clean)/, `, ${LABELS[next]}`));
|
|
130
|
+
announce.textContent = `Room ${btn.dataset.room} is now ${LABELS[next].toLowerCase()}.`;
|
|
131
|
+
hk.dispatchEvent(new CustomEvent('app:room-status', { bubbles: true, detail: { room: btn.dataset.room, status: next } }));
|
|
132
|
+
recount();
|
|
133
|
+
}));
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
</script>
|
|
137
|
+
|
|
138
|
+
<style>
|
|
139
|
+
.hk { container-type: inline-size; display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--sp-4, 1rem); min-width: 0; }
|
|
140
|
+
.hk--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); }
|
|
141
|
+
.hk__head { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--sp-3, 0.75rem); }
|
|
142
|
+
.hk__title { margin: 0; font-size: 1rem; font-weight: 700; color: var(--c-text-1, #fff); }
|
|
143
|
+
.hk__filters { display: flex; gap: 6px; min-width: 0; max-width: 100%; overflow-x: auto; scrollbar-width: none; }
|
|
144
|
+
.hk__filter { display: inline-flex; align-items: center; gap: 6px; min-height: 32px; padding: 0 0.7rem; border-radius: 999px; border: 1px solid var(--c-border, rgba(255, 255, 255, 0.1)); background: transparent; color: var(--c-text-2, #a3a3a3); font: inherit; font-size: 0.76rem; font-weight: 600; white-space: nowrap; cursor: pointer; }
|
|
145
|
+
.hk__filter i { width: 8px; height: 8px; border-radius: 50%; background: var(--tone); }
|
|
146
|
+
.hk__filter span { color: var(--c-text-3, #737373); font-variant-numeric: tabular-nums; }
|
|
147
|
+
.hk__filter[aria-pressed='true'] { background: var(--c-text-1, #fff); color: var(--c-bg, #050505); border-color: var(--c-text-1, #fff); }
|
|
148
|
+
.hk__filter[aria-pressed='true'] span { color: inherit; opacity: 0.7; }
|
|
149
|
+
|
|
150
|
+
.hk__floor[hidden] { display: none; }
|
|
151
|
+
.hk__floor-title { margin: 0 0 var(--sp-2, 0.5rem); font-size: 0.72rem; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; color: var(--c-text-3, #737373); }
|
|
152
|
+
.hk__grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: var(--sp-2, 0.5rem); margin: 0; padding: 0; list-style: none; }
|
|
153
|
+
.hk--sm .hk__grid { grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); }
|
|
154
|
+
.hk__grid li[hidden] { display: none; }
|
|
155
|
+
.hk__room {
|
|
156
|
+
--tone: var(--c-text-3, #737373);
|
|
157
|
+
display: grid;
|
|
158
|
+
gap: 2px;
|
|
159
|
+
width: 100%;
|
|
160
|
+
min-height: 96px;
|
|
161
|
+
padding: var(--sp-3, 0.75rem);
|
|
162
|
+
border: 1px solid color-mix(in srgb, var(--tone) 35%, transparent);
|
|
163
|
+
border-top: 3px solid var(--tone);
|
|
164
|
+
border-radius: 10px;
|
|
165
|
+
background: color-mix(in srgb, var(--tone) 7%, var(--c-bg-1, #0a0a0a));
|
|
166
|
+
color: var(--c-text-1, #fff);
|
|
167
|
+
font: inherit;
|
|
168
|
+
text-align: left;
|
|
169
|
+
cursor: pointer;
|
|
170
|
+
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
|
171
|
+
}
|
|
172
|
+
.hk--sm .hk__room { min-height: 76px; }
|
|
173
|
+
.hk__room:hover { transform: translateY(-2px); box-shadow: 0 10px 24px -14px color-mix(in srgb, var(--tone) 70%, transparent); }
|
|
174
|
+
.hk__room:focus-visible { outline: 2px solid var(--app-accent, var(--c-primary-light, #d4a017)); outline-offset: 2px; }
|
|
175
|
+
.hk__num { font-size: 1.2rem; font-weight: 800; letter-spacing: -0.02em; font-variant-numeric: tabular-nums; }
|
|
176
|
+
.hk__type { font-size: 0.72rem; color: var(--c-text-3, #737373); }
|
|
177
|
+
.hk__state { display: inline-flex; align-items: center; gap: 6px; margin-top: 4px; font-size: 0.75rem; font-weight: 700; color: var(--tone); }
|
|
178
|
+
.hk__state i { width: 7px; height: 7px; border-radius: 50%; background: currentColor; }
|
|
179
|
+
.hk__meta { font-size: 0.72rem; color: var(--c-text-2, #a3a3a3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
180
|
+
.hk__room--clean, .hk__filter--clean { --tone: var(--c-success, #34d399); }
|
|
181
|
+
.hk__room--dirty, .hk__filter--dirty { --tone: var(--c-danger, #f87171); }
|
|
182
|
+
.hk__room--inspecting, .hk__filter--inspecting { --tone: var(--c-warning, #fbbf24); }
|
|
183
|
+
.hk__room--occupied, .hk__filter--occupied { --tone: var(--app-accent, var(--c-primary-light, #d4a017)); }
|
|
184
|
+
.hk__room--ooo, .hk__filter--ooo { --tone: var(--c-text-3, #737373); }
|
|
185
|
+
.hk__room--ooo { background: repeating-linear-gradient(135deg, rgb(var(--ink-rgb, 255 255 255) / 0.04) 0 8px, transparent 8px 16px), var(--c-bg-1, #0a0a0a); }
|
|
186
|
+
.hk__filter:focus-visible { outline: 2px solid var(--app-accent, var(--c-primary-light, #d4a017)); outline-offset: 2px; }
|
|
187
|
+
.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
|
|
188
|
+
@media (prefers-reduced-motion: reduce) { .hk__room { transition: none; } }
|
|
189
|
+
</style>
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* PageHeader — App page heading: breadcrumb, title, description, period switcher and action
|
|
4
|
+
* buttons, with optional underline tabs below.
|
|
5
|
+
*
|
|
6
|
+
* @param {string} title - Page title. Default 'Dashboard'.
|
|
7
|
+
* @param {string} description - Supporting text.
|
|
8
|
+
* @param {Array<{label: string, href?: string}>} breadcrumb - Trail (optional).
|
|
9
|
+
* @param {Array<{label: string, href?: string, icon?: string, primary?: boolean}>} actions - Buttons on the right.
|
|
10
|
+
* @param {string[]} periods - Segmented period options (e.g. ['Today','7d','30d']); empty hides it.
|
|
11
|
+
* @param {number} activePeriod - Selected period index. Default 0.
|
|
12
|
+
* @param {Array<{label: string, href: string, count?: number}>} tabs - Underline tabs (optional).
|
|
13
|
+
* @param {string} currentTab - href of the active tab.
|
|
14
|
+
* @param {string} size - Title scale (sm|md|lg). Default 'md'.
|
|
15
|
+
* @param {string} className - Extra class.
|
|
16
|
+
*
|
|
17
|
+
* Accessibility: h1 title; the period switcher is a radio group (arrow keys) that dispatches
|
|
18
|
+
* `app:period` with `{ index, label }`; tabs are links with aria-current.
|
|
19
|
+
*/
|
|
20
|
+
import { appIcon } from './app-icons';
|
|
21
|
+
|
|
22
|
+
interface Props {
|
|
23
|
+
title?: string;
|
|
24
|
+
description?: string;
|
|
25
|
+
breadcrumb?: Array<{ label: string; href?: string }>;
|
|
26
|
+
actions?: Array<{ label: string; href?: string; icon?: string; primary?: boolean }>;
|
|
27
|
+
periods?: string[];
|
|
28
|
+
activePeriod?: number;
|
|
29
|
+
tabs?: Array<{ label: string; href: string; count?: number }>;
|
|
30
|
+
currentTab?: string;
|
|
31
|
+
size?: 'sm' | 'md' | 'lg';
|
|
32
|
+
className?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const {
|
|
36
|
+
title = 'Dashboard',
|
|
37
|
+
description = 'Good morning, Lucía — here is what is happening at Hotel Mirador today.',
|
|
38
|
+
breadcrumb = [],
|
|
39
|
+
actions = [{ label: 'Export', icon: 'download' }, { label: 'New booking', icon: 'plus', primary: true, href: '#' }],
|
|
40
|
+
periods = ['Today', '7 days', '30 days', 'Year'],
|
|
41
|
+
activePeriod = 2,
|
|
42
|
+
tabs = [],
|
|
43
|
+
currentTab,
|
|
44
|
+
size = 'md',
|
|
45
|
+
className = '',
|
|
46
|
+
} = Astro.props;
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
<div class:list={['app-ph', `app-ph--${size}`, className]}>
|
|
50
|
+
{breadcrumb.length > 0 && (
|
|
51
|
+
<nav class="app-ph__crumbs" aria-label="Breadcrumb">
|
|
52
|
+
<ol>{breadcrumb.map((b, i) => <li>{b.href && i < breadcrumb.length - 1 ? <a href={b.href}>{b.label}</a> : <span aria-current="page">{b.label}</span>}</li>)}</ol>
|
|
53
|
+
</nav>
|
|
54
|
+
)}
|
|
55
|
+
<div class="app-ph__row">
|
|
56
|
+
<div class="app-ph__text">
|
|
57
|
+
<h1 class="app-ph__title">{title}</h1>
|
|
58
|
+
{description && <p class="app-ph__desc">{description}</p>}
|
|
59
|
+
</div>
|
|
60
|
+
<div class="app-ph__tools">
|
|
61
|
+
{periods.length > 0 && (
|
|
62
|
+
<div class="app-ph__periods" role="radiogroup" aria-label="Period" data-periods>
|
|
63
|
+
{periods.map((p, i) => (
|
|
64
|
+
<button type="button" role="radio" aria-checked={i === activePeriod ? 'true' : 'false'} tabindex={i === activePeriod ? '0' : '-1'} data-i={i}>{p}</button>
|
|
65
|
+
))}
|
|
66
|
+
</div>
|
|
67
|
+
)}
|
|
68
|
+
{actions.map((a) => {
|
|
69
|
+
const Tag = a.href ? 'a' : 'button';
|
|
70
|
+
return (
|
|
71
|
+
<Tag class:list={['app-ph__btn', { 'app-ph__btn--primary': a.primary }]} href={a.href} type={a.href ? undefined : 'button'}>
|
|
72
|
+
{a.icon && <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><Fragment set:html={appIcon(a.icon)} /></svg>}
|
|
73
|
+
{a.label}
|
|
74
|
+
</Tag>
|
|
75
|
+
);
|
|
76
|
+
})}
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
{tabs.length > 0 && (
|
|
80
|
+
<nav class="app-ph__tabs" aria-label="Sections">
|
|
81
|
+
{tabs.map((tab) => (
|
|
82
|
+
<a href={tab.href} aria-current={tab.href === currentTab ? 'page' : undefined}>
|
|
83
|
+
{tab.label}{tab.count !== undefined && <span>{tab.count}</span>}
|
|
84
|
+
</a>
|
|
85
|
+
))}
|
|
86
|
+
</nav>
|
|
87
|
+
)}
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
<script>
|
|
91
|
+
document.querySelectorAll<HTMLElement>('[data-periods]:not([data-bound])').forEach((group) => {
|
|
92
|
+
group.dataset.bound = '';
|
|
93
|
+
const radios = Array.from(group.querySelectorAll<HTMLButtonElement>('[role="radio"]'));
|
|
94
|
+
const select = (i: number, focus = false) => {
|
|
95
|
+
radios.forEach((r, n) => { r.setAttribute('aria-checked', String(n === i)); r.tabIndex = n === i ? 0 : -1; });
|
|
96
|
+
if (focus) radios[i].focus();
|
|
97
|
+
group.dispatchEvent(new CustomEvent('app:period', { bubbles: true, detail: { index: i, label: radios[i].textContent } }));
|
|
98
|
+
};
|
|
99
|
+
radios.forEach((r, i) => {
|
|
100
|
+
r.addEventListener('click', () => select(i));
|
|
101
|
+
r.addEventListener('keydown', (e) => {
|
|
102
|
+
const d = e.key === 'ArrowRight' || e.key === 'ArrowDown' ? 1 : e.key === 'ArrowLeft' || e.key === 'ArrowUp' ? -1 : 0;
|
|
103
|
+
if (!d) return;
|
|
104
|
+
e.preventDefault();
|
|
105
|
+
select((i + d + radios.length) % radios.length, true);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
</script>
|
|
110
|
+
|
|
111
|
+
<style>
|
|
112
|
+
.app-ph { container-type: inline-size; display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--sp-4, 1rem); }
|
|
113
|
+
.app-ph__crumbs ol { display: flex; flex-wrap: wrap; gap: 6px; margin: 0; padding: 0; list-style: none; font-size: 0.78rem; color: var(--c-text-3, #737373); }
|
|
114
|
+
.app-ph__crumbs li + li::before { content: '/'; margin-right: 6px; opacity: 0.6; }
|
|
115
|
+
.app-ph__crumbs a { color: inherit; text-decoration: none; }
|
|
116
|
+
.app-ph__crumbs a:hover, .app-ph__crumbs [aria-current] { color: var(--c-text-1, #fff); }
|
|
117
|
+
.app-ph__row { display: flex; align-items: flex-end; justify-content: space-between; gap: var(--sp-4, 1rem); }
|
|
118
|
+
.app-ph__title { margin: 0; font-size: 1.6rem; font-weight: 800; letter-spacing: -0.025em; color: var(--c-text-1, #fff); }
|
|
119
|
+
.app-ph--sm .app-ph__title { font-size: 1.25rem; }
|
|
120
|
+
.app-ph--lg .app-ph__title { font-size: 2.1rem; }
|
|
121
|
+
.app-ph__desc { margin: 4px 0 0; font-size: 0.9rem; color: var(--c-text-2, #a3a3a3); }
|
|
122
|
+
.app-ph__tools { display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: var(--sp-2, 0.5rem); }
|
|
123
|
+
.app-ph__periods { display: inline-flex; padding: 3px; border-radius: 10px; border: 1px solid var(--c-border, rgba(255, 255, 255, 0.08)); background: rgb(var(--ink-rgb, 255 255 255) / 0.03); }
|
|
124
|
+
.app-ph__periods button { min-height: 34px; padding: 0 0.75rem; border: 0; border-radius: 7px; background: transparent; color: var(--c-text-2, #a3a3a3); font: inherit; font-size: 0.8rem; font-weight: 600; cursor: pointer; }
|
|
125
|
+
.app-ph__periods [aria-checked='true'] { background: var(--c-bg-1, #0a0a0a); color: var(--c-text-1, #fff); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2), 0 0 0 1px var(--c-border, rgba(255, 255, 255, 0.08)); }
|
|
126
|
+
.app-ph__btn {
|
|
127
|
+
display: inline-flex;
|
|
128
|
+
align-items: center;
|
|
129
|
+
gap: 6px;
|
|
130
|
+
min-height: 40px;
|
|
131
|
+
padding: 0 0.95rem;
|
|
132
|
+
border-radius: 10px;
|
|
133
|
+
border: 1px solid var(--c-border, rgba(255, 255, 255, 0.1));
|
|
134
|
+
background: var(--c-bg-1, #0a0a0a);
|
|
135
|
+
color: var(--c-text-1, #fff);
|
|
136
|
+
font: inherit;
|
|
137
|
+
font-size: 0.85rem;
|
|
138
|
+
font-weight: 600;
|
|
139
|
+
text-decoration: none;
|
|
140
|
+
cursor: pointer;
|
|
141
|
+
transition: background 0.15s ease, filter 0.15s ease;
|
|
142
|
+
}
|
|
143
|
+
.app-ph__btn:hover { background: rgb(var(--ink-rgb, 255 255 255) / 0.06); color: var(--c-text-1, #fff); }
|
|
144
|
+
.app-ph__btn--primary { border-color: transparent; background: var(--app-accent, var(--c-primary-light, #d4a017)); color: var(--c-on-primary, #050505); }
|
|
145
|
+
.app-ph__btn--primary:hover { background: var(--app-accent, var(--c-primary-light, #d4a017)); color: var(--c-on-primary, #050505); filter: brightness(1.08); }
|
|
146
|
+
.app-ph :is(a, button):focus-visible { outline: 2px solid var(--app-accent, var(--c-primary-light, #d4a017)); outline-offset: 2px; }
|
|
147
|
+
.app-ph__tabs { display: flex; gap: var(--sp-5, 1.25rem); border-bottom: 1px solid var(--c-border, rgba(255, 255, 255, 0.08)); overflow-x: auto; scrollbar-width: none; }
|
|
148
|
+
.app-ph__tabs a { display: inline-flex; align-items: center; gap: 6px; min-height: 42px; border-bottom: 2px solid transparent; color: var(--c-text-2, #a3a3a3); font-size: 0.85rem; font-weight: 600; text-decoration: none; white-space: nowrap; }
|
|
149
|
+
.app-ph__tabs a:hover { color: var(--c-text-1, #fff); }
|
|
150
|
+
.app-ph__tabs a[aria-current] { color: var(--c-text-1, #fff); border-bottom-color: var(--app-accent, var(--c-primary-light, #d4a017)); }
|
|
151
|
+
.app-ph__tabs span { padding: 0 6px; border-radius: 999px; background: rgb(var(--ink-rgb, 255 255 255) / 0.08); font-size: 0.7rem; }
|
|
152
|
+
|
|
153
|
+
@container (max-width: 720px) {
|
|
154
|
+
.app-ph__row { flex-direction: column; align-items: stretch; }
|
|
155
|
+
.app-ph__tools { justify-content: flex-start; }
|
|
156
|
+
}
|
|
157
|
+
</style>
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* PlanUsageCard — Billing summary: current plan and price, renewal date, usage meters with
|
|
4
|
+
* warning thresholds, payment method and upgrade / manage actions.
|
|
5
|
+
*
|
|
6
|
+
* @param {string} plan - Plan name. Default 'Pro'.
|
|
7
|
+
* @param {string} price - Price label. Default '€149 / month'.
|
|
8
|
+
* @param {string} renews - Renewal text. Default 'Renews on Dec 1, 2026'.
|
|
9
|
+
* @param {Array<{label: string, used: number, limit: number, unit?: string}>} usage - Meters.
|
|
10
|
+
* @param {string} card - Payment method label (optional). Default 'Visa •••• 4242'.
|
|
11
|
+
* @param {{label: string, href: string}} upgrade - Primary action. Default upgrade.
|
|
12
|
+
* @param {{label: string, href: string}} manage - Secondary action (optional).
|
|
13
|
+
* @param {number} warnAt - Percentage at which a meter turns amber. Default 80.
|
|
14
|
+
* @param {string} variant - Style (card|accent). Default 'card'.
|
|
15
|
+
* @param {string} className - Extra class.
|
|
16
|
+
*
|
|
17
|
+
* Accessibility: each meter is a role="meter" with aria-valuenow/min/max and a text value,
|
|
18
|
+
* so usage is never conveyed by colour alone.
|
|
19
|
+
*/
|
|
20
|
+
interface Props {
|
|
21
|
+
plan?: string;
|
|
22
|
+
price?: string;
|
|
23
|
+
renews?: string;
|
|
24
|
+
usage?: Array<{ label: string; used: number; limit: number; unit?: string }>;
|
|
25
|
+
card?: string;
|
|
26
|
+
upgrade?: { label: string; href: string };
|
|
27
|
+
manage?: { label: string; href: string };
|
|
28
|
+
warnAt?: number;
|
|
29
|
+
variant?: 'card' | 'accent';
|
|
30
|
+
className?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const {
|
|
34
|
+
plan = 'Pro',
|
|
35
|
+
price = '€149 / month',
|
|
36
|
+
renews = 'Renews on Dec 1, 2026',
|
|
37
|
+
usage = [
|
|
38
|
+
{ label: 'Rooms', used: 42, limit: 50 },
|
|
39
|
+
{ label: 'Team seats', used: 4, limit: 10 },
|
|
40
|
+
{ label: 'Channel connections', used: 3, limit: 5 },
|
|
41
|
+
{ label: 'Guest emails', used: 8200, limit: 10000, unit: '/ month' },
|
|
42
|
+
],
|
|
43
|
+
card = 'Visa •••• 4242',
|
|
44
|
+
upgrade = { label: 'Upgrade to Business', href: '#' },
|
|
45
|
+
manage = { label: 'Manage billing', href: '#' },
|
|
46
|
+
warnAt = 80,
|
|
47
|
+
variant = 'card',
|
|
48
|
+
className = '',
|
|
49
|
+
} = Astro.props;
|
|
50
|
+
|
|
51
|
+
const fmt = (n: number) => new Intl.NumberFormat('en-GB').format(n);
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
<section class:list={['plan', `plan--${variant}`, className]} aria-labelledby="plan-title">
|
|
55
|
+
<header class="plan__head">
|
|
56
|
+
<div>
|
|
57
|
+
<p class="plan__eyebrow">Current plan</p>
|
|
58
|
+
<h2 class="plan__title" id="plan-title">{plan} <span>{price}</span></h2>
|
|
59
|
+
<p class="plan__renew">{renews}</p>
|
|
60
|
+
</div>
|
|
61
|
+
{card && <p class="plan__card"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2" y="5" width="20" height="14" rx="2"/><path d="M2 10h20"/></svg>{card}</p>}
|
|
62
|
+
</header>
|
|
63
|
+
<ul class="plan__meters">
|
|
64
|
+
{usage.map((u) => {
|
|
65
|
+
const pct = Math.min(100, Math.round((u.used / u.limit) * 100));
|
|
66
|
+
const tone = pct >= 100 ? 'danger' : pct >= warnAt ? 'warning' : 'ok';
|
|
67
|
+
return (
|
|
68
|
+
<li>
|
|
69
|
+
<div class="plan__meter-head">
|
|
70
|
+
<span>{u.label}</span>
|
|
71
|
+
<strong>{fmt(u.used)} <small>of {fmt(u.limit)}{u.unit ? ` ${u.unit}` : ''}</small></strong>
|
|
72
|
+
</div>
|
|
73
|
+
<div class:list={['plan__bar', `plan__bar--${tone}`]} role="meter" aria-label={u.label} aria-valuemin="0" aria-valuemax={u.limit} aria-valuenow={u.used} aria-valuetext={`${fmt(u.used)} of ${fmt(u.limit)}, ${pct}%`}>
|
|
74
|
+
<span style={`width: ${pct}%`}></span>
|
|
75
|
+
</div>
|
|
76
|
+
{tone === 'warning' && <p class="plan__warn">{pct}% used — consider upgrading soon.</p>}
|
|
77
|
+
</li>
|
|
78
|
+
);
|
|
79
|
+
})}
|
|
80
|
+
</ul>
|
|
81
|
+
<div class="plan__actions">
|
|
82
|
+
{upgrade && <a class="plan__btn plan__btn--primary" href={upgrade.href}>{upgrade.label}</a>}
|
|
83
|
+
{manage && <a class="plan__btn" href={manage.href}>{manage.label}</a>}
|
|
84
|
+
</div>
|
|
85
|
+
</section>
|
|
86
|
+
|
|
87
|
+
<style>
|
|
88
|
+
.plan { container-type: inline-size; display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--sp-5, 1.25rem); padding: var(--sp-6, 1.5rem); border-radius: var(--r-lg, 16px); border: 1px solid var(--c-border, rgba(255, 255, 255, 0.08)); background: var(--c-bg-1, #0a0a0a); }
|
|
89
|
+
.plan--accent { background: radial-gradient(circle at 100% 0%, color-mix(in srgb, var(--app-accent, var(--c-primary-light, #d4a017)) 22%, transparent), transparent 55%), var(--c-bg-1, #0a0a0a); }
|
|
90
|
+
.plan__head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--sp-4, 1rem); }
|
|
91
|
+
.plan__eyebrow { margin: 0; font-size: 0.72rem; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; color: var(--app-accent, var(--c-primary-light, #d4a017)); }
|
|
92
|
+
.plan__title { margin: 4px 0 0; font-size: 1.5rem; font-weight: 800; letter-spacing: -0.02em; color: var(--c-text-1, #fff); }
|
|
93
|
+
.plan__title span { font-size: 0.95rem; font-weight: 500; color: var(--c-text-2, #a3a3a3); }
|
|
94
|
+
.plan__renew { margin: 2px 0 0; font-size: 0.8rem; color: var(--c-text-3, #737373); }
|
|
95
|
+
.plan__card { display: inline-flex; align-items: center; gap: 6px; margin: 0; padding: 6px 10px; border-radius: 8px; border: 1px solid var(--c-border, rgba(255, 255, 255, 0.1)); font-size: 0.8rem; color: var(--c-text-1, #fff); white-space: nowrap; }
|
|
96
|
+
.plan__meters { display: grid; gap: var(--sp-4, 1rem); margin: 0; padding: 0; list-style: none; }
|
|
97
|
+
.plan__meter-head { display: flex; justify-content: space-between; gap: var(--sp-2, 0.5rem); margin-bottom: 6px; font-size: 0.85rem; color: var(--c-text-2, #a3a3a3); }
|
|
98
|
+
.plan__meter-head strong { color: var(--c-text-1, #fff); font-variant-numeric: tabular-nums; }
|
|
99
|
+
.plan__meter-head small { font-weight: 500; color: var(--c-text-3, #737373); }
|
|
100
|
+
.plan__bar { height: 8px; border-radius: 999px; background: rgb(var(--ink-rgb, 255 255 255) / 0.08); overflow: hidden; }
|
|
101
|
+
.plan__bar span { display: block; height: 100%; border-radius: inherit; background: var(--app-accent, var(--c-primary-light, #d4a017)); }
|
|
102
|
+
.plan__bar--warning span { background: var(--c-warning, #fbbf24); }
|
|
103
|
+
.plan__bar--danger span { background: var(--c-danger, #f87171); }
|
|
104
|
+
.plan__warn { margin: 4px 0 0; font-size: 0.75rem; color: var(--c-warning, #fbbf24); }
|
|
105
|
+
.plan__actions { display: flex; flex-wrap: wrap; gap: var(--sp-2, 0.5rem); }
|
|
106
|
+
.plan__btn { display: inline-flex; align-items: center; min-height: 40px; padding: 0 1rem; border-radius: 10px; border: 1px solid var(--c-border, rgba(255, 255, 255, 0.12)); color: var(--c-text-1, #fff); font-size: 0.85rem; font-weight: 600; text-decoration: none; }
|
|
107
|
+
.plan__btn:hover { background: rgb(var(--ink-rgb, 255 255 255) / 0.05); color: var(--c-text-1, #fff); }
|
|
108
|
+
.plan__btn--primary { border-color: transparent; background: var(--app-accent, var(--c-primary-light, #d4a017)); color: var(--c-on-primary, #050505); }
|
|
109
|
+
.plan__btn--primary:hover { background: var(--app-accent, var(--c-primary-light, #d4a017)); color: var(--c-on-primary, #050505); filter: brightness(1.08); }
|
|
110
|
+
.plan__btn:focus-visible { outline: 2px solid var(--app-accent, var(--c-primary-light, #d4a017)); outline-offset: 2px; }
|
|
111
|
+
@container (max-width: 420px) { .plan__head { flex-direction: column; } }
|
|
112
|
+
</style>
|