@stacksjs/defaults 0.70.54
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/LICENSE.md +21 -0
- package/README.md +2 -0
- package/package.json +20 -0
- package/resources/assets/fonts/013717e9.woff +0 -0
- package/resources/assets/fonts/03355817.woff2 +0 -0
- package/resources/assets/fonts/0380de7e.woff2 +0 -0
- package/resources/assets/fonts/0ba5d32a.woff +0 -0
- package/resources/assets/fonts/0bd57ddf.woff2 +0 -0
- package/resources/assets/fonts/19de0a60.woff2 +0 -0
- package/resources/assets/fonts/20b87e84.woff +0 -0
- package/resources/assets/fonts/4bbc67e1.woff +0 -0
- package/resources/assets/fonts/576fb0d7.woff +0 -0
- package/resources/assets/fonts/5ea10ee5.woff2 +0 -0
- package/resources/assets/fonts/6413fc0a.woff +0 -0
- package/resources/assets/fonts/72c114cf.woff +0 -0
- package/resources/assets/fonts/73c57e73.woff2 +0 -0
- package/resources/assets/fonts/813dca6c.woff +0 -0
- package/resources/assets/fonts/8a9b0995.woff2 +0 -0
- package/resources/assets/fonts/90b4832f.woff +0 -0
- package/resources/assets/fonts/9394ab85.woff2 +0 -0
- package/resources/assets/fonts/9b3c7d7f.woff2 +0 -0
- package/resources/assets/fonts/Monaco.ttf +0 -0
- package/resources/assets/fonts/b1b36d74.woff +0 -0
- package/resources/assets/fonts/b5d74798.woff2 +0 -0
- package/resources/assets/fonts/b9f81a4d.woff +0 -0
- package/resources/assets/fonts/c9784008.woff2 +0 -0
- package/resources/assets/fonts/cf692227.woff +0 -0
- package/resources/assets/fonts/d1c6ab51.woff2 +0 -0
- package/resources/assets/fonts/e2eb62fc.woff +0 -0
- package/resources/assets/fonts/fe21f8d8.woff2 +0 -0
- package/resources/assets/fonts/nps/CampmateScript-Regular.woff2 +0 -0
- package/resources/assets/fonts/nps/NPS_2026-variable.woff2 +0 -0
- package/resources/assets/fonts/nps/RedwoodSerif-Regular.woff2 +0 -0
- package/resources/assets/fonts/nps/SequoiaSans-Regular.woff2 +0 -0
- package/resources/assets/fonts/nps/Switchback-Regular.woff2 +0 -0
- package/resources/assets/images/app-icon.png +0 -0
- package/resources/assets/images/bg-img.png +0 -0
- package/resources/assets/images/park-ridge.svg +15 -0
- package/resources/assets/images/topography.svg +1 -0
- package/resources/assets/scripts/main.ts +2153 -0
- package/resources/assets/styles/README.md +19 -0
- package/resources/assets/styles/docs/main.css +1 -0
- package/resources/assets/styles/docs/overrides.css +1 -0
- package/resources/assets/styles/docs/vars.css +8 -0
- package/resources/assets/styles/main.css +1419 -0
- package/resources/components/Button.stx +70 -0
- package/resources/components/Buttons/BaseButton.stx +39 -0
- package/resources/components/Buttons/Counter.stx +28 -0
- package/resources/components/Buttons/ToggleDark.stx +11 -0
- package/resources/components/Container.stx +10 -0
- package/resources/components/Dashboard/.gitkeep +0 -0
- package/resources/components/Dashboard/Auth/ForgotPassword.stx +127 -0
- package/resources/components/Dashboard/Auth/Login.stx +212 -0
- package/resources/components/Dashboard/Auth/Register.stx +306 -0
- package/resources/components/Dashboard/Avatar.stx +123 -0
- package/resources/components/Dashboard/Billing/ActivePlan.stx +57 -0
- package/resources/components/Dashboard/Billing/CardBrands.stx +32 -0
- package/resources/components/Dashboard/Billing/CardForm.stx +34 -0
- package/resources/components/Dashboard/Billing/OneTimePayment.stx +28 -0
- package/resources/components/Dashboard/Billing/PaymentForm.stx +33 -0
- package/resources/components/Dashboard/Billing/PaymentMethod.stx +66 -0
- package/resources/components/Dashboard/Billing/PaymentMethodList.stx +52 -0
- package/resources/components/Dashboard/Billing/Plans.stx +136 -0
- package/resources/components/Dashboard/Buttons/AppButton.stx +48 -0
- package/resources/components/Dashboard/Buttons/IconButton.stx +20 -0
- package/resources/components/Dashboard/Card.stx +78 -0
- package/resources/components/Dashboard/Chart.stx +75 -0
- package/resources/components/Dashboard/CodeEditor.stx +41 -0
- package/resources/components/Dashboard/Commerce/BulkActionBar.stx +70 -0
- package/resources/components/Dashboard/Commerce/CategoriesTable.stx +133 -0
- package/resources/components/Dashboard/Commerce/CouponsTable.stx +161 -0
- package/resources/components/Dashboard/Commerce/CustomersTable.stx +142 -0
- package/resources/components/Dashboard/Commerce/Delivery/DeliveryRoutesTable.stx +80 -0
- package/resources/components/Dashboard/Commerce/Delivery/DigitalDeliveryTable.stx +85 -0
- package/resources/components/Dashboard/Commerce/Delivery/DriversTable.stx +99 -0
- package/resources/components/Dashboard/Commerce/Delivery/LicenseKeysTable.stx +92 -0
- package/resources/components/Dashboard/Commerce/Delivery/LicenseTemplatesTable.stx +88 -0
- package/resources/components/Dashboard/Commerce/Delivery/Pagination.stx +122 -0
- package/resources/components/Dashboard/Commerce/Delivery/SearchFilter.stx +23 -0
- package/resources/components/Dashboard/Commerce/Delivery/ShippingMethodsTable.stx +80 -0
- package/resources/components/Dashboard/Commerce/Delivery/ShippingRatesTable.stx +59 -0
- package/resources/components/Dashboard/Commerce/Delivery/ShippingZonesTable.stx +71 -0
- package/resources/components/Dashboard/Commerce/Delivery/TabNavigation.stx +36 -0
- package/resources/components/Dashboard/Commerce/Forms/CouponsForm.stx +191 -0
- package/resources/components/Dashboard/Commerce/OrdersTable.stx +131 -0
- package/resources/components/Dashboard/Commerce/ProductsTable.stx +170 -0
- package/resources/components/Dashboard/CreateRecordModal.stx +363 -0
- package/resources/components/Dashboard/DashboardLayout.stx +155 -0
- package/resources/components/Dashboard/Deployments/ActivityFeed.stx +54 -0
- package/resources/components/Dashboard/Deployments/DeployScript.stx +23 -0
- package/resources/components/Dashboard/Deployments/DeploymentHistory.stx +97 -0
- package/resources/components/Dashboard/Deployments/DeploymentList.stx +94 -0
- package/resources/components/Dashboard/Deployments/LiveTerminalOutput.stx +18 -0
- package/resources/components/Dashboard/Elements/Dropdown.stx +57 -0
- package/resources/components/Dashboard/Email/EmailActivityChart.stx +111 -0
- package/resources/components/Dashboard/Email/EmailCompose.stx +72 -0
- package/resources/components/Dashboard/Email/EmailDetail.stx +105 -0
- package/resources/components/Dashboard/Email/EmailList.stx +117 -0
- package/resources/components/Dashboard/Email/EmailSearch.stx +40 -0
- package/resources/components/Dashboard/Email/EmailSidebar.stx +133 -0
- package/resources/components/Dashboard/GlobalSearch.stx +124 -0
- package/resources/components/Dashboard/MobileSidebar.stx +47 -0
- package/resources/components/Dashboard/Modals/BaseModal.stx +50 -0
- package/resources/components/Dashboard/Modals/Popups/Alert.stx +112 -0
- package/resources/components/Dashboard/Modals/Popups/Toast.stx +70 -0
- package/resources/components/Dashboard/Modals/ToastWrapper.stx +35 -0
- package/resources/components/Dashboard/Monitoring/ErrorDetailModal.stx +228 -0
- package/resources/components/Dashboard/Monitoring/ErrorsTable.stx +184 -0
- package/resources/components/Dashboard/Navbar.stx +194 -0
- package/resources/components/Dashboard/NavbarModern.stx +188 -0
- package/resources/components/Dashboard/NotificationErrorModal.stx +105 -0
- package/resources/components/Dashboard/NotificationStatusBadge.stx +20 -0
- package/resources/components/Dashboard/Pagination.stx +90 -0
- package/resources/components/Dashboard/Queue/QueueTable.stx +127 -0
- package/resources/components/Dashboard/Settings/Forms/AIForm.stx +126 -0
- package/resources/components/Dashboard/Settings/Forms/AnalyticsForm.stx +52 -0
- package/resources/components/Dashboard/Settings/Forms/AppForm.stx +157 -0
- package/resources/components/Dashboard/Settings/Forms/CLIForm.stx +66 -0
- package/resources/components/Dashboard/Settings/Forms/CacheForm.stx +253 -0
- package/resources/components/Dashboard/Settings/Forms/CloudForm.stx +441 -0
- package/resources/components/Dashboard/Settings/Forms/DNSForm.stx +307 -0
- package/resources/components/Dashboard/Settings/Forms/DatabaseForm.stx +168 -0
- package/resources/components/Dashboard/Settings/Forms/EmailForm.stx +100 -0
- package/resources/components/Dashboard/Settings/Forms/FileSystemForm.stx +259 -0
- package/resources/components/Dashboard/Settings/Forms/HashingForm.stx +119 -0
- package/resources/components/Dashboard/Settings/Forms/LibraryForm.stx +389 -0
- package/resources/components/Dashboard/Settings/Forms/QueueForm.stx +219 -0
- package/resources/components/Dashboard/Settings/Forms/SearchEngineForm.stx +48 -0
- package/resources/components/Dashboard/Settings/Forms/SecurityForm.stx +143 -0
- package/resources/components/Dashboard/Settings/Forms/ServicesForm.stx +194 -0
- package/resources/components/Dashboard/Settings/Forms/StorageForm.stx +191 -0
- package/resources/components/Dashboard/Settings/Forms/TeamsForm.stx +33 -0
- package/resources/components/Dashboard/Settings/SettingsFormManager.stx +181 -0
- package/resources/components/Dashboard/SettingsHeader.stx +88 -0
- package/resources/components/Dashboard/SettingsSidebar.stx +234 -0
- package/resources/components/Dashboard/Sidebar.stx +23 -0
- package/resources/components/Dashboard/SidebarModern.stx +249 -0
- package/resources/components/Dashboard/Skeleton/LoadingCard.stx +15 -0
- package/resources/components/Dashboard/Skeleton/LoadingDetails.stx +26 -0
- package/resources/components/Dashboard/Table.stx +82 -0
- package/resources/components/Dashboard/TerminalContainer.stx +97 -0
- package/resources/components/Dashboard/Tooltip.stx +166 -0
- package/resources/components/Dashboard/Transaction/index.stx +88 -0
- package/resources/components/Dashboard/UI/ActivityTable.stx +54 -0
- package/resources/components/Dashboard/UI/Avatar.stx +102 -0
- package/resources/components/Dashboard/UI/Badge.stx +66 -0
- package/resources/components/Dashboard/UI/Button.stx +73 -0
- package/resources/components/Dashboard/UI/Card.stx +51 -0
- package/resources/components/Dashboard/UI/ChartCard.stx +36 -0
- package/resources/components/Dashboard/UI/Checkbox.stx +74 -0
- package/resources/components/Dashboard/UI/ConfirmDialog.stx +74 -0
- package/resources/components/Dashboard/UI/DataTable.stx +45 -0
- package/resources/components/Dashboard/UI/Dropdown.stx +72 -0
- package/resources/components/Dashboard/UI/DropdownItem.stx +80 -0
- package/resources/components/Dashboard/UI/EmptyState.stx +50 -0
- package/resources/components/Dashboard/UI/FilterBar.stx +48 -0
- package/resources/components/Dashboard/UI/Input.stx +77 -0
- package/resources/components/Dashboard/UI/Modal.stx +75 -0
- package/resources/components/Dashboard/UI/PageHeader.stx +69 -0
- package/resources/components/Dashboard/UI/PageLayout.stx +84 -0
- package/resources/components/Dashboard/UI/Pagination.stx +126 -0
- package/resources/components/Dashboard/UI/QuickLinks.stx +31 -0
- package/resources/components/Dashboard/UI/RouterLink.stx +31 -0
- package/resources/components/Dashboard/UI/Select.stx +84 -0
- package/resources/components/Dashboard/UI/ServiceHealth.stx +38 -0
- package/resources/components/Dashboard/UI/StatsCard.stx +79 -0
- package/resources/components/Dashboard/UI/Table.stx +143 -0
- package/resources/components/Dashboard/UI/Tabs.stx +45 -0
- package/resources/components/Dashboard/UI/Textarea.stx +93 -0
- package/resources/components/Dashboard/UI/Toggle.stx +84 -0
- package/resources/components/Dashboard/UI/WindowControls.stx +135 -0
- package/resources/components/Dashboard/Widget.stx +89 -0
- package/resources/components/Dashboard/WindowControls.stx +119 -0
- package/resources/components/Dashboard/index.ts +106 -0
- package/resources/components/Docs/Demo/ComboboxCode.md +114 -0
- package/resources/components/Docs/Demo/ComboboxDemo.stx +91 -0
- package/resources/components/Docs/Demo/DialogCode.md +42 -0
- package/resources/components/Docs/Demo/DialogDemo.stx +76 -0
- package/resources/components/Docs/Demo/DocsPlayground.stx +39 -0
- package/resources/components/Docs/Demo/DropdownCode.md +109 -0
- package/resources/components/Docs/Demo/DropdownDemo.stx +82 -0
- package/resources/components/Docs/Demo/Hero.stx +100 -0
- package/resources/components/Docs/Demo/ListboxCode.md +87 -0
- package/resources/components/Docs/Demo/ListboxDemo.stx +59 -0
- package/resources/components/Docs/Demo/NotificationCode.md +173 -0
- package/resources/components/Docs/Demo/NotificationDemo.stx +99 -0
- package/resources/components/Docs/Demo/PopoverCode.md +161 -0
- package/resources/components/Docs/Demo/PopoverDemo.stx +96 -0
- package/resources/components/Docs/Demo/RadioGroupCode.md +98 -0
- package/resources/components/Docs/Demo/RadioGroupDemo.stx +53 -0
- package/resources/components/Docs/Demo/StepperCode.md +216 -0
- package/resources/components/Docs/Demo/StepperDemo.stx +137 -0
- package/resources/components/Docs/Demo/SwitchCode.md +24 -0
- package/resources/components/Docs/Demo/SwitchDemo.stx +28 -0
- package/resources/components/Docs/Home.stx +7 -0
- package/resources/components/Docs/HomeContributors.stx +23 -0
- package/resources/components/Docs/HomeSponsors.stx +17 -0
- package/resources/components/Docs/HomeTeam.stx +18 -0
- package/resources/components/Docs/TeamMember.stx +79 -0
- package/resources/components/Email/EmailButton.stx +32 -0
- package/resources/components/Email/EmailDivider.stx +20 -0
- package/resources/components/Email/EmailImage.stx +24 -0
- package/resources/components/Email/EmailLayout.stx +55 -0
- package/resources/components/Email/EmailLink.stx +22 -0
- package/resources/components/Email/EmailSection.stx +20 -0
- package/resources/components/Email/EmailText.stx +30 -0
- package/resources/components/Forum/ForumBreadcrumb.stx +38 -0
- package/resources/components/Forum/ForumCategory.stx +16 -0
- package/resources/components/Forum/ForumCategoryItem.stx +46 -0
- package/resources/components/Forum/ForumLayout.stx +36 -0
- package/resources/components/Forum/ForumPagination.stx +39 -0
- package/resources/components/Forum/ForumPost.stx +71 -0
- package/resources/components/Forum/ForumReplyForm.stx +76 -0
- package/resources/components/Forum/ForumSidebar.stx +86 -0
- package/resources/components/Forum/ForumTopicItem.stx +77 -0
- package/resources/components/Forum/ForumTopicList.stx +23 -0
- package/resources/components/HelloWorld.stx +253 -0
- package/resources/components/Logo.stx +10 -0
- package/resources/components/Marketing/CallToAction.stx +30 -0
- package/resources/components/Marketing/ComingSoon.stx +155 -0
- package/resources/components/Marketing/Faqs.stx +99 -0
- package/resources/components/Marketing/Feature.stx +32 -0
- package/resources/components/Marketing/Fields.stx +28 -0
- package/resources/components/Marketing/Footer.stx +33 -0
- package/resources/components/Marketing/Header.stx +51 -0
- package/resources/components/Marketing/Hero.stx +46 -0
- package/resources/components/Marketing/Layout.stx +26 -0
- package/resources/components/Marketing/NavLink.stx +13 -0
- package/resources/components/Marketing/Plan.stx +43 -0
- package/resources/components/Marketing/Pricing.stx +120 -0
- package/resources/components/Marketing/PrimaryFeatures.stx +80 -0
- package/resources/components/Marketing/PrimaryFeatures2.stx +98 -0
- package/resources/components/Marketing/SecondaryFeatures.stx +80 -0
- package/resources/components/Marketing/Testimonials.stx +120 -0
- package/resources/components/Pagination.stx +149 -0
- package/resources/components/Playground.stx +9 -0
- package/resources/components/README.md +87 -0
- package/resources/components/Storefront/AnnouncementBar.stx +17 -0
- package/resources/components/Storefront/CartDrawer.stx +227 -0
- package/resources/components/Storefront/CheckoutSteps.stx +50 -0
- package/resources/components/Storefront/Footer.stx +81 -0
- package/resources/components/Storefront/Header.stx +66 -0
- package/resources/components/Storefront/OrderSummary.stx +79 -0
- package/resources/components/Storefront/ProductCard.stx +63 -0
- package/resources/components/Storefront/ProductGallery.stx +88 -0
- package/resources/components/Storefront/QuantitySelector.stx +97 -0
- package/resources/components/index.ts +13 -0
- package/resources/emails/email-verification.stx +46 -0
- package/resources/emails/layouts/base.html +113 -0
- package/resources/emails/password-changed.stx +55 -0
- package/resources/emails/password-reset.stx +43 -0
- package/resources/emails/subscription-confirmation.stx +99 -0
- package/resources/emails/welcome.stx +48 -0
- package/resources/functions/README.md +28 -0
- package/resources/functions/counter.ts +13 -0
- package/resources/functions/dark.ts +4 -0
- package/resources/functions/dashboard/analytics.ts +238 -0
- package/resources/functions/dashboard/config-io.ts +316 -0
- package/resources/functions/dashboard/data.ts +375 -0
- package/resources/functions/dashboard/library.ts +250 -0
- package/resources/functions/dashboard/sidebar.ts +537 -0
- package/resources/functions/dashboard/system.ts +150 -0
- package/resources/layouts/403.stx +188 -0
- package/resources/layouts/404.stx +34 -0
- package/resources/layouts/README.md +14 -0
- package/resources/layouts/dashboard/default.stx +20 -0
- package/resources/layouts/dashboard/guest.stx +10 -0
- package/resources/layouts/default.stx +3 -0
- package/resources/layouts/maintenance.stx +11 -0
- package/resources/layouts/storefront.stx +73 -0
- package/resources/layouts/system-tray/default.stx +3 -0
- package/resources/modules/.gitkeep +0 -0
- package/resources/plugins/.gitkeep +0 -0
- package/resources/plugins/preloader.ts +308 -0
- package/resources/types/README.md +3 -0
- package/resources/views/cart.stx +149 -0
- package/resources/views/checkout/contact.stx +101 -0
- package/resources/views/checkout/payment.stx +114 -0
- package/resources/views/checkout/shipping.stx +120 -0
- package/resources/views/coming-soon.stx +9 -0
- package/resources/views/dashboard/custom-page.stx +45 -0
- package/resources/views/dashboard/index.stx +24 -0
- package/resources/views/emails/welcome.stx +0 -0
- package/resources/views/errors/tester.stx +26 -0
- package/resources/views/index.stx +2865 -0
- package/resources/views/orders/[id].stx +130 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
interface TestimonialsProps {
|
|
3
|
+
testimonials?: string
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const { testimonials = [ } = defineProps<TestimonialsProps>()
|
|
7
|
+
|
|
8
|
+
[
|
|
9
|
+
{
|
|
10
|
+
content: 'Stacks is a rapid development framework that has helped me create and maintain frontends, backends, and clouds without worrying about the boilerplate.',
|
|
11
|
+
author: {
|
|
12
|
+
name: 'Sheryl Berge',
|
|
13
|
+
role: 'CEO at Lynch LLC',
|
|
14
|
+
image: '/images/avatars/avatar-1.png',
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
content: 'The Stacks mantra, "Convention over configuration", has made my development process much smoother and efficient.',
|
|
19
|
+
author: {
|
|
20
|
+
name: 'Amy Hahn',
|
|
21
|
+
role: 'Director at Velocity Industries',
|
|
22
|
+
image: '/images/avatars/avatar-4.png',
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
[
|
|
27
|
+
{
|
|
28
|
+
content: "Getting started with Stacks was incredibly easy. The Buddy toolkit has been a game changer for my project. I suggest you guys give it a try today! It's probably one of the best decisions you'll ever do for yourself this year.",
|
|
29
|
+
author: {
|
|
30
|
+
name: 'Leland Kiehn',
|
|
31
|
+
role: 'Founder of Kiehn and Sons',
|
|
32
|
+
image: '/images/avatars/avatar-5.png',
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
content: 'The Stacks framework is a harmony of several engines that has allowed me to build web and desktop applications in highly scalable and privacy-friendly ways.',
|
|
37
|
+
author: {
|
|
38
|
+
name: 'Erin Powlowski',
|
|
39
|
+
role: 'COO at Armstrong Inc',
|
|
40
|
+
image: '/images/avatars/avatar-2.png',
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
[
|
|
45
|
+
{
|
|
46
|
+
content: 'Stacks has enhanced my productivity as a developer. No more creating boilerplate, and the IDE integration is fantastic.',
|
|
47
|
+
author: {
|
|
48
|
+
name: 'Peter Renolds',
|
|
49
|
+
role: 'Founder of West Inc',
|
|
50
|
+
image: '/images/avatars/avatar-3.png',
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
content: 'The Stacks CI/CD has allowed me to focus on coding, not publishing. The CI/CD is superb!',
|
|
55
|
+
author: {
|
|
56
|
+
name: 'Amy Hahn',
|
|
57
|
+
role: 'Director at Velocity Industries',
|
|
58
|
+
image: '/images/avatars/avatar-4.png',
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
]
|
|
63
|
+
</script>
|
|
64
|
+
<section
|
|
65
|
+
id="testimonials"
|
|
66
|
+
aria-label="What our customers are saying"
|
|
67
|
+
class="py-20 sm:py-32 bg-slate-50"
|
|
68
|
+
>
|
|
69
|
+
<Container>
|
|
70
|
+
<div class="mx-auto max-w-2xl md:text-center">
|
|
71
|
+
<h2 class="font-display text-3xl text-slate-900 tracking-tight sm:text-4xl">
|
|
72
|
+
Enjoyed by businesses and highly recommended by devs across the globe.
|
|
73
|
+
</h2>
|
|
74
|
+
<p class="mt-4 text-lg text-slate-700 tracking-tight">
|
|
75
|
+
The goal of the framework is to help you create & maintain frontends, backends, and clouds—without having to worry about the boilerplate. Stacks is a rapid development framework, meeting all your full stack needs.
|
|
76
|
+
</p>
|
|
77
|
+
</div>
|
|
78
|
+
<ul
|
|
79
|
+
role="list"
|
|
80
|
+
class="grid gap-6 grid-cols-1 sm:gap-8 lg:grid-cols-3 mt-16 mx-auto lg:mt-20 max-w-2xl lg:max-w-none"
|
|
81
|
+
>
|
|
82
|
+
@foreach(testimonials as column)
|
|
83
|
+
<li>
|
|
84
|
+
<ul role="list" class="flex flex-col gap-y-6 sm:gap-y-8">
|
|
85
|
+
@foreach(column as testimonial)
|
|
86
|
+
<li>
|
|
87
|
+
<figure class="relative p-6 bg-white rounded-2xl shadow-slate-900/10 shadow-xl">
|
|
88
|
+
<blockquote class="relative">
|
|
89
|
+
<p class="text-lg text-slate-900 tracking-tight">
|
|
90
|
+
{{ testimonial.content }}
|
|
91
|
+
</p>
|
|
92
|
+
</blockquote>
|
|
93
|
+
<figcaption class="flex relative items-center justify-between mt-6 pt-6 border-slate-100 border-t">
|
|
94
|
+
<div>
|
|
95
|
+
<div class="font-display text-base text-slate-900">
|
|
96
|
+
{{ testimonial.author.name }}
|
|
97
|
+
</div>
|
|
98
|
+
<div class="mt-1 text-slate-500 text-sm">
|
|
99
|
+
{{ testimonial.author.role }}
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
<div class="overflow-hidden bg-slate-50 rounded-full">
|
|
103
|
+
<img
|
|
104
|
+
class="object-cover h-14 w-14"
|
|
105
|
+
src="{{ testimonial.author.image }}"
|
|
106
|
+
alt=""
|
|
107
|
+
width="56"
|
|
108
|
+
height="56"
|
|
109
|
+
>
|
|
110
|
+
</div>
|
|
111
|
+
</figcaption>
|
|
112
|
+
</figure>
|
|
113
|
+
</li>
|
|
114
|
+
@endforeach
|
|
115
|
+
</ul>
|
|
116
|
+
</li>
|
|
117
|
+
@endforeach
|
|
118
|
+
</ul>
|
|
119
|
+
</Container>
|
|
120
|
+
</section>
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
/**
|
|
3
|
+
* Pagination component (stacksjs/stacks#1909, P5 from #1910).
|
|
4
|
+
*
|
|
5
|
+
* Renders the right shape for whichever paginator variant comes in:
|
|
6
|
+
*
|
|
7
|
+
* - Paginator<T> → prev / page-numbers / next + first/last jumps
|
|
8
|
+
* - SimplePaginator<T> → prev / next only
|
|
9
|
+
* - CursorPaginator<T> → prev / next only (cursor-based, no page #s)
|
|
10
|
+
*
|
|
11
|
+
* The paginator picks up its prev/next URLs from `paginator-request.ts`
|
|
12
|
+
* (P2) — they include query-string preservation, so search filters
|
|
13
|
+
* survive across page navigations without extra work here.
|
|
14
|
+
*
|
|
15
|
+
* Accessibility: wraps everything in `<nav aria-label="Pagination">`,
|
|
16
|
+
* sets `aria-current="page"` on the active page, uses real `<a>` tags
|
|
17
|
+
* (not `<button>`) because pagination is navigation, not action.
|
|
18
|
+
*
|
|
19
|
+
* Window-size: the `window` prop controls how many neighbors of the
|
|
20
|
+
* current page render before the ellipsis kicks in. Default 2 gives
|
|
21
|
+
* a compact "1 ... 3 4 [5] 6 7 ... 12" shape.
|
|
22
|
+
*
|
|
23
|
+
* Algorithm lives in `@stacksjs/ui/pagination` (buildPageSequence /
|
|
24
|
+
* urlForPage / paginatorVariant) so it's unit-testable independently
|
|
25
|
+
* of the stx render pipeline.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { buildPageSequence, paginatorVariant, urlForPage } from '@stacksjs/ui'
|
|
29
|
+
|
|
30
|
+
interface PaginationLike {
|
|
31
|
+
data: unknown[]
|
|
32
|
+
per_page: number
|
|
33
|
+
has_more_pages: boolean
|
|
34
|
+
prev_page_url?: string | null
|
|
35
|
+
next_page_url?: string | null
|
|
36
|
+
first_page_url?: string
|
|
37
|
+
last_page_url?: string
|
|
38
|
+
// Full paginator only:
|
|
39
|
+
current_page?: number
|
|
40
|
+
total?: number
|
|
41
|
+
last_page?: number
|
|
42
|
+
from?: number | null
|
|
43
|
+
to?: number | null
|
|
44
|
+
// Cursor paginator only:
|
|
45
|
+
next_cursor?: string | null
|
|
46
|
+
prev_cursor?: string | null
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface PaginationProps {
|
|
50
|
+
/** The paginator to render — accepts all three Stacks paginator shapes. */
|
|
51
|
+
paginator: PaginationLike
|
|
52
|
+
/**
|
|
53
|
+
* Page numbers shown on either side of the current page in the full
|
|
54
|
+
* Paginator variant. `1 ... [3] 4 5 6 7 ... 12` is `window=2`.
|
|
55
|
+
* @default 2
|
|
56
|
+
*/
|
|
57
|
+
window?: number
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const { paginator, window: windowSize = 2 } = defineProps<PaginationProps>()
|
|
61
|
+
|
|
62
|
+
const variant = paginatorVariant(paginator)
|
|
63
|
+
const isFull = variant === 'full'
|
|
64
|
+
const isSimple = variant === 'simple'
|
|
65
|
+
const isCursor = variant === 'cursor'
|
|
66
|
+
|
|
67
|
+
const pages = isFull
|
|
68
|
+
? buildPageSequence(paginator.current_page!, paginator.last_page!, windowSize)
|
|
69
|
+
: []
|
|
70
|
+
|
|
71
|
+
const baseLink = 'inline-flex items-center px-3 py-2 text-sm font-medium border'
|
|
72
|
+
const activeLink = `${baseLink} border-indigo-500 text-indigo-600 dark:text-indigo-400 bg-indigo-50 dark:bg-indigo-900/30`
|
|
73
|
+
const inactiveLink = `${baseLink} border-gray-300 dark:border-neutral-600 text-gray-700 dark:text-neutral-300 hover:bg-gray-50 dark:hover:bg-neutral-700`
|
|
74
|
+
const disabledLink = `${baseLink} border-gray-200 dark:border-neutral-700 text-gray-400 dark:text-neutral-500 cursor-not-allowed`
|
|
75
|
+
</script>
|
|
76
|
+
|
|
77
|
+
<nav aria-label="Pagination" class="flex gap-2 items-center justify-between py-3 select-none">
|
|
78
|
+
@if(isFull)
|
|
79
|
+
{{-- Mobile: prev / next only (page numbers hidden) --}}
|
|
80
|
+
<div class="flex sm:hidden flex-1 justify-between">
|
|
81
|
+
@if(paginator.prev_page_url)
|
|
82
|
+
<a href="{{ paginator.prev_page_url }}" class="{{ inactiveLink }} rounded-md">Previous</a>
|
|
83
|
+
@else
|
|
84
|
+
<span class="{{ disabledLink }} rounded-md">Previous</span>
|
|
85
|
+
@endif
|
|
86
|
+
@if(paginator.next_page_url)
|
|
87
|
+
<a href="{{ paginator.next_page_url }}" class="{{ inactiveLink }} rounded-md">Next</a>
|
|
88
|
+
@else
|
|
89
|
+
<span class="{{ disabledLink }} rounded-md">Next</span>
|
|
90
|
+
@endif
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
{{-- Desktop: prev / page numbers / next + "Showing X to Y of Z" --}}
|
|
94
|
+
<div class="hidden sm:flex sm:flex-1 sm:items-center sm:justify-between">
|
|
95
|
+
<p class="text-gray-700 text-sm dark:text-neutral-300">
|
|
96
|
+
@if(paginator.from && paginator.to)
|
|
97
|
+
Showing <span class="font-medium">{{ paginator.from }}</span>
|
|
98
|
+
to <span class="font-medium">{{ paginator.to }}</span>
|
|
99
|
+
of <span class="font-medium">{{ paginator.total }}</span> results
|
|
100
|
+
@else
|
|
101
|
+
No results
|
|
102
|
+
@endif
|
|
103
|
+
</p>
|
|
104
|
+
|
|
105
|
+
<div class="inline-flex -space-x-px rounded-md shadow-sm isolate">
|
|
106
|
+
@if(paginator.prev_page_url)
|
|
107
|
+
<a href="{{ paginator.prev_page_url }}" rel="prev" class="{{ inactiveLink }} rounded-l-md" aria-label="Previous page">←</a>
|
|
108
|
+
@else
|
|
109
|
+
<span class="{{ disabledLink }} rounded-l-md" aria-label="Previous page">←</span>
|
|
110
|
+
@endif
|
|
111
|
+
|
|
112
|
+
@foreach(pages as p)
|
|
113
|
+
@if(p === '…')
|
|
114
|
+
<span class="{{ inactiveLink }}">…</span>
|
|
115
|
+
@else
|
|
116
|
+
@if(p === paginator.current_page)
|
|
117
|
+
<a href="{{ urlForPage(paginator, p) }}" aria-current="page" class="{{ activeLink }}">{{ p }}</a>
|
|
118
|
+
@else
|
|
119
|
+
<a href="{{ urlForPage(paginator, p) }}" class="{{ inactiveLink }}">{{ p }}</a>
|
|
120
|
+
@endif
|
|
121
|
+
@endif
|
|
122
|
+
@endforeach
|
|
123
|
+
|
|
124
|
+
@if(paginator.next_page_url)
|
|
125
|
+
<a href="{{ paginator.next_page_url }}" rel="next" class="{{ inactiveLink }} rounded-r-md" aria-label="Next page">→</a>
|
|
126
|
+
@else
|
|
127
|
+
<span class="{{ disabledLink }} rounded-r-md" aria-label="Next page">→</span>
|
|
128
|
+
@endif
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
@endif
|
|
132
|
+
|
|
133
|
+
@if(isSimple || isCursor)
|
|
134
|
+
{{-- Simple / cursor: just prev / next. No page numbers because we
|
|
135
|
+
don't know last_page (simple) or it doesn't apply (cursor). --}}
|
|
136
|
+
<div class="flex flex-1 justify-between">
|
|
137
|
+
@if(paginator.prev_page_url)
|
|
138
|
+
<a href="{{ paginator.prev_page_url }}" rel="prev" class="{{ inactiveLink }} rounded-md">← Previous</a>
|
|
139
|
+
@else
|
|
140
|
+
<span class="{{ disabledLink }} rounded-md">← Previous</span>
|
|
141
|
+
@endif
|
|
142
|
+
@if(paginator.next_page_url)
|
|
143
|
+
<a href="{{ paginator.next_page_url }}" rel="next" class="{{ inactiveLink }} rounded-md">Next →</a>
|
|
144
|
+
@else
|
|
145
|
+
<span class="{{ disabledLink }} rounded-md">Next →</span>
|
|
146
|
+
@endif
|
|
147
|
+
</div>
|
|
148
|
+
@endif
|
|
149
|
+
</nav>
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# The Components Folder
|
|
2
|
+
|
|
3
|
+
This is where you create your components.
|
|
4
|
+
|
|
5
|
+
> **Note**
|
|
6
|
+
> If your project does not require any UI components, you can remove this folder. If your library does include components, you may use this readme as the boilerplate of your component library.
|
|
7
|
+
|
|
8
|
+
## 💡 Get Started
|
|
9
|
+
|
|
10
|
+
It's easy to get started. The only prerequisite is a basic understanding of HTML, sprinkled with *minimal* JavaScript. In other words, there is virtually no learning curve.
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
# you may use this GitHub template or the following command
|
|
14
|
+
bunx stacks new hello-world
|
|
15
|
+
cd hello-world
|
|
16
|
+
|
|
17
|
+
buddy install # install deps for all packages
|
|
18
|
+
buddy dev:components # starts the component dev server
|
|
19
|
+
buddy build:components # builds the component library for production-ready use
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Additionally, the `package.json` contains some useful snippets you likely want to be aware of.
|
|
23
|
+
|
|
24
|
+
## 🤖 Usage
|
|
25
|
+
|
|
26
|
+
Because this project is optimized toward the development of easily reusable & composable component libraries, it’s very easy to use (and distribute):
|
|
27
|
+
|
|
28
|
+
```vue
|
|
29
|
+
<script setup lang="ts">
|
|
30
|
+
import HelloWorld from 'hello-world-stack'
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<template>
|
|
34
|
+
<HelloWorld name="J Doe" />
|
|
35
|
+
</template>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Tips
|
|
39
|
+
|
|
40
|
+
This project also includes a simple way to handle your versioning. Through semantic commit names, it will also generate the two changelogs: one as part of the GitHub releases & the one markdown file that's stored within the root of the project.
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# how to create a git commit
|
|
44
|
+
git add . # select the changes you want to commit
|
|
45
|
+
buddy commit # then simply follow the prompts
|
|
46
|
+
|
|
47
|
+
# after you successfully committed, you may create a "release"
|
|
48
|
+
buddy release # automates git commits, versioning, and CHANGELOG generation
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Read more about these tips in the docs.
|
|
52
|
+
|
|
53
|
+
### Coding Style
|
|
54
|
+
|
|
55
|
+
You may want to consider using the "Composition API" with [`<script setup>` SFC syntax](https://github.com/vuejs/rfcs/pull/227), as it is perfectly suited for building component libraries via Stacks. [ESLint](https://github.com/eslint) also comes pre-configured to statically analyze, fix and format your code without the need for Prettier.
|
|
56
|
+
|
|
57
|
+
When using this template, feel free to adjust it to your needs. It is a framework to help you quickly & efficiently bootstrap and design component libraries using industry best practices.
|
|
58
|
+
|
|
59
|
+
## 🧪 Testing
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
bun test
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## 📈 Changelog
|
|
66
|
+
|
|
67
|
+
Please see our [releases](https://github.com/stacksjs/stacks/releases) page for more information on what has changed recently.
|
|
68
|
+
|
|
69
|
+
## 🚜 Contributing
|
|
70
|
+
|
|
71
|
+
Please review the [Contributing Guide](https://github.com/stacksjs/contributing) for details.
|
|
72
|
+
|
|
73
|
+
## 🏝 Community
|
|
74
|
+
|
|
75
|
+
For help, discussion about best practices, or any other conversation that would benefit from being searchable:
|
|
76
|
+
|
|
77
|
+
[Discussions on GitHub](https://github.com/stacksjs/stacks/discussions)
|
|
78
|
+
|
|
79
|
+
For casual chit-chat with others using this package:
|
|
80
|
+
|
|
81
|
+
[Join the Stacks Discord Server](https://discord.gg/stacksjs)
|
|
82
|
+
|
|
83
|
+
## 📄 License
|
|
84
|
+
|
|
85
|
+
The MIT License (MIT). Please see [LICENSE](https://github.com/stacksjs/stacks/tree/main/LICENSE.md) for more information.
|
|
86
|
+
|
|
87
|
+
Made with 💙
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
interface AnnouncementBarProps {
|
|
3
|
+
message?: string
|
|
4
|
+
className?: string
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const {
|
|
8
|
+
message = 'Free shipping on orders over $40 · Vet-formulated · Made in the USA',
|
|
9
|
+
className = '',
|
|
10
|
+
} = defineProps<AnnouncementBarProps>()
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<div class="bg-stone-900 text-stone-100 text-xs {{ className }}">
|
|
14
|
+
<div class="mx-auto px-6 py-2 max-w-6xl text-center tracking-wide">
|
|
15
|
+
{{ message }}
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
/**
|
|
3
|
+
* Slide-in cart drawer.
|
|
4
|
+
*
|
|
5
|
+
* Server-rendered closed by default. The storefront's add-to-cart button
|
|
6
|
+
* triggers an XHR POST to `/api/cart/add`; on a 200 the drawer's contents
|
|
7
|
+
* are refreshed (via `/api/cart`) and the drawer slides in. We use small
|
|
8
|
+
* vanilla DOM for the open/close because stx signals can't reach across
|
|
9
|
+
* the layout slot in this case — the data-stx-* hooks below are the
|
|
10
|
+
* agreed-upon interface other components use to drive it.
|
|
11
|
+
*/
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<div data-stx-cart-drawer class="fixed inset-0 z-50 pointer-events-none" aria-hidden="true">
|
|
15
|
+
<!-- Backdrop -->
|
|
16
|
+
<div
|
|
17
|
+
data-stx-cart-backdrop
|
|
18
|
+
class="absolute inset-0 bg-stone-900/40 opacity-0 duration-300 ease-out transition-opacity"
|
|
19
|
+
style="will-change: opacity"
|
|
20
|
+
></div>
|
|
21
|
+
|
|
22
|
+
<!-- Panel — translate3d + will-change promote it to its own GPU layer so
|
|
23
|
+
the slide-in doesn't re-paint the underlying header / content layers
|
|
24
|
+
and cause the perceived flicker on neighboring elements. -->
|
|
25
|
+
<aside
|
|
26
|
+
data-stx-cart-panel
|
|
27
|
+
class="flex absolute bottom-0 right-0 top-0 flex-col max-w-md w-full bg-stone-50 shadow-2xl duration-300 ease-out transition-transform"
|
|
28
|
+
style="transform: translate3d(100%, 0, 0); will-change: transform"
|
|
29
|
+
role="dialog"
|
|
30
|
+
aria-modal="true"
|
|
31
|
+
aria-label="Cart"
|
|
32
|
+
tabindex="-1"
|
|
33
|
+
>
|
|
34
|
+
<header class="flex items-center justify-between p-6 border-b border-stone-200">
|
|
35
|
+
<h2 class="font-bold text-lg text-stone-900">Your bowl</h2>
|
|
36
|
+
<button type="button" data-stx-cart-close class="text-stone-500 focus-visible:text-stone-900 hover:text-stone-900 focus-visible:outline-none" aria-label="Close cart">
|
|
37
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /></svg>
|
|
38
|
+
</button>
|
|
39
|
+
</header>
|
|
40
|
+
|
|
41
|
+
<div data-stx-cart-body class="overflow-y-auto flex-1 p-6 space-y-4">
|
|
42
|
+
<div data-stx-cart-empty class="py-12 text-center">
|
|
43
|
+
<div class="inline-flex items-center justify-center mx-auto h-14 w-14 bg-stone-100 rounded-full">
|
|
44
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-7 w-7 text-stone-400" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 3h1.386c.51 0 .955.343 1.087.835l.383 1.437M7.5 14.25a3 3 0 00-3 3h15.75m-12.75-3h11.218c.51 0 .956-.343 1.087-.835l1.872-7.024A1.125 1.125 0 0019.797 5.25H5.106M7.5 14.25L5.106 5.25M6 20.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm12.75 0a.75.75 0 11-1.5 0 .75.75 0 011.5 0z" /></svg>
|
|
45
|
+
</div>
|
|
46
|
+
<p class="mt-4 font-medium text-sm text-stone-700">Your bowl is empty.</p>
|
|
47
|
+
<p class="mt-1 text-stone-500 text-xs">Pick a treat and we'll start one.</p>
|
|
48
|
+
<a href="/products" class="inline-flex gap-2 items-center mt-5 px-5 py-2.5 font-semibold text-stone-50 text-xs bg-stone-900 hover:bg-stone-800 rounded-full focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-stone-50 focus-visible:ring-stone-900 transition-colors">
|
|
49
|
+
Browse the shop
|
|
50
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3"/></svg>
|
|
51
|
+
</a>
|
|
52
|
+
</div>
|
|
53
|
+
<ul data-stx-cart-list class="hidden space-y-4"></ul>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<footer data-stx-cart-footer class="hidden p-6 space-y-4 border-stone-200 border-t">
|
|
57
|
+
<div class="flex justify-between text-sm"><span class="text-stone-600">Subtotal</span><span class="font-bold text-stone-900" data-stx-cart-subtotal>$0.00</span></div>
|
|
58
|
+
<a href="/cart" class="block px-6 py-3 w-full font-semibold text-center text-sm text-stone-900 border border-stone-300 rounded-full hover:border-stone-900 transition-colors">View cart</a>
|
|
59
|
+
<a href="/checkout/contact" class="block px-6 py-3 w-full font-semibold text-center text-sm text-stone-50 bg-stone-900 hover:bg-stone-800 rounded-full transition-colors">Checkout</a>
|
|
60
|
+
</footer>
|
|
61
|
+
</aside>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<script data-stx-scoped>
|
|
65
|
+
;(() => {
|
|
66
|
+
// Idempotency guard. SPA fragment swaps re-inject scoped scripts when
|
|
67
|
+
// the cart link is followed; without this, every navigation stacked
|
|
68
|
+
// another set of click listeners on [data-stx-add-to-cart] buttons,
|
|
69
|
+
// and a single click would fire two/three/N XHRs — the user-visible
|
|
70
|
+
// tell was a brief flicker as the drawer opened, refreshed, then
|
|
71
|
+
// refreshed again from the duplicate handlers.
|
|
72
|
+
if (window.__stxCartWired) return
|
|
73
|
+
window.__stxCartWired = true
|
|
74
|
+
|
|
75
|
+
const root = document.querySelector('[data-stx-cart-drawer]')
|
|
76
|
+
if (!root) return
|
|
77
|
+
const backdrop = root.querySelector('[data-stx-cart-backdrop]')
|
|
78
|
+
const panel = root.querySelector('[data-stx-cart-panel]')
|
|
79
|
+
const closeBtn = root.querySelector('[data-stx-cart-close]')
|
|
80
|
+
const list = root.querySelector('[data-stx-cart-list]')
|
|
81
|
+
const empty = root.querySelector('[data-stx-cart-empty]')
|
|
82
|
+
const footer = root.querySelector('[data-stx-cart-footer]')
|
|
83
|
+
const subtotalEl = root.querySelector('[data-stx-cart-subtotal]')
|
|
84
|
+
|
|
85
|
+
// Reserve the scrollbar gutter on <html> so locking page scroll while
|
|
86
|
+
// the drawer is open doesn't cause the page width to jump (~17px on
|
|
87
|
+
// platforms with overlay scrollbars off). This was the visible flicker
|
|
88
|
+
// on neighboring elements when the drawer slid in: the page reflowed
|
|
89
|
+
// because the scrollbar disappeared.
|
|
90
|
+
document.documentElement.style.scrollbarGutter = 'stable'
|
|
91
|
+
|
|
92
|
+
let lastFocus = null
|
|
93
|
+
|
|
94
|
+
function open() {
|
|
95
|
+
lastFocus = document.activeElement
|
|
96
|
+
root.style.pointerEvents = 'auto'
|
|
97
|
+
root.setAttribute('aria-hidden', 'false')
|
|
98
|
+
backdrop.style.opacity = '1'
|
|
99
|
+
panel.style.transform = 'translate3d(0, 0, 0)'
|
|
100
|
+
document.body.style.overflow = 'hidden'
|
|
101
|
+
// Move focus into the panel so Esc + Tab work as expected.
|
|
102
|
+
requestAnimationFrame(() => { panel.focus({ preventScroll: true }) })
|
|
103
|
+
}
|
|
104
|
+
function shut() {
|
|
105
|
+
root.style.pointerEvents = 'none'
|
|
106
|
+
root.setAttribute('aria-hidden', 'true')
|
|
107
|
+
backdrop.style.opacity = '0'
|
|
108
|
+
panel.style.transform = 'translate3d(100%, 0, 0)'
|
|
109
|
+
document.body.style.overflow = ''
|
|
110
|
+
if (lastFocus && typeof lastFocus.focus === 'function') {
|
|
111
|
+
try { lastFocus.focus({ preventScroll: true }) } catch { /* element gone */ }
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
backdrop?.addEventListener('click', shut)
|
|
116
|
+
closeBtn?.addEventListener('click', shut)
|
|
117
|
+
document.addEventListener('keydown', (e) => {
|
|
118
|
+
if (e.key === 'Escape' && root.getAttribute('aria-hidden') === 'false') shut()
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
function escapeHtml(s) {
|
|
122
|
+
return String(s ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c])
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function render(items, subtotal) {
|
|
126
|
+
if (!list || !empty || !footer) return
|
|
127
|
+
if (!items || items.length === 0) {
|
|
128
|
+
list.classList.add('hidden')
|
|
129
|
+
empty.classList.remove('hidden')
|
|
130
|
+
footer.classList.add('hidden')
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
empty.classList.add('hidden')
|
|
134
|
+
list.classList.remove('hidden')
|
|
135
|
+
footer.classList.remove('hidden')
|
|
136
|
+
list.innerHTML = items.map((it) => {
|
|
137
|
+
const slug = escapeHtml(it.slug || '')
|
|
138
|
+
const name = escapeHtml(it.name || '')
|
|
139
|
+
const image = escapeHtml(it.image || '')
|
|
140
|
+
const qty = Number(it.qty) || 0
|
|
141
|
+
const price = Number(it.price) || 0
|
|
142
|
+
const lineTotal = Number(it.lineTotal) || 0
|
|
143
|
+
return [
|
|
144
|
+
'<li class="flex gap-3 items-start">',
|
|
145
|
+
' <a href="/products/' + slug + '" class="block overflow-hidden flex-shrink-0 h-16 w-16 bg-stone-200 rounded-xl">',
|
|
146
|
+
' <img src="' + image + '" alt="' + name + '" class="object-cover h-full w-full">',
|
|
147
|
+
' </a>',
|
|
148
|
+
' <div class="flex-1 min-w-0">',
|
|
149
|
+
' <p class="font-semibold text-sm text-stone-900 truncate">' + name + '</p>',
|
|
150
|
+
' <p class="mt-0.5 text-stone-500 text-xs">Qty ' + qty + ' · $' + price.toFixed(2) + '</p>',
|
|
151
|
+
' </div>',
|
|
152
|
+
' <p class="font-semibold text-sm text-stone-900">$' + lineTotal.toFixed(2) + '</p>',
|
|
153
|
+
'</li>',
|
|
154
|
+
].join('')
|
|
155
|
+
}).join('')
|
|
156
|
+
if (subtotalEl) subtotalEl.textContent = '$' + (Number(subtotal) || 0).toFixed(2)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
let inflightRefresh = null
|
|
160
|
+
async function refresh() {
|
|
161
|
+
// Coalesce concurrent refreshes — if a click fires two refresh() in a
|
|
162
|
+
// row (e.g. add-to-cart + open both call refresh), they reuse the
|
|
163
|
+
// same network request and the drawer doesn't briefly toggle between
|
|
164
|
+
// empty and populated.
|
|
165
|
+
if (inflightRefresh) return inflightRefresh
|
|
166
|
+
inflightRefresh = (async () => {
|
|
167
|
+
try {
|
|
168
|
+
const res = await fetch('/api/cart', {
|
|
169
|
+
credentials: 'same-origin',
|
|
170
|
+
headers: { 'X-Requested-With': 'XMLHttpRequest' },
|
|
171
|
+
})
|
|
172
|
+
if (!res.ok) return
|
|
173
|
+
const data = await res.json()
|
|
174
|
+
render(data.items || [], data.subtotal || 0)
|
|
175
|
+
const badge = document.querySelector('[data-stx-cart-count]')
|
|
176
|
+
if (badge) {
|
|
177
|
+
const total = (data.items || []).reduce((s, i) => s + (Number(i.qty) || 0), 0)
|
|
178
|
+
badge.textContent = String(total)
|
|
179
|
+
badge.classList.toggle('hidden', total === 0)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
catch { /* leave existing render in place */ }
|
|
183
|
+
finally { inflightRefresh = null }
|
|
184
|
+
})()
|
|
185
|
+
return inflightRefresh
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Public hooks for other components to call.
|
|
189
|
+
window.stxCart = {
|
|
190
|
+
open: () => { open(); refresh() },
|
|
191
|
+
shut,
|
|
192
|
+
refresh,
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Wire any add-to-cart submit buttons via a single document-level
|
|
196
|
+
// delegated listener. Compared to attaching per-button listeners, this:
|
|
197
|
+
// - Stays correct when the catalog page fragment swaps in new
|
|
198
|
+
// [data-stx-add-to-cart] buttons.
|
|
199
|
+
// - Can't double-fire if the scoped script runs more than once (the
|
|
200
|
+
// guard at the top is the primary defense; this is belt-and-braces).
|
|
201
|
+
document.addEventListener('click', async (e) => {
|
|
202
|
+
const target = e.target instanceof Element ? e.target : null
|
|
203
|
+
if (!target) return
|
|
204
|
+
const btn = target.closest('[data-stx-add-to-cart]')
|
|
205
|
+
if (!btn) return
|
|
206
|
+
const form = btn.closest('form')
|
|
207
|
+
if (!form) return
|
|
208
|
+
e.preventDefault()
|
|
209
|
+
const fd = new FormData(form)
|
|
210
|
+
btn.setAttribute('aria-busy', 'true')
|
|
211
|
+
try {
|
|
212
|
+
const res = await fetch(form.action || '/api/cart/add', {
|
|
213
|
+
method: form.method || 'POST',
|
|
214
|
+
body: fd,
|
|
215
|
+
credentials: 'same-origin',
|
|
216
|
+
headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json' },
|
|
217
|
+
})
|
|
218
|
+
if (res.ok) {
|
|
219
|
+
await refresh()
|
|
220
|
+
open()
|
|
221
|
+
}
|
|
222
|
+
else { form.submit() }
|
|
223
|
+
} catch { form.submit() }
|
|
224
|
+
finally { btn.removeAttribute('aria-busy') }
|
|
225
|
+
})
|
|
226
|
+
})()
|
|
227
|
+
</script>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
interface CheckoutStep {
|
|
3
|
+
key: string
|
|
4
|
+
label: string
|
|
5
|
+
href?: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface CheckoutStepsProps {
|
|
9
|
+
current: string
|
|
10
|
+
steps?: CheckoutStep[]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const {
|
|
14
|
+
current,
|
|
15
|
+
steps = [
|
|
16
|
+
{ key: 'contact', label: 'Contact', href: '/checkout/contact' },
|
|
17
|
+
{ key: 'shipping', label: 'Shipping', href: '/checkout/shipping' },
|
|
18
|
+
{ key: 'payment', label: 'Payment', href: '/checkout/payment' },
|
|
19
|
+
],
|
|
20
|
+
} = defineProps<CheckoutStepsProps>()
|
|
21
|
+
|
|
22
|
+
const currentIndex = steps.findIndex(s => s.key === current)
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<nav aria-label="Checkout progress" class="mb-10">
|
|
26
|
+
<ol class="flex gap-3 items-center text-sm">
|
|
27
|
+
@foreach (steps as step, i)
|
|
28
|
+
<li class="flex flex-1 gap-3 items-center min-w-0">
|
|
29
|
+
<span class="inline-flex h-7 w-7 items-center justify-center rounded-full text-xs font-semibold flex-shrink-0
|
|
30
|
+
{{ i < currentIndex ? 'bg-stone-900 text-stone-50' : '' }}
|
|
31
|
+
{{ i === currentIndex ? 'bg-stone-900 text-stone-50 ring-4 ring-stone-200' : '' }}
|
|
32
|
+
{{ i > currentIndex ? 'bg-stone-200 text-stone-500' : '' }}">
|
|
33
|
+
@if (i < currentIndex)
|
|
34
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke-width="3" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" /></svg>
|
|
35
|
+
@else
|
|
36
|
+
{{ i + 1 }}
|
|
37
|
+
@endif
|
|
38
|
+
</span>
|
|
39
|
+
@if (step.href && i <= currentIndex)
|
|
40
|
+
<a href="{{ step.href }}" class="font-semibold text-stone-900 truncate hover:underline">{{ step.label }}</a>
|
|
41
|
+
@else
|
|
42
|
+
<span class="{{ i === currentIndex ? 'text-stone-900 font-semibold' : 'text-stone-500' }} truncate">{{ step.label }}</span>
|
|
43
|
+
@endif
|
|
44
|
+
@if (i < steps.length - 1)
|
|
45
|
+
<span class="hidden md:block flex-1 h-px bg-stone-200"></span>
|
|
46
|
+
@endif
|
|
47
|
+
</li>
|
|
48
|
+
@endforeach
|
|
49
|
+
</ol>
|
|
50
|
+
</nav>
|