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,249 @@
1
+ ---
2
+ /**
3
+ * AppSidebar — SaaS app navigation: workspace switcher, grouped nav items with icons and
4
+ * count badges, collapsible to an icon rail, and an upgrade/plan card at the bottom.
5
+ *
6
+ * @param {{name: string, plan?: string, initials?: string}} workspace - Workspace shown at the top.
7
+ * @param {Array<{label?: string, items: {label: string, href: string, icon?: string, badge?: string|number}[]}>} groups - Nav groups.
8
+ * @param {string} currentHref - Marks the matching item with aria-current.
9
+ * @param {{title: string, text?: string, href: string, cta?: string, progress?: number}} promo - Card at the bottom (optional).
10
+ * @param {boolean} fullHeight - Full-viewport sticky sidebar (app shell). false renders it inline (docs, cards). Default true.
11
+ * @param {boolean} collapsible - Show the collapse toggle (remembered in localStorage). Default true.
12
+ * @param {string} variant - Style (surface|transparent|accent). Default 'surface'.
13
+ * @param {string} size - Width preset (sm|md|lg). Default 'md'.
14
+ * @param {string} id - Element id (AppTopbar's menu button opens it on mobile). Default 'app-sidebar'.
15
+ * @param {string} className - Extra class.
16
+ *
17
+ * Accessibility: a labelled nav landmark; the current page uses aria-current; when collapsed,
18
+ * labels stay available to screen readers and appear as tooltips on hover/focus.
19
+ */
20
+ import { appIcon } from './app-icons';
21
+
22
+ interface Item { label: string; href: string; icon?: string; badge?: string | number }
23
+ interface Group { label?: string; items: Item[] }
24
+ interface Props {
25
+ workspace?: { name: string; plan?: string; initials?: string };
26
+ groups?: Group[];
27
+ currentHref?: string;
28
+ promo?: { title: string; text?: string; href: string; cta?: string; progress?: number };
29
+ fullHeight?: boolean;
30
+ collapsible?: boolean;
31
+ variant?: 'surface' | 'transparent' | 'accent';
32
+ size?: 'sm' | 'md' | 'lg';
33
+ id?: string;
34
+ className?: string;
35
+ }
36
+
37
+ const {
38
+ workspace = { name: 'Hotel Mirador', plan: 'Pro plan', initials: 'HM' },
39
+ groups = [
40
+ { items: [
41
+ { label: 'Dashboard', href: '#', icon: 'dashboard' },
42
+ { label: 'Reservations', href: '#reservations', icon: 'calendar', badge: 12 },
43
+ { label: 'Rooms', href: '#rooms', icon: 'bed' },
44
+ { label: 'Guests', href: '#guests', icon: 'users' },
45
+ ] },
46
+ { label: 'Insights', items: [
47
+ { label: 'Reports', href: '#reports', icon: 'chart' },
48
+ { label: 'Revenue', href: '#revenue', icon: 'wallet' },
49
+ ] },
50
+ { label: 'Account', items: [{ label: 'Settings', href: '#settings', icon: 'settings' }] },
51
+ ],
52
+ currentHref = '#',
53
+ promo = { title: 'Channel manager', text: '3 of 5 channels connected', href: '#', cta: 'Connect more', progress: 60 },
54
+ fullHeight = true,
55
+ collapsible = true,
56
+ variant = 'surface',
57
+ size = 'md',
58
+ id = 'app-sidebar',
59
+ className = '',
60
+ } = Astro.props;
61
+ ---
62
+
63
+ <aside class:list={['app-sb', `app-sb--${variant}`, `app-sb--${size}`, { 'app-sb--inline': !fullHeight }, className]} id={id} data-app-sidebar aria-label="Application">
64
+ <div class="app-sb__top">
65
+ <a class="app-sb__ws" href={groups[0]?.items[0]?.href ?? '#'}>
66
+ <span class="app-sb__ws-logo" aria-hidden="true">{workspace.initials ?? workspace.name.slice(0, 2).toUpperCase()}</span>
67
+ <span class="app-sb__ws-text app-sb__label">
68
+ <strong>{workspace.name}</strong>
69
+ {workspace.plan && <small>{workspace.plan}</small>}
70
+ </span>
71
+ </a>
72
+ {collapsible && (
73
+ <button class="app-sb__collapse" type="button" aria-label="Collapse sidebar" aria-pressed="false" data-collapse>
74
+ <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="3" y="3" width="18" height="18" rx="2"/><path d="M9 3v18"/></svg>
75
+ </button>
76
+ )}
77
+ </div>
78
+
79
+ <nav class="app-sb__nav" aria-label="Main">
80
+ {groups.map((g) => (
81
+ <div class="app-sb__group">
82
+ {g.label && <p class="app-sb__group-label app-sb__label">{g.label}</p>}
83
+ <ul>
84
+ {g.items.map((item) => {
85
+ const active = item.href === currentHref;
86
+ return (
87
+ <li>
88
+ <a class:list={['app-sb__link', { 'is-active': active }]} href={item.href} aria-current={active ? 'page' : undefined} data-tip={item.label}>
89
+ <span class="app-sb__icon" aria-hidden="true"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><Fragment set:html={appIcon(item.icon)} /></svg></span>
90
+ <span class="app-sb__text app-sb__label">{item.label}</span>
91
+ {item.badge !== undefined && <span class="app-sb__badge app-sb__label">{item.badge}</span>}
92
+ </a>
93
+ </li>
94
+ );
95
+ })}
96
+ </ul>
97
+ </div>
98
+ ))}
99
+ </nav>
100
+
101
+ {promo && (
102
+ <a class="app-sb__promo app-sb__label" href={promo.href}>
103
+ <strong>{promo.title}</strong>
104
+ {promo.text && <span>{promo.text}</span>}
105
+ {promo.progress !== undefined && <span class="app-sb__meter" aria-hidden="true"><i style={`width: ${promo.progress}%`}></i></span>}
106
+ {promo.cta && <em>{promo.cta} →</em>}
107
+ </a>
108
+ )}
109
+ </aside>
110
+ <div class="app-sb-backdrop" data-sidebar-backdrop hidden></div>
111
+
112
+ <script>
113
+ document.querySelectorAll<HTMLElement>('[data-app-sidebar]:not([data-bound])').forEach((sb) => {
114
+ sb.dataset.bound = '';
115
+ const btn = sb.querySelector<HTMLButtonElement>('[data-collapse]');
116
+ const key = `${sb.id}:collapsed`;
117
+ const set = (on: boolean) => {
118
+ sb.classList.toggle('is-collapsed', on);
119
+ btn?.setAttribute('aria-pressed', String(on));
120
+ btn?.setAttribute('aria-label', on ? 'Expand sidebar' : 'Collapse sidebar');
121
+ };
122
+ try { set(window.localStorage.getItem(key) === '1'); } catch { /* ignore */ }
123
+ btn?.addEventListener('click', () => {
124
+ const on = !sb.classList.contains('is-collapsed');
125
+ set(on);
126
+ try { window.localStorage.setItem(key, on ? '1' : '0'); } catch { /* ignore */ }
127
+ });
128
+
129
+ // Mobile drawer: opened by any [data-sidebar-open="<id>"] button (AppTopbar's menu button).
130
+ const backdrop = sb.nextElementSibling as HTMLElement | null;
131
+ const open = (v: boolean) => {
132
+ sb.classList.toggle('is-open', v);
133
+ if (backdrop) backdrop.hidden = !v;
134
+ document.querySelectorAll(`[data-sidebar-open="${sb.id}"]`).forEach((b) => b.setAttribute('aria-expanded', String(v)));
135
+ if (v) sb.querySelector<HTMLElement>('a, button')?.focus();
136
+ };
137
+ document.querySelectorAll<HTMLElement>(`[data-sidebar-open="${sb.id}"]`).forEach((b) => b.addEventListener('click', () => open(!sb.classList.contains('is-open'))));
138
+ backdrop?.addEventListener('click', () => open(false));
139
+ document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && sb.classList.contains('is-open')) open(false); });
140
+ });
141
+ </script>
142
+
143
+ <style>
144
+ .app-sb {
145
+ --sb-w: 252px;
146
+ position: sticky;
147
+ top: 0;
148
+ display: flex;
149
+ flex-direction: column;
150
+ gap: var(--sp-4, 1rem);
151
+ width: var(--sb-w);
152
+ height: 100dvh;
153
+ padding: var(--sp-4, 1rem) var(--sp-3, 0.75rem);
154
+ border-right: 1px solid var(--c-border, rgba(255, 255, 255, 0.08));
155
+ background: var(--c-bg-1, #0a0a0a);
156
+ overflow-y: auto;
157
+ transition: width 0.25s cubic-bezier(0.16, 1, 0.3, 1);
158
+ z-index: 40;
159
+ }
160
+ .app-sb--inline { position: relative; height: auto; min-height: 520px; border: 1px solid var(--c-border, rgba(255, 255, 255, 0.08)); border-radius: var(--r-lg, 16px); }
161
+ .app-sb--sm { --sb-w: 220px; }
162
+ .app-sb--lg { --sb-w: 280px; }
163
+ .app-sb--transparent { background: transparent; }
164
+ .app-sb--accent { background: var(--app-accent, var(--c-primary-light, #d4a017)); --c-text-1: #fff; --c-text-2: rgba(255, 255, 255, 0.8); }
165
+ .app-sb.is-collapsed { --sb-w: 72px; }
166
+ .app-sb.is-collapsed .app-sb__label { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
167
+ .app-sb.is-collapsed .app-sb__top { flex-direction: column; }
168
+
169
+ .app-sb__top { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2, 0.5rem); }
170
+ .app-sb__ws { display: flex; align-items: center; gap: var(--sp-3, 0.75rem); min-width: 0; padding: 4px; border-radius: var(--r-md, 12px); color: var(--c-text-1, #fff); text-decoration: none; }
171
+ .app-sb__ws:hover { background: rgb(var(--ink-rgb, 255 255 255) / 0.05); color: var(--c-text-1, #fff); }
172
+ .app-sb__ws-logo { display: grid; place-items: center; flex-shrink: 0; width: 36px; height: 36px; border-radius: 10px; background: var(--app-accent, var(--c-primary-light, #d4a017)); color: var(--c-on-primary, #050505); font-size: 0.8rem; font-weight: 800; letter-spacing: 0.02em; }
173
+ .app-sb__ws-text { display: grid; min-width: 0; line-height: 1.2; }
174
+ .app-sb__ws-text strong { font-size: 0.9rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
175
+ .app-sb__ws-text small { font-size: 0.72rem; color: var(--c-text-2, #a3a3a3); }
176
+ .app-sb__collapse { display: grid; place-items: center; flex-shrink: 0; width: 36px; height: 36px; border: 0; border-radius: 10px; background: transparent; color: var(--c-text-2, #a3a3a3); cursor: pointer; }
177
+ .app-sb__collapse:hover { background: rgb(var(--ink-rgb, 255 255 255) / 0.06); color: var(--c-text-1, #fff); }
178
+
179
+ .app-sb__nav { display: grid; gap: var(--sp-4, 1rem); }
180
+ .app-sb__group ul { display: grid; gap: 2px; margin: 0; padding: 0; list-style: none; }
181
+ .app-sb__group-label { margin: 0 0 var(--sp-1, 0.25rem); padding: 0 var(--sp-3, 0.75rem); font-size: 0.68rem; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: var(--c-text-3, #737373); }
182
+ .app-sb__link {
183
+ position: relative;
184
+ display: flex;
185
+ align-items: center;
186
+ gap: var(--sp-3, 0.75rem);
187
+ min-height: 40px;
188
+ padding: 0 var(--sp-3, 0.75rem);
189
+ border-radius: 10px;
190
+ color: var(--c-text-2, #a3a3a3);
191
+ font-size: 0.88rem;
192
+ font-weight: 500;
193
+ text-decoration: none;
194
+ transition: background 0.15s ease, color 0.15s ease;
195
+ }
196
+ .app-sb__link:hover { background: rgb(var(--ink-rgb, 255 255 255) / 0.05); color: var(--c-text-1, #fff); }
197
+ .app-sb__link.is-active { background: color-mix(in srgb, var(--app-accent, var(--c-primary-light, #d4a017)) 14%, transparent); color: var(--c-text-1, #fff); font-weight: 600; }
198
+ .app-sb__link.is-active .app-sb__icon { color: var(--app-accent, var(--c-primary-light, #d4a017)); }
199
+ .app-sb__icon { display: grid; place-items: center; flex-shrink: 0; width: 20px; }
200
+ .app-sb__text { flex: 1; white-space: nowrap; }
201
+ .app-sb__badge { min-width: 22px; padding: 1px 7px; border-radius: 999px; background: rgb(var(--ink-rgb, 255 255 255) / 0.08); color: var(--c-text-1, #fff); font-size: 0.7rem; font-weight: 700; text-align: center; }
202
+ .app-sb__link.is-active .app-sb__badge { background: var(--app-accent, var(--c-primary-light, #d4a017)); color: var(--c-on-primary, #050505); }
203
+ .app-sb.is-collapsed .app-sb__link { justify-content: center; padding: 0; }
204
+ .app-sb.is-collapsed .app-sb__link:is(:hover, :focus-visible)::after {
205
+ content: attr(data-tip);
206
+ position: absolute;
207
+ left: calc(100% + 10px);
208
+ top: 50%;
209
+ transform: translateY(-50%);
210
+ padding: 4px 8px;
211
+ border-radius: 6px;
212
+ background: var(--c-text-1, #fff);
213
+ color: var(--c-bg, #050505);
214
+ font-size: 0.75rem;
215
+ white-space: nowrap;
216
+ z-index: 5;
217
+ }
218
+ .app-sb :is(a, button):focus-visible { outline: 2px solid var(--app-accent, var(--c-primary-light, #d4a017)); outline-offset: 2px; }
219
+
220
+ .app-sb__promo {
221
+ display: grid;
222
+ gap: 6px;
223
+ margin-top: auto;
224
+ padding: var(--sp-4, 1rem);
225
+ border-radius: 14px;
226
+ border: 1px solid var(--c-border, rgba(255, 255, 255, 0.08));
227
+ background: rgb(var(--ink-rgb, 255 255 255) / 0.03);
228
+ color: var(--c-text-2, #a3a3a3);
229
+ font-size: 0.8rem;
230
+ text-decoration: none;
231
+ }
232
+ .app-sb__promo strong { color: var(--c-text-1, #fff); font-size: 0.85rem; }
233
+ .app-sb__promo em { font-style: normal; font-weight: 600; color: var(--app-accent, var(--c-primary-light, #d4a017)); }
234
+ .app-sb__meter { height: 6px; border-radius: 999px; background: rgb(var(--ink-rgb, 255 255 255) / 0.08); overflow: hidden; }
235
+ .app-sb__meter i { display: block; height: 100%; border-radius: inherit; background: var(--app-accent, var(--c-primary-light, #d4a017)); }
236
+ .app-sb.is-collapsed .app-sb__promo { display: none; }
237
+
238
+ .app-sb-backdrop { position: fixed; inset: 0; z-index: 39; background: rgba(0, 0, 0, 0.5); }
239
+ .app-sb-backdrop[hidden] { display: none; }
240
+
241
+ @media (max-width: 960px) {
242
+ .app-sb:not(.app-sb--inline) { position: fixed; left: 0; top: 0; transform: translateX(-100%); transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1); --sb-w: 280px; box-shadow: 20px 0 60px -20px rgba(0, 0, 0, 0.5); }
243
+ .app-sb.is-open { transform: none; }
244
+ .app-sb.is-collapsed { --sb-w: 280px; }
245
+ .app-sb.is-collapsed .app-sb__label { position: static; width: auto; height: auto; clip: auto; }
246
+ .app-sb__collapse { display: none; }
247
+ }
248
+ @media (prefers-reduced-motion: reduce) { .app-sb { transition: none; } }
249
+ </style>
@@ -0,0 +1,284 @@
1
+ ---
2
+ /**
3
+ * AppTopbar — SaaS app top bar: mobile menu button, search (⌘K / Ctrl+K), notifications dropdown
4
+ * with unread count and "mark all read", light/dark toggle and a user menu.
5
+ *
6
+ * @param {string} searchPlaceholder - Search placeholder. Default 'Search guests, bookings, rooms…'.
7
+ * @param {string} searchAction - Search form action. Default '#'.
8
+ * @param {Array<{title: string, text?: string, time: string, unread?: boolean, tone?: 'info'|'success'|'warning'|'danger'}>} notifications - Dropdown items.
9
+ * @param {{name: string, role?: string, email?: string}} user - Signed-in user.
10
+ * @param {Array<{label: string, href: string}>} userLinks - User menu links.
11
+ * @param {string} signOutHref - Sign-out link. Default '#'.
12
+ * @param {string} sidebarId - id of the AppSidebar the menu button opens on mobile. Default 'app-sidebar'.
13
+ * @param {boolean} themeToggle - Show the light/dark toggle. Default true.
14
+ * @param {string} variant - Style (blur|solid|transparent). Default 'blur'.
15
+ * @param {string} className - Extra class.
16
+ *
17
+ * Slots: `start` (e.g. page title or breadcrumb next to the menu button), `actions` (extra buttons).
18
+ *
19
+ * Accessibility: dropdowns are disclosure buttons closed with Escape or an outside click;
20
+ * the unread count is part of the bell's label; ⌘K moves focus to the search field.
21
+ */
22
+ import { appIcon, initials, hueFrom } from './app-icons';
23
+
24
+ interface Notice { title: string; text?: string; time: string; unread?: boolean; tone?: 'info' | 'success' | 'warning' | 'danger' }
25
+ interface Props {
26
+ searchPlaceholder?: string;
27
+ searchAction?: string;
28
+ notifications?: Notice[];
29
+ user?: { name: string; role?: string; email?: string };
30
+ userLinks?: Array<{ label: string; href: string }>;
31
+ signOutHref?: string;
32
+ sidebarId?: string;
33
+ themeToggle?: boolean;
34
+ variant?: 'blur' | 'solid' | 'transparent';
35
+ className?: string;
36
+ }
37
+
38
+ const {
39
+ searchPlaceholder = 'Search guests, bookings, rooms…',
40
+ searchAction = '#',
41
+ notifications = [
42
+ { title: 'New booking from Booking.com', text: 'Deluxe King · 3 nights · €612', time: '2 min ago', unread: true, tone: 'success' },
43
+ { title: 'Room 204 reported maintenance', text: 'Air conditioning not cooling', time: '18 min ago', unread: true, tone: 'warning' },
44
+ { title: 'Payout sent', text: '€18,240 to your bank account', time: '1 h ago', tone: 'info' },
45
+ ],
46
+ user = { name: 'Lucía Ortega', role: 'General manager', email: 'lucia@mirador.com' },
47
+ userLinks = [{ label: 'Your profile', href: '#' }, { label: 'Settings', href: '#' }, { label: 'Billing', href: '#' }],
48
+ signOutHref = '#',
49
+ sidebarId = 'app-sidebar',
50
+ themeToggle = true,
51
+ variant = 'blur',
52
+ className = '',
53
+ } = Astro.props;
54
+
55
+ const unread = notifications.filter((n) => n.unread).length;
56
+ const hue = hueFrom(user.name);
57
+ ---
58
+
59
+ <header class:list={['app-tb', `app-tb--${variant}`, className]} data-app-topbar>
60
+ <button class="app-tb__icon app-tb__menu" type="button" aria-label="Open navigation" aria-controls={sidebarId} aria-expanded="false" data-sidebar-open={sidebarId}>
61
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><Fragment set:html={appIcon('menu')} /></svg>
62
+ </button>
63
+ <div class="app-tb__start"><slot name="start" /></div>
64
+
65
+ <form class="app-tb__search" role="search" action={searchAction}>
66
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><Fragment set:html={appIcon('search')} /></svg>
67
+ <label class="sr-only" for="app-tb-q">{searchPlaceholder}</label>
68
+ <input id="app-tb-q" name="q" type="search" placeholder={searchPlaceholder} autocomplete="off" data-search />
69
+ <kbd aria-hidden="true">⌘K</kbd>
70
+ </form>
71
+
72
+ <div class="app-tb__actions">
73
+ <slot name="actions" />
74
+ {themeToggle && (
75
+ <button class="app-tb__icon" type="button" aria-label="Switch theme" data-theme-toggle>
76
+ <svg class="i-sun" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><Fragment set:html={appIcon('sun')} /></svg>
77
+ <svg class="i-moon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><Fragment set:html={appIcon('moon')} /></svg>
78
+ </button>
79
+ )}
80
+
81
+ <div class="app-tb__menu-wrap">
82
+ <button class="app-tb__icon" type="button" aria-expanded="false" aria-controls="app-tb-notes" aria-label={unread ? `Notifications, ${unread} unread` : 'Notifications'} data-dd>
83
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><Fragment set:html={appIcon('bell')} /></svg>
84
+ {unread > 0 && <span class="app-tb__dot" aria-hidden="true" data-unread>{unread}</span>}
85
+ </button>
86
+ <div class="app-tb__panel app-tb__panel--notes" id="app-tb-notes" hidden data-panel>
87
+ <div class="app-tb__panel-head">
88
+ <strong>Notifications</strong>
89
+ <button type="button" class="app-tb__link" data-mark-read>Mark all as read</button>
90
+ </div>
91
+ <ul>
92
+ {notifications.map((n) => (
93
+ <li class:list={['app-tb__note', `app-tb__note--${n.tone ?? 'info'}`, { 'is-unread': n.unread }]}>
94
+ <span class="app-tb__note-dot" aria-hidden="true"></span>
95
+ <span class="app-tb__note-body">
96
+ <strong>{n.title}</strong>
97
+ {n.text && <span>{n.text}</span>}
98
+ <time>{n.time}</time>
99
+ </span>
100
+ </li>
101
+ ))}
102
+ </ul>
103
+ </div>
104
+ </div>
105
+
106
+ <div class="app-tb__menu-wrap">
107
+ <button class="app-tb__user" type="button" aria-expanded="false" aria-controls="app-tb-user" data-dd>
108
+ <span class="app-tb__avatar" style={`--h: ${hue}`} aria-hidden="true">{initials(user.name)}</span>
109
+ <span class="app-tb__who"><strong>{user.name}</strong>{user.role && <small>{user.role}</small>}</span>
110
+ <span class="sr-only">Account menu</span>
111
+ </button>
112
+ <div class="app-tb__panel app-tb__panel--user" id="app-tb-user" hidden data-panel>
113
+ <div class="app-tb__panel-head app-tb__panel-head--user">
114
+ <strong>{user.name}</strong>
115
+ {user.email && <span>{user.email}</span>}
116
+ </div>
117
+ <ul>
118
+ {userLinks.map((l) => <li><a href={l.href}>{l.label}</a></li>)}
119
+ <li class="app-tb__sep"><a href={signOutHref}>
120
+ <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('logout')} /></svg>
121
+ Sign out</a></li>
122
+ </ul>
123
+ </div>
124
+ </div>
125
+ </div>
126
+ </header>
127
+
128
+ <script>
129
+ document.querySelectorAll<HTMLElement>('[data-app-topbar]:not([data-bound])').forEach((bar) => {
130
+ bar.dataset.bound = '';
131
+ const toggles = Array.from(bar.querySelectorAll<HTMLButtonElement>('[data-dd]'));
132
+ const panel = (b: HTMLButtonElement) => document.getElementById(b.getAttribute('aria-controls') || '');
133
+ const closeAll = (except?: HTMLButtonElement) => toggles.forEach((b) => {
134
+ if (b === except) return;
135
+ b.setAttribute('aria-expanded', 'false');
136
+ const p = panel(b);
137
+ if (p) p.hidden = true;
138
+ });
139
+ toggles.forEach((b) => b.addEventListener('click', (e) => {
140
+ e.stopPropagation();
141
+ const open = b.getAttribute('aria-expanded') !== 'true';
142
+ closeAll(b);
143
+ b.setAttribute('aria-expanded', String(open));
144
+ const p = panel(b);
145
+ if (p) p.hidden = !open;
146
+ }));
147
+ document.addEventListener('click', (e) => { if (!bar.contains(e.target as Node)) closeAll(); });
148
+ document.addEventListener('keydown', (e) => {
149
+ if (e.key === 'Escape') {
150
+ const open = toggles.find((b) => b.getAttribute('aria-expanded') === 'true');
151
+ closeAll();
152
+ open?.focus();
153
+ }
154
+ if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') {
155
+ e.preventDefault();
156
+ bar.querySelector<HTMLInputElement>('[data-search]')?.focus();
157
+ }
158
+ });
159
+
160
+ bar.querySelector('[data-mark-read]')?.addEventListener('click', () => {
161
+ bar.querySelectorAll('.app-tb__note.is-unread').forEach((n) => n.classList.remove('is-unread'));
162
+ bar.querySelector('[data-unread]')?.remove();
163
+ bar.querySelector('[aria-controls="app-tb-notes"]')?.setAttribute('aria-label', 'Notifications');
164
+ });
165
+
166
+ bar.querySelector('[data-theme-toggle]')?.addEventListener('click', () => {
167
+ const root = document.documentElement;
168
+ const next = root.dataset.theme === 'light' ? 'dark' : 'light';
169
+ root.dataset.theme = next;
170
+ try { window.localStorage.setItem('ack-theme', next); } catch { /* ignore */ }
171
+ });
172
+ });
173
+ </script>
174
+
175
+ <style>
176
+ .app-tb {
177
+ position: sticky;
178
+ top: 0;
179
+ z-index: 30;
180
+ display: flex;
181
+ align-items: center;
182
+ gap: var(--sp-3, 0.75rem);
183
+ min-height: 64px;
184
+ padding: 0 var(--sp-6, 1.5rem);
185
+ border-bottom: 1px solid var(--c-border, rgba(255, 255, 255, 0.08));
186
+ }
187
+ .app-tb--blur { background: rgb(var(--paper-rgb, 5 5 5) / 0.8); backdrop-filter: blur(14px) saturate(1.3); }
188
+ .app-tb--solid { background: var(--c-bg-1, #0a0a0a); }
189
+ .app-tb--transparent { background: transparent; border-bottom-color: transparent; }
190
+ .app-tb__start { min-width: 0; }
191
+ .app-tb__start:empty { display: none; }
192
+
193
+ .app-tb__search {
194
+ position: relative;
195
+ display: flex;
196
+ align-items: center;
197
+ gap: var(--sp-2, 0.5rem);
198
+ flex: 1;
199
+ max-width: 440px;
200
+ height: 40px;
201
+ padding: 0 var(--sp-3, 0.75rem);
202
+ border-radius: 10px;
203
+ border: 1px solid var(--c-border, rgba(255, 255, 255, 0.08));
204
+ background: rgb(var(--ink-rgb, 255 255 255) / 0.03);
205
+ color: var(--c-text-2, #a3a3a3);
206
+ }
207
+ .app-tb__search:focus-within { border-color: var(--app-accent, var(--c-primary-light, #d4a017)); box-shadow: 0 0 0 3px color-mix(in srgb, var(--app-accent, var(--c-primary-light, #d4a017)) 18%, transparent); }
208
+ .app-tb__search input { flex: 1; min-width: 0; border: 0; outline: 0; background: none; color: var(--c-text-1, #fff); font: inherit; font-size: 0.85rem; }
209
+ .app-tb__search input::placeholder { color: var(--c-text-3, #737373); }
210
+ .app-tb__search kbd { padding: 1px 6px; border-radius: 6px; border: 1px solid var(--c-border, rgba(255, 255, 255, 0.1)); font-family: var(--font-mono, monospace); font-size: 0.7rem; color: var(--c-text-3, #737373); }
211
+
212
+ .app-tb__actions { display: flex; align-items: center; gap: var(--sp-1, 0.25rem); margin-left: auto; }
213
+ .app-tb__icon {
214
+ position: relative;
215
+ display: grid;
216
+ place-items: center;
217
+ width: 40px;
218
+ height: 40px;
219
+ border: 0;
220
+ border-radius: 10px;
221
+ background: transparent;
222
+ color: var(--c-text-2, #a3a3a3);
223
+ cursor: pointer;
224
+ }
225
+ .app-tb__icon:hover, .app-tb__icon[aria-expanded='true'] { background: rgb(var(--ink-rgb, 255 255 255) / 0.06); color: var(--c-text-1, #fff); }
226
+ .app-tb__menu { display: none; }
227
+ .app-tb .i-moon { display: none; }
228
+ :global([data-theme='light']) .app-tb .i-sun { display: none; }
229
+ :global([data-theme='light']) .app-tb .i-moon { display: block; }
230
+ .app-tb__dot { position: absolute; top: 4px; right: 4px; min-width: 16px; height: 16px; padding: 0 4px; border-radius: 999px; background: var(--c-danger, #f87171); color: #fff; font-size: 0.62rem; font-weight: 800; line-height: 16px; text-align: center; box-shadow: 0 0 0 2px var(--c-bg-1, #0a0a0a); }
231
+
232
+ .app-tb__menu-wrap { position: relative; }
233
+ .app-tb__user { display: flex; align-items: center; gap: var(--sp-2, 0.5rem); min-height: 44px; padding: 0 var(--sp-2, 0.5rem) 0 4px; margin-left: var(--sp-1, 0.25rem); border: 0; border-radius: 999px; background: transparent; color: var(--c-text-1, #fff); font: inherit; cursor: pointer; text-align: left; }
234
+ .app-tb__user:hover, .app-tb__user[aria-expanded='true'] { background: rgb(var(--ink-rgb, 255 255 255) / 0.06); }
235
+ .app-tb__avatar { display: grid; place-items: center; width: 34px; height: 34px; border-radius: 50%; background: hsl(var(--h) 55% 45%); color: #fff; font-size: 0.75rem; font-weight: 700; }
236
+ .app-tb__who { display: grid; line-height: 1.2; }
237
+ .app-tb__who strong { font-size: 0.82rem; }
238
+ .app-tb__who small { font-size: 0.7rem; color: var(--c-text-2, #a3a3a3); }
239
+ .app-tb :is(a, button, input):focus-visible { outline: 2px solid var(--app-accent, var(--c-primary-light, #d4a017)); outline-offset: 2px; }
240
+
241
+ .app-tb__panel {
242
+ position: absolute;
243
+ right: 0;
244
+ top: calc(100% + 8px);
245
+ width: 340px;
246
+ max-width: calc(100vw - 24px);
247
+ border-radius: 14px;
248
+ border: 1px solid var(--c-border, rgba(255, 255, 255, 0.1));
249
+ background: var(--c-bg-1, #0a0a0a);
250
+ box-shadow: 0 24px 60px -20px rgba(0, 0, 0, 0.5);
251
+ overflow: hidden;
252
+ animation: tb-pop 0.16s ease-out;
253
+ }
254
+ .app-tb__panel--user { width: 240px; }
255
+ .app-tb__panel[hidden] { display: none; }
256
+ @keyframes tb-pop { from { opacity: 0; transform: translateY(-4px); } }
257
+ .app-tb__panel-head { display: flex; align-items: center; justify-content: space-between; padding: var(--sp-3, 0.75rem) var(--sp-4, 1rem); border-bottom: 1px solid var(--c-border, rgba(255, 255, 255, 0.08)); color: var(--c-text-1, #fff); font-size: 0.88rem; }
258
+ .app-tb__panel-head--user { display: grid; gap: 2px; }
259
+ .app-tb__panel-head--user span { font-size: 0.78rem; color: var(--c-text-2, #a3a3a3); }
260
+ .app-tb__link { border: 0; background: none; color: var(--app-accent, var(--c-primary-light, #d4a017)); font: inherit; font-size: 0.78rem; font-weight: 600; cursor: pointer; min-height: 32px; }
261
+ .app-tb__panel ul { margin: 0; padding: 4px; list-style: none; }
262
+ .app-tb__note { display: flex; gap: var(--sp-3, 0.75rem); padding: var(--sp-3, 0.75rem); border-radius: 10px; }
263
+ .app-tb__note:hover { background: rgb(var(--ink-rgb, 255 255 255) / 0.04); }
264
+ .app-tb__note-dot { flex-shrink: 0; width: 8px; height: 8px; margin-top: 6px; border-radius: 50%; background: transparent; }
265
+ .app-tb__note.is-unread .app-tb__note-dot { background: var(--tone, var(--app-accent, var(--c-primary-light, #d4a017))); }
266
+ .app-tb__note--success { --tone: var(--c-success, #34d399); }
267
+ .app-tb__note--warning { --tone: var(--c-warning, #fbbf24); }
268
+ .app-tb__note--danger { --tone: var(--c-danger, #f87171); }
269
+ .app-tb__note-body { display: grid; gap: 2px; font-size: 0.8rem; color: var(--c-text-2, #a3a3a3); }
270
+ .app-tb__note-body strong { font-size: 0.85rem; color: var(--c-text-1, #fff); font-weight: 600; }
271
+ .app-tb__note-body time { font-size: 0.72rem; color: var(--c-text-3, #737373); }
272
+ .app-tb__panel--user a { display: flex; align-items: center; gap: var(--sp-2, 0.5rem); min-height: 40px; padding: 0 var(--sp-3, 0.75rem); border-radius: 8px; color: var(--c-text-1, #fff); font-size: 0.85rem; text-decoration: none; }
273
+ .app-tb__panel--user a:hover { background: rgb(var(--ink-rgb, 255 255 255) / 0.05); color: var(--c-text-1, #fff); }
274
+ .app-tb__sep { margin-top: 4px; padding-top: 4px; border-top: 1px solid var(--c-border, rgba(255, 255, 255, 0.08)); }
275
+ .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
276
+
277
+ @media (max-width: 960px) { .app-tb__menu { display: grid; } }
278
+ @media (max-width: 720px) {
279
+ .app-tb { padding: 0 var(--sp-3, 0.75rem); }
280
+ .app-tb__search kbd, .app-tb__who { display: none; }
281
+ }
282
+ @media (max-width: 480px) { .app-tb__search { display: none; } }
283
+ @media (prefers-reduced-motion: reduce) { .app-tb__panel { animation: none; } }
284
+ </style>