astro-component-kit 0.4.0 → 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.
Files changed (180) hide show
  1. package/README.md +105 -27
  2. package/package.json +16 -3
  3. package/src/components/index.ts +2 -0
  4. package/src/components/lib/Galaxy3D.astro +249 -137
  5. package/src/components/lib/app/AppSidebar.astro +249 -0
  6. package/src/components/lib/app/AppTopbar.astro +284 -0
  7. package/src/components/lib/app/ArrivalsBoard.astro +176 -0
  8. package/src/components/lib/app/AuthCard.astro +213 -0
  9. package/src/components/lib/app/DataTable.astro +346 -0
  10. package/src/components/lib/app/GuestProfileCard.astro +133 -0
  11. package/src/components/lib/app/HousekeepingBoard.astro +189 -0
  12. package/src/components/lib/app/PageHeader.astro +157 -0
  13. package/src/components/lib/app/PlanUsageCard.astro +112 -0
  14. package/src/components/lib/app/RoomRack.astro +207 -0
  15. package/src/components/lib/app/SettingsSection.astro +96 -0
  16. package/src/components/lib/app/SwitchRow.astro +71 -0
  17. package/src/components/lib/app/TeamMembers.astro +190 -0
  18. package/src/components/lib/app/app-icons.ts +52 -0
  19. package/src/components/lib/app/index.ts +19 -0
  20. package/src/components/lib/charts/AreaChart.astro +4 -4
  21. package/src/components/lib/charts/BarChart.astro +3 -3
  22. package/src/components/lib/charts/ChartFrame.astro +10 -10
  23. package/src/components/lib/charts/DonutChart.astro +2 -2
  24. package/src/components/lib/charts/DumbbellChart.astro +1 -1
  25. package/src/components/lib/charts/ForecastLineChart.astro +1 -1
  26. package/src/components/lib/charts/GaugeChart.astro +1 -1
  27. package/src/components/lib/charts/HeatmapChart.astro +1 -1
  28. package/src/components/lib/charts/HorizontalBarChart.astro +2 -2
  29. package/src/components/lib/charts/KpiStatTile.astro +4 -4
  30. package/src/components/lib/charts/LinearProgressGauge.astro +4 -4
  31. package/src/components/lib/charts/MetricDeltaTable.astro +3 -3
  32. package/src/components/lib/charts/RadarMetricChart.astro +4 -4
  33. package/src/components/lib/charts/RadialBarChart.astro +1 -1
  34. package/src/components/lib/charts/RetentionCohortTable.astro +2 -2
  35. package/src/components/lib/charts/TreemapChart.astro +1 -1
  36. package/src/components/lib/ecommerce/CartDrawer.astro +483 -0
  37. package/src/components/lib/ecommerce/CartPageList.astro +290 -0
  38. package/src/components/lib/ecommerce/CartToast.astro +276 -0
  39. package/src/components/lib/ecommerce/CategoryTiles.astro +143 -0
  40. package/src/components/lib/ecommerce/CheckoutForm.astro +423 -0
  41. package/src/components/lib/ecommerce/CountdownBanner.astro +230 -0
  42. package/src/components/lib/ecommerce/FilterBar.astro +278 -0
  43. package/src/components/lib/ecommerce/FilterSidebar.astro +405 -0
  44. package/src/components/lib/ecommerce/LiveActivityToast.astro +213 -0
  45. package/src/components/lib/ecommerce/OrderSummary.astro +295 -0
  46. package/src/components/lib/ecommerce/ProductBuyBox.astro +247 -0
  47. package/src/components/lib/ecommerce/ProductCardMinimal.astro +229 -0
  48. package/src/components/lib/ecommerce/ProductGallery.astro +145 -0
  49. package/src/components/lib/ecommerce/ProductGrid.astro +118 -0
  50. package/src/components/lib/ecommerce/ProductMedia.astro +113 -0
  51. package/src/components/lib/ecommerce/ProductRating.astro +78 -0
  52. package/src/components/lib/ecommerce/ProductToolbar.astro +93 -0
  53. package/src/components/lib/ecommerce/PromoBar.astro +145 -0
  54. package/src/components/lib/ecommerce/StockIndicator.astro +127 -0
  55. package/src/components/lib/ecommerce/StoreHeader.astro +446 -0
  56. package/src/components/lib/ecommerce/StoreHero.astro +260 -0
  57. package/src/components/lib/ecommerce/StoreProductCard.astro +307 -0
  58. package/src/components/lib/ecommerce/TrustBadges.astro +101 -0
  59. package/src/components/lib/ecommerce/VariantPicker.astro +145 -0
  60. package/src/components/lib/ecommerce/cart-store.ts +151 -0
  61. package/src/components/lib/ecommerce/catalog-filter.ts +62 -0
  62. package/src/components/lib/ecommerce/index.ts +34 -0
  63. package/src/components/lib/ecommerce/product-icons.ts +28 -0
  64. package/src/components/lib/ecommerce/types.ts +70 -0
  65. package/src/components/lib/forms/AnimatedInput.astro +4 -2
  66. package/src/components/lib/forms/CustomRadioGroup.astro +3 -1
  67. package/src/components/lib/index.ts +2 -0
  68. package/src/components/lib/layout/BentoGrid3.astro +6 -4
  69. package/src/components/lib/layout/CenterFixedBox.astro +2 -2
  70. package/src/components/lib/layout/CenteredContainer.astro +4 -2
  71. package/src/components/lib/layout/ClusterHorizontal.astro +2 -2
  72. package/src/components/lib/layout/FeatureHero.astro +4 -2
  73. package/src/components/lib/layout/FooterCenteredMinimal.astro +4 -2
  74. package/src/components/lib/layout/FullBleedSection.astro +2 -2
  75. package/src/components/lib/layout/GalleryGrid4.astro +2 -2
  76. package/src/components/lib/layout/HolyGrailGrid.astro +11 -9
  77. package/src/components/lib/layout/LandingSectionSplit.astro +5 -3
  78. package/src/components/lib/layout/MasonryGrid.astro +2 -2
  79. package/src/components/lib/layout/MinimalFooterGrid.astro +1 -1
  80. package/src/components/lib/layout/ScrollSnapSection.astro +1 -1
  81. package/src/components/lib/layout/SidebarLayoutWrapper.astro +4 -2
  82. package/src/components/lib/layout/SplitScreenLayout.astro +3 -3
  83. package/src/components/lib/layout/StepLayoutVertical.astro +1 -1
  84. package/src/components/lib/layout/StickySidebarWrapper.astro +4 -2
  85. package/src/components/lib/sections/AgencyContactBrief.astro +194 -0
  86. package/src/components/lib/sections/AgencyFooterCta.astro +94 -0
  87. package/src/components/lib/sections/AgencyHeader.astro +161 -0
  88. package/src/components/lib/sections/AgencyHeroKinetic.astro +172 -0
  89. package/src/components/lib/sections/AgencyServicesList.astro +113 -0
  90. package/src/components/lib/sections/AgencyTeamGrid.astro +111 -0
  91. package/src/components/lib/sections/AgencyWorkGrid.astro +147 -0
  92. package/src/components/lib/sections/BlogGridSection.astro +9 -7
  93. package/src/components/lib/sections/ButtonsShowcase.astro +20 -18
  94. package/src/components/lib/sections/CardsShowcase.astro +28 -26
  95. package/src/components/lib/sections/CareersOpenings.astro +10 -8
  96. package/src/components/lib/sections/ChangelogSection.astro +14 -7
  97. package/src/components/lib/sections/ContactGlassForm.astro +10 -8
  98. package/src/components/lib/sections/ContactSplitMap.astro +11 -9
  99. package/src/components/lib/sections/CtaAppDownload.astro +13 -11
  100. package/src/components/lib/sections/CtaGlowBanner.astro +12 -10
  101. package/src/components/lib/sections/CtaNewsletterSection.astro +13 -11
  102. package/src/components/lib/sections/CtaSplitImage.astro +13 -11
  103. package/src/components/lib/sections/DataShowcase.astro +23 -21
  104. package/src/components/lib/sections/FaqAccordionSection.astro +8 -8
  105. package/src/components/lib/sections/FaqTwoColumn.astro +9 -7
  106. package/src/components/lib/sections/FeatureAlternatingRows.astro +10 -8
  107. package/src/components/lib/sections/FeatureBentoGrid.astro +13 -11
  108. package/src/components/lib/sections/FeatureIconGrid.astro +6 -6
  109. package/src/components/lib/sections/FeatureTabsShowcase.astro +13 -11
  110. package/src/components/lib/sections/FeatureTimelineFlow.astro +8 -6
  111. package/src/components/lib/sections/FeedbackShowcase.astro +15 -13
  112. package/src/components/lib/sections/FooterMegaSection.astro +13 -11
  113. package/src/components/lib/sections/FormsShowcase.astro +22 -20
  114. package/src/components/lib/sections/HeroDashboardPreview.astro +11 -9
  115. package/src/components/lib/sections/HeroGlass.astro +22 -20
  116. package/src/components/lib/sections/HeroGradient.astro +15 -13
  117. package/src/components/lib/sections/HeroMinimalCentered.astro +10 -10
  118. package/src/components/lib/sections/HeroShowcaseSplit.astro +18 -16
  119. package/src/components/lib/sections/HeroSplit.astro +13 -11
  120. package/src/components/lib/sections/HeroTerminal.astro +14 -12
  121. package/src/components/lib/sections/HeroWaitlist.astro +10 -8
  122. package/src/components/lib/sections/HowItWorksSteps.astro +12 -10
  123. package/src/components/lib/sections/IntegrationsGrid.astro +7 -7
  124. package/src/components/lib/sections/LayoutShowcase.astro +21 -19
  125. package/src/components/lib/sections/LogoCloudSection.astro +4 -4
  126. package/src/components/lib/sections/LogoWallGrid.astro +6 -4
  127. package/src/components/lib/sections/NavigationShowcase.astro +15 -13
  128. package/src/components/lib/sections/OverlaysShowcase.astro +21 -19
  129. package/src/components/lib/sections/PricingComparisonMatrix.astro +15 -13
  130. package/src/components/lib/sections/PricingSinglePlan.astro +13 -11
  131. package/src/components/lib/sections/PricingTierCards.astro +11 -9
  132. package/src/components/lib/sections/PricingToggleCards.astro +13 -13
  133. package/src/components/lib/sections/ShopCategoryHero.astro +103 -0
  134. package/src/components/lib/sections/ShopCollectionMosaic.astro +131 -0
  135. package/src/components/lib/sections/ShopFooterWordmark.astro +100 -0
  136. package/src/components/lib/sections/ShopHeaderEditorial.astro +396 -0
  137. package/src/components/lib/sections/ShopHeroEditorial.astro +151 -0
  138. package/src/components/lib/sections/ShopLookbook.astro +250 -0
  139. package/src/components/lib/sections/ShopNewsletterEditorial.astro +128 -0
  140. package/src/components/lib/sections/ShopProductRail.astro +150 -0
  141. package/src/components/lib/sections/ShopReviewsEditorial.astro +133 -0
  142. package/src/components/lib/sections/ShopValuesMarquee.astro +91 -0
  143. package/src/components/lib/sections/StatsCounterBand.astro +9 -7
  144. package/src/components/lib/sections/StatsDashboardSection.astro +4 -2
  145. package/src/components/lib/sections/StatsMetricShowcase.astro +9 -7
  146. package/src/components/lib/sections/TeamGridSection.astro +12 -10
  147. package/src/components/lib/sections/TestimonialCarousel.astro +10 -8
  148. package/src/components/lib/sections/TestimonialFeatured.astro +7 -5
  149. package/src/components/lib/sections/TestimonialGridSection.astro +7 -7
  150. package/src/components/lib/sections/TestimonialWallGlass.astro +11 -9
  151. package/src/components/lib/sections/TypographyShowcase.astro +10 -8
  152. package/src/components/lib/sections/index.ts +10 -0
  153. package/src/components/lib/templates/ecommerce/StoreCategoryPage.astro +160 -0
  154. package/src/components/lib/templates/ecommerce/StoreCheckoutPage.astro +98 -0
  155. package/src/components/lib/templates/ecommerce/StoreHomePage.astro +191 -0
  156. package/src/components/lib/templates/ecommerce/StoreLayout.astro +169 -0
  157. package/src/components/lib/templates/ecommerce/StoreProductPage.astro +165 -0
  158. package/src/components/lib/templates/ecommerce/data.ts +187 -0
  159. package/src/components/lib/templates/ecommerce/index.ts +14 -0
  160. package/src/components/lib/templates/ecommerce-editorial/EditorialBagPage.astro +52 -0
  161. package/src/components/lib/templates/ecommerce-editorial/EditorialCheckoutPage.astro +123 -0
  162. package/src/components/lib/templates/ecommerce-editorial/EditorialHomePage.astro +102 -0
  163. package/src/components/lib/templates/ecommerce-editorial/EditorialLayout.astro +216 -0
  164. package/src/components/lib/templates/ecommerce-editorial/EditorialProductPage.astro +205 -0
  165. package/src/components/lib/templates/ecommerce-editorial/EditorialShopPage.astro +112 -0
  166. package/src/components/lib/templates/ecommerce-editorial/data.ts +125 -0
  167. package/src/components/lib/templates/ecommerce-editorial/index.ts +16 -0
  168. package/src/components/lib/templates/saas-hotel/AppLayout.astro +83 -0
  169. package/src/components/lib/templates/saas-hotel/DashboardPage.astro +66 -0
  170. package/src/components/lib/templates/saas-hotel/GuestsPage.astro +65 -0
  171. package/src/components/lib/templates/saas-hotel/LandingPage.astro +213 -0
  172. package/src/components/lib/templates/saas-hotel/ReportsPage.astro +63 -0
  173. package/src/components/lib/templates/saas-hotel/ReservationsPage.astro +49 -0
  174. package/src/components/lib/templates/saas-hotel/RoomsPage.astro +48 -0
  175. package/src/components/lib/templates/saas-hotel/SettingsPage.astro +115 -0
  176. package/src/components/lib/templates/saas-hotel/SignInPage.astro +50 -0
  177. package/src/components/lib/templates/saas-hotel/data.ts +229 -0
  178. package/src/components/lib/templates/saas-hotel/index.ts +19 -0
  179. package/src/components/lib/templates/saas-hotel/lobby-theme.css +90 -0
  180. 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>