From f6ec57ca5fddd9bf59e5637d501e07324df25bb5 Mon Sep 17 00:00:00 2001 From: Luca Argentieri Date: Wed, 4 Mar 2026 17:50:56 +0100 Subject: [PATCH 01/14] remove legacy critical and icon assets references --- assets/critical.css | 117 ----------------------------------- assets/icon-account.svg | 6 -- assets/icon-cart.svg | 5 -- layout/password.liquid | 27 ++++---- sections/header.liquid | 123 ++++++++++++++++++------------------- templates/gift_card.liquid | 117 +++++++++++++++++------------------ 6 files changed, 131 insertions(+), 264 deletions(-) delete mode 100644 assets/critical.css delete mode 100644 assets/icon-account.svg delete mode 100644 assets/icon-cart.svg diff --git a/assets/critical.css b/assets/critical.css deleted file mode 100644 index cdb1ae1aa..000000000 --- a/assets/critical.css +++ /dev/null @@ -1,117 +0,0 @@ -/** Critical CSS for the theme. This file is included on every page. */ - -/* Reset styles inspired by https://www.joshwcomeau.com/css/custom-css-reset/ */ -* { - box-sizing: border-box; - margin: 0; -} - -body { - display: flex; - flex-direction: column; - margin: 0; - min-height: 100svh; -} - -html:has(dialog[scroll-lock][open], details[scroll-lock][open]) { - overflow: hidden; -} - -img, -picture, -video, -canvas, -svg { - display: block; - max-width: 100%; - height: auto; -} - -input, -textarea, -select { - font: inherit; - border-radius: var(--style-border-radius-inputs); -} - -select { - background-color: var(--color-background); - color: currentcolor; -} - -dialog { - background-color: var(--color-background); - color: var(--color-foreground); -} - -p { - text-wrap: pretty; -} -p, -h1, -h2, -h3, -h4, -h5, -h6 { - overflow-wrap: break-word; -} - -p:empty { - display: none; -} - -:is(p, h1, h2, h3, h4, h5, h6):first-child, -:empty:first-child + :where(p, h1, h2, h3, h4, h5, h6) { - margin-block-start: 0; -} - -:is(p, h1, h2, h3, h4, h5, h6):last-child, -:where(p, h1, h2, h3, h4, h5, h6) + :has(+ :empty:last-child) { - margin-block-end: 0; -} - -/** Theme styles below */ -body { - font-family: var(--font-primary--family); - background-color: var(--color-background); - color: var(--color-foreground); -} - -/** Section layout utilities */ - -/** - * Setup a grid that enables both full-width and constrained layouts - * depending on the class of the child elements. - * - * By default, a minimum content margin is set on the left and right - * sides of the section and the content is centered in the viewport to - * not exceed the maximum page width. - * - * When a child element is given the `full-width` class, it will span - * the entire viewport. - */ -.shopify-section { - --content-width: min( - calc(var(--page-width) - var(--page-margin) * 2), - calc(100% - var(--page-margin) * 2) - ); - --content-margin: minmax(var(--page-margin), 1fr); - --content-grid: var(--content-margin) var(--content-width) var(--content-margin); - - /* This is required to make elements work as background images */ - position: relative; - grid-template-columns: var(--content-grid); - display: grid; - width: 100%; -} - -/* Child elements, by default, are constrained to the central column of the grid. */ -.shopify-section > * { - grid-column: 2; -} - -/* Child elements that use the full-width utility class span the entire viewport. */ -.shopify-section > .full-width { - grid-column: 1 / -1; -} diff --git a/assets/icon-account.svg b/assets/icon-account.svg deleted file mode 100644 index 5c523a3f8..000000000 --- a/assets/icon-account.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - diff --git a/assets/icon-cart.svg b/assets/icon-cart.svg deleted file mode 100644 index 0a8d0a9b1..000000000 --- a/assets/icon-cart.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - \ No newline at end of file diff --git a/layout/password.liquid b/layout/password.liquid index 350ec0bd1..3ac1a5a6f 100644 --- a/layout/password.liquid +++ b/layout/password.liquid @@ -1,19 +1,18 @@ - - {% # Inlined CSS Variables %} - {% render 'css-variables' %} + + {% # Social, title, etc. %} + {% render 'meta-tags' %} - {% # Load and preload the critical CSS %} - {{ 'critical.css' | asset_url | stylesheet_tag: preload: true }} + {%- liquid + render 'vite-tag' with 'css/main.css' + render 'vite-tag' with 'ts/theme.ts' + render 'vite-tag' with 'ts/password.ts' + -%} + {{ content_for_header }} + - {% # Social, title, etc. %} - {% render 'meta-tags' %} - - {{ content_for_header }} - - - - {{ content_for_layout }} - + + {{ content_for_layout }} + diff --git a/sections/header.liquid b/sections/header.liquid index d873bdda7..1a17b6257 100644 --- a/sections/header.liquid +++ b/sections/header.liquid @@ -1,77 +1,74 @@
-

- {{ shop.name | link_to: routes.root_url }} -

+

+ {{ shop.name | link_to: routes.root_url }} +

-
- {% for link in section.settings.menu.links %} - {{ link.title | link_to: link.url }} - {% endfor %} -
+
+ {% for link in section.settings.menu.links %} + {{ link.title | link_to: link.url }} + {% endfor %} +
-
- {% if shop.customer_accounts_enabled %} - - {{ 'icon-account.svg' | inline_asset_content }} - - {% endif %} + + {{ 'cart.title' | t }} + +
{% stylesheet %} - header { - height: 5rem; - display: flex; - align-items: center; - justify-content: space-between; - } - header a { - position: relative; - text-decoration: none; - color: var(--color-foreground); - display: flex; - align-items: center; - justify-content: center; - } - header a sup { - position: absolute; - left: 100%; - overflow: hidden; - max-width: var(--page-margin); - } - header svg { - width: 2rem; - } - header .header__menu, - header .header__icons { - display: flex; - gap: 1rem; - } + header { + height: 5rem; + display: flex; + align-items: center; + justify-content: space-between; + } + header a { + position: relative; + text-decoration: none; + color: var(--color-foreground); + display: flex; + align-items: center; + justify-content: center; + } + header a sup { + position: absolute; + left: 100%; + overflow: hidden; + max-width: var(--page-margin); + } + header .header__menu, + header .header__icons { + display: flex; + gap: 1rem; + } {% endstylesheet %} {% schema %} { - "name": "t:general.header", - "settings": [ - { - "type": "link_list", - "id": "menu", - "label": "t:labels.menu" - }, - { - "type": "link_list", - "id": "customer_account_menu", - "label": "t:labels.customer_account_menu", - "default": "customer-account-main-menu" - } - ] + "name": "t:general.header", + "settings": [ + { + "type": "link_list", + "id": "menu", + "label": "t:labels.menu" + }, + { + "type": "link_list", + "id": "customer_account_menu", + "label": "t:labels.customer_account_menu", + "default": "customer-account-main-menu" + } + ] } {% endschema %} diff --git a/templates/gift_card.liquid b/templates/gift_card.liquid index db5d8289b..9dbd72f1f 100644 --- a/templates/gift_card.liquid +++ b/templates/gift_card.liquid @@ -2,72 +2,71 @@ - - {% # Inlined CSS Variables %} - {% render 'css-variables' %} + + {% # Social, title, etc. %} + {% render 'meta-tags' %} - {% # Load and preload the critical CSS %} - {{ 'critical.css' | asset_url | stylesheet_tag: preload: true }} + {% style %} + main { + text-align: center; + } + main img { + display: unset; + } + {% endstyle %} - {% # Social, title, etc. %} - {% render 'meta-tags' %} + {% liquid + render 'vite-tag' with 'ts/theme.ts' + render 'vite-tag' with 'ts/gift-card.ts' + %} - {% style %} - main { - text-align: center; - } - main img { - display: unset; - } - {% endstyle %} + {{ content_for_header }} + - {{ content_for_header }} - + +
+
+

{{ gift_card.balance | money }}

- -
-
-

{{ gift_card.balance | money }}

+ {% if gift_card.enabled == false or gift_card.expired %} +

{{ 'gift_card.expired' | t }}

+ {% endif %} - {% if gift_card.enabled == false or gift_card.expired %} -

{{ 'gift_card.expired' | t }}

- {% endif %} + {% if gift_card.expires_on %} + {% assign expires_on = gift_card.expires_on | date: '%B %e, %Y' %} +

+ {{ 'gift_card.expires_on' | t: expires_on: expires_on }} +

+ {% endif %} - {% if gift_card.expires_on %} - {% assign expires_on = gift_card.expires_on | date: '%B %e, %Y' %} -

- {{ 'gift_card.expires_on' | t: expires_on: expires_on }} -

- {% endif %} +

+ {% if settings.logo %} + {{ settings.logo | image_url: width: 300 | image_tag: alt: shop.name }} + {% else %} + {{ 'gift_card.card' | t }} + {% endif %} +

-

- {% if settings.logo %} - {{ settings.logo | image_url: width: 300 | image_tag: alt: shop.name }} - {% else %} - {{ 'gift_card.card' | t }} - {% endif %} -

+

{{ shop.name }}

+

{{ 'gift_card.use_at_checkout' | t }}

+

{{ gift_card.code | format_code }}

-

{{ shop.name }}

-

{{ 'gift_card.use_at_checkout' | t }}

-

{{ gift_card.code | format_code }}

- - {% if gift_card.pass_url %} - - {{ 'gift_card.add_to_apple_wallet' | t }} - - {% endif %} -
-
- + {% if gift_card.pass_url %} + + {{ 'gift_card.add_to_apple_wallet' | t }} + + {% endif %} +
+
+ From 2a7c8bb5935a8fbdc34dc568f6ced852fc9a96b7 Mon Sep 17 00:00:00 2001 From: Luca Argentieri Date: Wed, 4 Mar 2026 18:30:40 +0100 Subject: [PATCH 02/14] pre-core setup --- .agents/skills/shopify-development/README.md | 60 + .agents/skills/shopify-development/SKILL.md | 368 +++++ .../references/app-development.md | 578 ++++++++ .../references/extensions.md | 555 +++++++ .../shopify-development/references/themes.md | 498 +++++++ .../shopify-development/scripts/.gitignore | 49 + .../scripts/requirements.txt | 19 + .../scripts/shopify_graphql.py | 428 ++++++ .../scripts/shopify_init.py | 441 ++++++ .../scripts/tests/test_shopify_init.py | 379 +++++ .agents/skills/tailwind/SKILL.md | 80 + .../tailwind/figma-tokens/breakpoints.css | 39 + .../skills/tailwind/figma-tokens/colors.css | 42 + .../tailwind/figma-tokens/radius-shadows.css | 44 + .../skills/tailwind/figma-tokens/spacing.css | 35 + .../tailwind/figma-tokens/typography.css | 65 + .../tailwind/rules/a11y-and-dark-mode.md | 135 ++ .../tailwind/rules/core-custom-styles.md | 136 ++ .../rules/core-responsive-and-states.md | 164 +++ .../tailwind/rules/core-theme-variables.md | 160 ++ .../tailwind/rules/core-utility-model.md | 99 ++ .../tailwind/rules/figma-to-theme-workflow.md | 302 ++++ .../rules/perf-purging-and-scanning.md | 87 ++ .agents/skills/vite/GENERATION.md | 5 + .agents/skills/vite/SKILL.md | 78 + .../skills/vite/references/build-and-ssr.md | 238 +++ .agents/skills/vite/references/core-config.md | 162 +++ .../skills/vite/references/core-features.md | 205 +++ .../skills/vite/references/core-plugin-api.md | 235 +++ .../skills/vite/references/environment-api.md | 108 ++ .../vite/references/rolldown-migration.md | 157 ++ .claude/settings.json | 11 + .claude/skills/shopify-development/README.md | 60 + .claude/skills/shopify-development/SKILL.md | 368 +++++ .../references/app-development.md | 578 ++++++++ .../references/extensions.md | 555 +++++++ .../shopify-development/references/themes.md | 498 +++++++ .../shopify-development/scripts/.gitignore | 49 + .../scripts/requirements.txt | 19 + .../scripts/shopify_graphql.py | 428 ++++++ .../scripts/shopify_init.py | 441 ++++++ .../scripts/tests/test_shopify_init.py | 379 +++++ .claude/skills/tailwind/SKILL.md | 80 + .../tailwind/figma-tokens/breakpoints.css | 39 + .../skills/tailwind/figma-tokens/colors.css | 42 + .../tailwind/figma-tokens/radius-shadows.css | 44 + .../skills/tailwind/figma-tokens/spacing.css | 35 + .../tailwind/figma-tokens/typography.css | 65 + .../tailwind/rules/a11y-and-dark-mode.md | 135 ++ .../tailwind/rules/core-custom-styles.md | 136 ++ .../rules/core-responsive-and-states.md | 164 +++ .../tailwind/rules/core-theme-variables.md | 160 ++ .../tailwind/rules/core-utility-model.md | 99 ++ .../tailwind/rules/figma-to-theme-workflow.md | 302 ++++ .../rules/perf-purging-and-scanning.md | 87 ++ .claude/skills/vite/GENERATION.md | 5 + .claude/skills/vite/SKILL.md | 78 + .../skills/vite/references/build-and-ssr.md | 238 +++ .claude/skills/vite/references/core-config.md | 162 +++ .../skills/vite/references/core-features.md | 205 +++ .../skills/vite/references/core-plugin-api.md | 235 +++ .../skills/vite/references/environment-api.md | 108 ++ .../vite/references/rolldown-migration.md | 157 ++ .env.example | 1 + .github/workflows/ci.yml | 22 +- .gitignore | 11 +- .mcp.json | 8 + .shopifyignore | 14 +- CLAUDE.md | 210 +++ CODE_OF_CONDUCT.md | 132 -- CONTRIBUTING.md | 24 - assets/.vite/manifest.json | 89 ++ assets/404-l0sNRNKZ.js | 1 + assets/article-l0sNRNKZ.js | 1 + assets/blog-l0sNRNKZ.js | 1 + assets/cart-l0sNRNKZ.js | 1 + assets/collection-l0sNRNKZ.js | 1 + assets/gift-card-l0sNRNKZ.js | 1 + assets/index-l0sNRNKZ.js | 1 + assets/list-collections-l0sNRNKZ.js | 1 + assets/main-C4sk1vw8.css | 1 + assets/page-l0sNRNKZ.js | 1 + assets/password-l0sNRNKZ.js | 1 + assets/product-l0sNRNKZ.js | 1 + assets/search-l0sNRNKZ.js | 1 + assets/shoppy-x-ray.svg | 1 - assets/theme-B5Qt9EMX.js | 1 + bun.lock | 1286 +++++++++++++++++ example.shopify.theme.toml | 2 + frontend/entrypoints/css/main.css | 1 + frontend/entrypoints/ts/404.ts | 0 frontend/entrypoints/ts/article.ts | 0 frontend/entrypoints/ts/blog.ts | 0 frontend/entrypoints/ts/cart.ts | 0 frontend/entrypoints/ts/collection.ts | 0 frontend/entrypoints/ts/gift-card.ts | 0 frontend/entrypoints/ts/index.ts | 0 frontend/entrypoints/ts/list-collections.ts | 0 frontend/entrypoints/ts/page.ts | 0 frontend/entrypoints/ts/password.ts | 0 frontend/entrypoints/ts/product.ts | 0 frontend/entrypoints/ts/search.ts | 0 frontend/entrypoints/ts/theme.ts | 1 + layout/theme.liquid | 74 +- package.json | 22 + sections/hello-world.liquid | 144 -- sections/index.liquid | 3 + snippets/css-variables.liquid | 18 - snippets/vite-tag.liquid | 37 + templates/index.json | 6 +- tsconfig.json | 22 + vite.config.js | 37 + 112 files changed, 13714 insertions(+), 378 deletions(-) create mode 100644 .agents/skills/shopify-development/README.md create mode 100644 .agents/skills/shopify-development/SKILL.md create mode 100644 .agents/skills/shopify-development/references/app-development.md create mode 100644 .agents/skills/shopify-development/references/extensions.md create mode 100644 .agents/skills/shopify-development/references/themes.md create mode 100644 .agents/skills/shopify-development/scripts/.gitignore create mode 100644 .agents/skills/shopify-development/scripts/requirements.txt create mode 100644 .agents/skills/shopify-development/scripts/shopify_graphql.py create mode 100644 .agents/skills/shopify-development/scripts/shopify_init.py create mode 100644 .agents/skills/shopify-development/scripts/tests/test_shopify_init.py create mode 100644 .agents/skills/tailwind/SKILL.md create mode 100644 .agents/skills/tailwind/figma-tokens/breakpoints.css create mode 100644 .agents/skills/tailwind/figma-tokens/colors.css create mode 100644 .agents/skills/tailwind/figma-tokens/radius-shadows.css create mode 100644 .agents/skills/tailwind/figma-tokens/spacing.css create mode 100644 .agents/skills/tailwind/figma-tokens/typography.css create mode 100644 .agents/skills/tailwind/rules/a11y-and-dark-mode.md create mode 100644 .agents/skills/tailwind/rules/core-custom-styles.md create mode 100644 .agents/skills/tailwind/rules/core-responsive-and-states.md create mode 100644 .agents/skills/tailwind/rules/core-theme-variables.md create mode 100644 .agents/skills/tailwind/rules/core-utility-model.md create mode 100644 .agents/skills/tailwind/rules/figma-to-theme-workflow.md create mode 100644 .agents/skills/tailwind/rules/perf-purging-and-scanning.md create mode 100644 .agents/skills/vite/GENERATION.md create mode 100644 .agents/skills/vite/SKILL.md create mode 100644 .agents/skills/vite/references/build-and-ssr.md create mode 100644 .agents/skills/vite/references/core-config.md create mode 100644 .agents/skills/vite/references/core-features.md create mode 100644 .agents/skills/vite/references/core-plugin-api.md create mode 100644 .agents/skills/vite/references/environment-api.md create mode 100644 .agents/skills/vite/references/rolldown-migration.md create mode 100644 .claude/settings.json create mode 100644 .claude/skills/shopify-development/README.md create mode 100644 .claude/skills/shopify-development/SKILL.md create mode 100644 .claude/skills/shopify-development/references/app-development.md create mode 100644 .claude/skills/shopify-development/references/extensions.md create mode 100644 .claude/skills/shopify-development/references/themes.md create mode 100644 .claude/skills/shopify-development/scripts/.gitignore create mode 100644 .claude/skills/shopify-development/scripts/requirements.txt create mode 100644 .claude/skills/shopify-development/scripts/shopify_graphql.py create mode 100644 .claude/skills/shopify-development/scripts/shopify_init.py create mode 100644 .claude/skills/shopify-development/scripts/tests/test_shopify_init.py create mode 100644 .claude/skills/tailwind/SKILL.md create mode 100644 .claude/skills/tailwind/figma-tokens/breakpoints.css create mode 100644 .claude/skills/tailwind/figma-tokens/colors.css create mode 100644 .claude/skills/tailwind/figma-tokens/radius-shadows.css create mode 100644 .claude/skills/tailwind/figma-tokens/spacing.css create mode 100644 .claude/skills/tailwind/figma-tokens/typography.css create mode 100644 .claude/skills/tailwind/rules/a11y-and-dark-mode.md create mode 100644 .claude/skills/tailwind/rules/core-custom-styles.md create mode 100644 .claude/skills/tailwind/rules/core-responsive-and-states.md create mode 100644 .claude/skills/tailwind/rules/core-theme-variables.md create mode 100644 .claude/skills/tailwind/rules/core-utility-model.md create mode 100644 .claude/skills/tailwind/rules/figma-to-theme-workflow.md create mode 100644 .claude/skills/tailwind/rules/perf-purging-and-scanning.md create mode 100644 .claude/skills/vite/GENERATION.md create mode 100644 .claude/skills/vite/SKILL.md create mode 100644 .claude/skills/vite/references/build-and-ssr.md create mode 100644 .claude/skills/vite/references/core-config.md create mode 100644 .claude/skills/vite/references/core-features.md create mode 100644 .claude/skills/vite/references/core-plugin-api.md create mode 100644 .claude/skills/vite/references/environment-api.md create mode 100644 .claude/skills/vite/references/rolldown-migration.md create mode 100644 .env.example create mode 100644 .mcp.json create mode 100644 CLAUDE.md delete mode 100644 CODE_OF_CONDUCT.md delete mode 100644 CONTRIBUTING.md create mode 100644 assets/.vite/manifest.json create mode 100644 assets/404-l0sNRNKZ.js create mode 100644 assets/article-l0sNRNKZ.js create mode 100644 assets/blog-l0sNRNKZ.js create mode 100644 assets/cart-l0sNRNKZ.js create mode 100644 assets/collection-l0sNRNKZ.js create mode 100644 assets/gift-card-l0sNRNKZ.js create mode 100644 assets/index-l0sNRNKZ.js create mode 100644 assets/list-collections-l0sNRNKZ.js create mode 100644 assets/main-C4sk1vw8.css create mode 100644 assets/page-l0sNRNKZ.js create mode 100644 assets/password-l0sNRNKZ.js create mode 100644 assets/product-l0sNRNKZ.js create mode 100644 assets/search-l0sNRNKZ.js delete mode 100644 assets/shoppy-x-ray.svg create mode 100644 assets/theme-B5Qt9EMX.js create mode 100644 bun.lock create mode 100644 example.shopify.theme.toml create mode 100644 frontend/entrypoints/css/main.css create mode 100644 frontend/entrypoints/ts/404.ts create mode 100644 frontend/entrypoints/ts/article.ts create mode 100644 frontend/entrypoints/ts/blog.ts create mode 100644 frontend/entrypoints/ts/cart.ts create mode 100644 frontend/entrypoints/ts/collection.ts create mode 100644 frontend/entrypoints/ts/gift-card.ts create mode 100644 frontend/entrypoints/ts/index.ts create mode 100644 frontend/entrypoints/ts/list-collections.ts create mode 100644 frontend/entrypoints/ts/page.ts create mode 100644 frontend/entrypoints/ts/password.ts create mode 100644 frontend/entrypoints/ts/product.ts create mode 100644 frontend/entrypoints/ts/search.ts create mode 100644 frontend/entrypoints/ts/theme.ts create mode 100644 package.json delete mode 100644 sections/hello-world.liquid create mode 100644 sections/index.liquid delete mode 100644 snippets/css-variables.liquid create mode 100644 snippets/vite-tag.liquid create mode 100644 tsconfig.json create mode 100644 vite.config.js diff --git a/.agents/skills/shopify-development/README.md b/.agents/skills/shopify-development/README.md new file mode 100644 index 000000000..998f63cbc --- /dev/null +++ b/.agents/skills/shopify-development/README.md @@ -0,0 +1,60 @@ +# Shopify Development Skill + +Comprehensive skill for building on Shopify platform: apps, extensions, themes, and API integrations. + +## Features + +- **App Development** - OAuth authentication, GraphQL Admin API, webhooks, billing integration +- **UI Extensions** - Checkout, Admin, POS customizations with Polaris components +- **Theme Development** - Liquid templating, sections, snippets +- **Shopify Functions** - Custom discounts, payment, delivery rules + +## Structure + +``` +shopify-development/ +├── SKILL.md # Main skill file (AI-optimized) +├── README.md # This file +├── references/ +│ ├── app-development.md # OAuth, API, webhooks, billing +│ ├── extensions.md # UI extensions, Functions +│ └── themes.md # Liquid, theme architecture +└── scripts/ + ├── shopify_init.py # Interactive project scaffolding + ├── shopify_graphql.py # GraphQL utilities & templates + └── tests/ # Unit tests +``` + +## Validated GraphQL + +All GraphQL queries and mutations in this skill have been validated against Shopify Admin API 2026-01 schema using the official Shopify MCP. + +## Quick Start + +```bash +# Install Shopify CLI +npm install -g @shopify/cli@latest + +# Create new app +shopify app init + +# Start development +shopify app dev +``` + +## Usage Triggers + +This skill activates when the user mentions: + +- "shopify app", "shopify extension", "shopify theme" +- "checkout extension", "admin extension", "POS extension" +- "liquid template", "polaris", "shopify graphql" +- "shopify webhook", "shopify billing", "metafields" + +## API Version + +Current: **2026-01** (Quarterly releases with 12-month support) + +## License + +MIT diff --git a/.agents/skills/shopify-development/SKILL.md b/.agents/skills/shopify-development/SKILL.md new file mode 100644 index 000000000..349e3d983 --- /dev/null +++ b/.agents/skills/shopify-development/SKILL.md @@ -0,0 +1,368 @@ +--- +name: shopify-development +description: | + Build Shopify apps, extensions, themes using GraphQL Admin API, Shopify CLI, Polaris UI, and Liquid. + TRIGGER: "shopify", "shopify app", "checkout extension", "admin extension", "POS extension", + "shopify theme", "liquid template", "polaris", "shopify graphql", "shopify webhook", + "shopify billing", "app subscription", "metafields", "shopify functions" +metadata: + author: display studio +--- + +# Shopify Development Skill + +Use this skill when the user asks about: + +- Building Shopify apps or extensions +- Creating checkout/admin/POS UI customizations +- Developing themes with Liquid templating +- Integrating with Shopify GraphQL or REST APIs +- Implementing webhooks or billing +- Working with metafields or Shopify Functions + +--- + +## ROUTING: What to Build + +**IF user wants to integrate external services OR build merchant tools OR charge for features:** +→ Build an **App** (see `references/app-development.md`) + +**IF user wants to customize checkout OR add admin UI OR create POS actions OR implement discount rules:** +→ Build an **Extension** (see `references/extensions.md`) + +**IF user wants to customize storefront design OR modify product/collection pages:** +→ Build a **Theme** (see `references/themes.md`) + +**IF user needs both backend logic AND storefront UI:** +→ Build **App + Theme Extension** combination + +--- + +## Shopify CLI Commands + +Install CLI: + +```bash +npm install -g @shopify/cli@latest +``` + +Create and run app: + +```bash +shopify app init # Create new app +shopify app dev # Start dev server with tunnel +shopify app deploy # Build and upload to Shopify +``` + +Generate extension: + +```bash +shopify app generate extension --type checkout_ui_extension +shopify app generate extension --type admin_action +shopify app generate extension --type admin_block +shopify app generate extension --type pos_ui_extension +shopify app generate extension --type function +``` + +Theme development: + +```bash +shopify theme init # Create new theme +shopify theme dev # Start local preview at localhost:9292 +shopify theme pull --live # Pull live theme +shopify theme push --development # Push to dev theme +``` + +--- + +## Access Scopes + +Configure in `shopify.app.toml`: + +```toml +[access_scopes] +scopes = "read_products,write_products,read_orders,write_orders,read_customers" +``` + +Common scopes: + +- `read_products`, `write_products` - Product catalog access +- `read_orders`, `write_orders` - Order management +- `read_customers`, `write_customers` - Customer data +- `read_inventory`, `write_inventory` - Stock levels +- `read_fulfillments`, `write_fulfillments` - Order fulfillment + +--- + +## GraphQL Patterns (Validated against API 2026-01) + +### Query Products + +```graphql +query GetProducts($first: Int!, $query: String) { + products(first: $first, query: $query) { + edges { + node { + id + title + handle + status + variants(first: 5) { + edges { + node { + id + price + inventoryQuantity + } + } + } + } + } + pageInfo { + hasNextPage + endCursor + } + } +} +``` + +### Query Orders + +```graphql +query GetOrders($first: Int!) { + orders(first: $first) { + edges { + node { + id + name + createdAt + displayFinancialStatus + totalPriceSet { + shopMoney { + amount + currencyCode + } + } + } + } + } +} +``` + +### Set Metafields + +```graphql +mutation SetMetafields($metafields: [MetafieldsSetInput!]!) { + metafieldsSet(metafields: $metafields) { + metafields { + id + namespace + key + value + } + userErrors { + field + message + } + } +} +``` + +Variables example: + +```json +{ + "metafields": [ + { + "ownerId": "gid://shopify/Product/123", + "namespace": "custom", + "key": "care_instructions", + "value": "Handle with care", + "type": "single_line_text_field" + } + ] +} +``` + +--- + +## Checkout Extension Example + +```tsx +import { + reactExtension, + BlockStack, + TextField, + Checkbox, + useApplyAttributeChange, +} from "@shopify/ui-extensions-react/checkout"; + +export default reactExtension("purchase.checkout.block.render", () => ( + +)); + +function GiftMessage() { + const [isGift, setIsGift] = useState(false); + const [message, setMessage] = useState(""); + const applyAttributeChange = useApplyAttributeChange(); + + useEffect(() => { + if (isGift && message) { + applyAttributeChange({ + type: "updateAttribute", + key: "gift_message", + value: message, + }); + } + }, [isGift, message]); + + return ( + + + This is a gift + + {isGift && ( + + )} + + ); +} +``` + +--- + +## Liquid Template Example + +```liquid +{% comment %} Product Card Snippet {% endcomment %} + +``` + +--- + +## Webhook Configuration + +In `shopify.app.toml`: + +```toml +[webhooks] +api_version = "2026-01" + +[[webhooks.subscriptions]] +topics = ["orders/create", "orders/updated"] +uri = "/webhooks/orders" + +[[webhooks.subscriptions]] +topics = ["products/update"] +uri = "/webhooks/products" + +# GDPR mandatory webhooks (required for app approval) +[webhooks.privacy_compliance] +customer_data_request_url = "/webhooks/gdpr/data-request" +customer_deletion_url = "/webhooks/gdpr/customer-deletion" +shop_deletion_url = "/webhooks/gdpr/shop-deletion" +``` + +--- + +## Best Practices + +### API Usage + +- Use GraphQL over REST for new development +- Request only fields you need (reduces query cost) +- Implement cursor-based pagination with `pageInfo.endCursor` +- Use bulk operations for processing more than 250 items +- Handle rate limits with exponential backoff + +### Security + +- Store API credentials in environment variables +- Always verify webhook HMAC signatures before processing +- Validate OAuth state parameter to prevent CSRF +- Request minimal access scopes +- Use session tokens for embedded apps + +### Performance + +- Cache API responses when data doesn't change frequently +- Use lazy loading in extensions +- Optimize images in themes using `img_url` filter +- Monitor GraphQL query costs via response headers + +--- + +## Troubleshooting + +**IF you see rate limit errors:** +→ Implement exponential backoff retry logic +→ Switch to bulk operations for large datasets +→ Monitor `X-Shopify-Shop-Api-Call-Limit` header + +**IF authentication fails:** +→ Verify the access token is still valid +→ Check that all required scopes were granted +→ Ensure OAuth flow completed successfully + +**IF extension is not appearing:** +→ Verify the extension target is correct +→ Check that extension is published via `shopify app deploy` +→ Confirm the app is installed on the test store + +**IF webhook is not receiving events:** +→ Verify the webhook URL is publicly accessible +→ Check HMAC signature validation logic +→ Review webhook logs in Partner Dashboard + +**IF GraphQL query fails:** +→ Validate query against schema (use GraphiQL explorer) +→ Check for deprecated fields in error message +→ Verify you have required access scopes + +--- + +## Reference Files + +For detailed implementation guides, read these files: + +- `references/app-development.md` - OAuth authentication flow, GraphQL mutations for products/orders/billing, webhook handlers, billing API integration +- `references/extensions.md` - Checkout UI components, Admin UI extensions, POS extensions, Shopify Functions for discounts/payment/delivery +- `references/themes.md` - Liquid syntax reference, theme directory structure, sections and snippets, common patterns + +--- + +## Scripts + +- `scripts/shopify_init.py` - Interactive project scaffolding. Run: `python scripts/shopify_init.py` +- `scripts/shopify_graphql.py` - GraphQL utilities with query templates, pagination, rate limiting. Import: `from shopify_graphql import ShopifyGraphQL` + +--- + +## Official Documentation Links + +- Shopify Developer Docs: https://shopify.dev/docs +- GraphQL Admin API Reference: https://shopify.dev/docs/api/admin-graphql +- Shopify CLI Reference: https://shopify.dev/docs/api/shopify-cli +- Polaris Design System: https://polaris.shopify.com + +API Version: 2026-01 (quarterly releases, 12-month deprecation window) diff --git a/.agents/skills/shopify-development/references/app-development.md b/.agents/skills/shopify-development/references/app-development.md new file mode 100644 index 000000000..d1134c815 --- /dev/null +++ b/.agents/skills/shopify-development/references/app-development.md @@ -0,0 +1,578 @@ +# App Development Reference + +Guide for building Shopify apps with OAuth, GraphQL/REST APIs, webhooks, and billing. + +## OAuth Authentication + +### OAuth 2.0 Flow + +**1. Redirect to Authorization URL:** + +``` +https://{shop}.myshopify.com/admin/oauth/authorize? + client_id={api_key}& + scope={scopes}& + redirect_uri={redirect_uri}& + state={nonce} +``` + +**2. Handle Callback:** + +```javascript +app.get("/auth/callback", async (req, res) => { + const { code, shop, state } = req.query; + + // Verify state to prevent CSRF + if (state !== storedState) { + return res.status(403).send("Invalid state"); + } + + // Exchange code for access token + const accessToken = await exchangeCodeForToken(shop, code); + + // Store token securely + await storeAccessToken(shop, accessToken); + + res.redirect(`https://${shop}/admin/apps/${appHandle}`); +}); +``` + +**3. Exchange Code for Token:** + +```javascript +async function exchangeCodeForToken(shop, code) { + const response = await fetch(`https://${shop}/admin/oauth/access_token`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + client_id: process.env.SHOPIFY_API_KEY, + client_secret: process.env.SHOPIFY_API_SECRET, + code, + }), + }); + + const { access_token } = await response.json(); + return access_token; +} +``` + +### Access Scopes + +**Common Scopes:** + +- `read_products`, `write_products` - Product catalog +- `read_orders`, `write_orders` - Order management +- `read_customers`, `write_customers` - Customer data +- `read_inventory`, `write_inventory` - Stock levels +- `read_fulfillments`, `write_fulfillments` - Order fulfillment +- `read_shipping`, `write_shipping` - Shipping rates +- `read_analytics` - Store analytics +- `read_checkouts`, `write_checkouts` - Checkout data + +Full list: https://shopify.dev/api/usage/access-scopes + +### Session Tokens (Embedded Apps) + +For embedded apps using App Bridge: + +```javascript +import { getSessionToken } from '@shopify/app-bridge/utilities'; + +async function authenticatedFetch(url, options = {}) { + const app = createApp({ ... }); + const token = await getSessionToken(app); + + return fetch(url, { + ...options, + headers: { + ...options.headers, + 'Authorization': `Bearer ${token}` + } + }); +} +``` + +## GraphQL Admin API + +### Making Requests + +```javascript +async function graphqlRequest(shop, accessToken, query, variables = {}) { + const response = await fetch( + `https://${shop}/admin/api/2026-01/graphql.json`, + { + method: "POST", + headers: { + "X-Shopify-Access-Token": accessToken, + "Content-Type": "application/json", + }, + body: JSON.stringify({ query, variables }), + }, + ); + + const data = await response.json(); + + if (data.errors) { + throw new Error(`GraphQL errors: ${JSON.stringify(data.errors)}`); + } + + return data.data; +} +``` + +### Product Operations + +**Create Product:** + +```graphql +mutation CreateProduct($input: ProductInput!) { + productCreate(input: $input) { + product { + id + title + handle + } + userErrors { + field + message + } + } +} +``` + +Variables: + +```json +{ + "input": { + "title": "New Product", + "productType": "Apparel", + "vendor": "Brand", + "status": "ACTIVE", + "variants": [ + { "price": "29.99", "sku": "SKU-001", "inventoryQuantity": 100 } + ] + } +} +``` + +**Update Product:** + +```graphql +mutation UpdateProduct($input: ProductInput!) { + productUpdate(input: $input) { + product { + id + title + } + userErrors { + field + message + } + } +} +``` + +**Query Products:** + +```graphql +query GetProducts($first: Int!, $query: String) { + products(first: $first, query: $query) { + edges { + node { + id + title + status + variants(first: 5) { + edges { + node { + id + price + inventoryQuantity + } + } + } + } + } + pageInfo { + hasNextPage + endCursor + } + } +} +``` + +### Order Operations + +**Query Orders:** + +```graphql +query GetOrders($first: Int!) { + orders(first: $first) { + edges { + node { + id + name + createdAt + displayFinancialStatus + totalPriceSet { + shopMoney { + amount + currencyCode + } + } + customer { + email + firstName + lastName + } + } + } + } +} +``` + +**Fulfill Order:** + +```graphql +mutation FulfillOrder($fulfillment: FulfillmentInput!) { + fulfillmentCreate(fulfillment: $fulfillment) { + fulfillment { + id + status + trackingInfo { + number + url + } + } + userErrors { + field + message + } + } +} +``` + +## Webhooks + +### Configuration + +In `shopify.app.toml`: + +```toml +[webhooks] +api_version = "2025-01" + +[[webhooks.subscriptions]] +topics = ["orders/create"] +uri = "/webhooks/orders/create" + +[[webhooks.subscriptions]] +topics = ["products/update"] +uri = "/webhooks/products/update" + +[[webhooks.subscriptions]] +topics = ["app/uninstalled"] +uri = "/webhooks/app/uninstalled" + +# GDPR mandatory webhooks +[webhooks.privacy_compliance] +customer_data_request_url = "/webhooks/gdpr/data-request" +customer_deletion_url = "/webhooks/gdpr/customer-deletion" +shop_deletion_url = "/webhooks/gdpr/shop-deletion" +``` + +### Webhook Handler + +```javascript +import crypto from "crypto"; + +function verifyWebhook(req) { + const hmac = req.headers["x-shopify-hmac-sha256"]; + const body = req.rawBody; // Raw body buffer + + const hash = crypto + .createHmac("sha256", process.env.SHOPIFY_API_SECRET) + .update(body, "utf8") + .digest("base64"); + + return hmac === hash; +} + +app.post("/webhooks/orders/create", async (req, res) => { + if (!verifyWebhook(req)) { + return res.status(401).send("Unauthorized"); + } + + const order = req.body; + console.log("New order:", order.id, order.name); + + // Process order... + + res.status(200).send("OK"); +}); +``` + +### Common Webhook Topics + +**Orders:** + +- `orders/create`, `orders/updated`, `orders/delete` +- `orders/paid`, `orders/cancelled`, `orders/fulfilled` + +**Products:** + +- `products/create`, `products/update`, `products/delete` + +**Customers:** + +- `customers/create`, `customers/update`, `customers/delete` + +**Inventory:** + +- `inventory_levels/update` + +**App:** + +- `app/uninstalled` (critical for cleanup) + +## Billing Integration + +### App Charges + +**One-time Charge:** + +```graphql +mutation CreateCharge($input: AppPurchaseOneTimeInput!) { + appPurchaseOneTimeCreate(input: $input) { + appPurchaseOneTime { + id + name + price { + amount + } + status + confirmationUrl + } + userErrors { + field + message + } + } +} +``` + +Variables: + +```json +{ + "input": { + "name": "Premium Feature", + "price": { "amount": 49.99, "currencyCode": "USD" }, + "returnUrl": "https://your-app.com/billing/callback" + } +} +``` + +**Recurring Charge (Subscription):** + +```graphql +mutation CreateSubscription( + $name: String! + $returnUrl: URL! + $lineItems: [AppSubscriptionLineItemInput!]! + $trialDays: Int +) { + appSubscriptionCreate( + name: $name + returnUrl: $returnUrl + lineItems: $lineItems + trialDays: $trialDays + ) { + appSubscription { + id + name + status + } + confirmationUrl + userErrors { + field + message + } + } +} +``` + +Variables: + +```json +{ + "name": "Monthly Subscription", + "returnUrl": "https://your-app.com/billing/callback", + "trialDays": 7, + "lineItems": [ + { + "plan": { + "appRecurringPricingDetails": { + "price": { "amount": 29.99, "currencyCode": "USD" }, + "interval": "EVERY_30_DAYS" + } + } + } + ] +} +``` + +**Usage-based Billing:** + +```graphql +mutation CreateUsageCharge( + $subscriptionLineItemId: ID! + $price: MoneyInput! + $description: String! +) { + appUsageRecordCreate( + subscriptionLineItemId: $subscriptionLineItemId + price: $price + description: $description + ) { + appUsageRecord { + id + price { + amount + currencyCode + } + description + } + userErrors { + field + message + } + } +} +``` + +Variables: + +```json +{ + "subscriptionLineItemId": "gid://shopify/AppSubscriptionLineItem/123", + "price": { "amount": "5.00", "currencyCode": "USD" }, + "description": "100 API calls used" +} +``` + +## Metafields + +### Create/Update Metafields + +```graphql +mutation SetMetafields($metafields: [MetafieldsSetInput!]!) { + metafieldsSet(metafields: $metafields) { + metafields { + id + namespace + key + value + } + userErrors { + field + message + } + } +} +``` + +Variables: + +```json +{ + "metafields": [ + { + "ownerId": "gid://shopify/Product/123", + "namespace": "custom", + "key": "instructions", + "value": "Handle with care", + "type": "single_line_text_field" + } + ] +} +``` + +**Metafield Types:** + +- `single_line_text_field`, `multi_line_text_field` +- `number_integer`, `number_decimal` +- `date`, `date_time` +- `url`, `json` +- `file_reference`, `product_reference` + +## Rate Limiting + +### GraphQL Cost-Based Limits + +**Limits:** + +- Available points: 2000 +- Restore rate: 100 points/second +- Max query cost: 2000 + +**Check Cost:** + +```javascript +const response = await graphqlRequest(shop, token, query); +const cost = response.extensions?.cost; + +console.log( + `Cost: ${cost.actualQueryCost}/${cost.throttleStatus.maximumAvailable}`, +); +``` + +**Handle Throttling:** + +```javascript +async function graphqlWithRetry(shop, token, query, retries = 3) { + for (let i = 0; i < retries; i++) { + try { + return await graphqlRequest(shop, token, query); + } catch (error) { + if (error.message.includes("Throttled") && i < retries - 1) { + await sleep(Math.pow(2, i) * 1000); // Exponential backoff + continue; + } + throw error; + } + } +} +``` + +## Best Practices + +**Security:** + +- Store credentials in environment variables +- Verify webhook HMAC signatures +- Validate OAuth state parameter +- Use HTTPS for all endpoints +- Implement rate limiting on your endpoints + +**Performance:** + +- Cache access tokens securely +- Use bulk operations for large datasets +- Implement pagination for queries +- Monitor GraphQL query costs + +**Reliability:** + +- Implement exponential backoff for retries +- Handle webhook delivery failures +- Log errors for debugging +- Monitor app health metrics + +**Compliance:** + +- Implement GDPR webhooks (mandatory) +- Handle customer data deletion requests +- Provide data export functionality +- Follow data retention policies diff --git a/.agents/skills/shopify-development/references/extensions.md b/.agents/skills/shopify-development/references/extensions.md new file mode 100644 index 000000000..8d42267f6 --- /dev/null +++ b/.agents/skills/shopify-development/references/extensions.md @@ -0,0 +1,555 @@ +# Extensions Reference + +Guide for building UI extensions and Shopify Functions. + +## Checkout UI Extensions + +Customize checkout and thank-you pages with native-rendered components. + +### Extension Points + +**Block Targets (Merchant-Configurable):** + +- `purchase.checkout.block.render` - Main checkout +- `purchase.thank-you.block.render` - Thank you page + +**Static Targets (Fixed Position):** + +- `purchase.checkout.header.render-after` +- `purchase.checkout.contact.render-before` +- `purchase.checkout.shipping-option-list.render-after` +- `purchase.checkout.payment-method-list.render-after` +- `purchase.checkout.footer.render-before` + +### Setup + +```bash +shopify app generate extension --type checkout_ui_extension +``` + +Configuration (`shopify.extension.toml`): + +```toml +api_version = "2026-01" +name = "gift-message" +type = "ui_extension" + +[[extensions.targeting]] +target = "purchase.checkout.block.render" + +[capabilities] +network_access = true +api_access = true +``` + +### Basic Example + +```javascript +import { + reactExtension, + BlockStack, + TextField, + Checkbox, + useApi, +} from "@shopify/ui-extensions-react/checkout"; + +export default reactExtension("purchase.checkout.block.render", () => ( + +)); + +function Extension() { + const [message, setMessage] = useState(""); + const [isGift, setIsGift] = useState(false); + const { applyAttributeChange } = useApi(); + + useEffect(() => { + if (isGift) { + applyAttributeChange({ + type: "updateAttribute", + key: "gift_message", + value: message, + }); + } + }, [message, isGift]); + + return ( + + + This is a gift + + {isGift && ( + + )} + + ); +} +``` + +### Common Hooks + +**useApi:** + +```javascript +const { extensionPoint, shop, storefront, i18n, sessionToken } = useApi(); +``` + +**useCartLines:** + +```javascript +const lines = useCartLines(); +lines.forEach((line) => { + console.log(line.merchandise.product.title, line.quantity); +}); +``` + +**useShippingAddress:** + +```javascript +const address = useShippingAddress(); +console.log(address.city, address.countryCode); +``` + +**useApplyCartLinesChange:** + +```javascript +const applyChange = useApplyCartLinesChange(); + +async function addItem() { + await applyChange({ + type: "addCartLine", + merchandiseId: "gid://shopify/ProductVariant/123", + quantity: 1, + }); +} +``` + +### Core Components + +**Layout:** + +- `BlockStack` - Vertical stacking +- `InlineStack` - Horizontal layout +- `Grid`, `GridItem` - Grid layout +- `View` - Container +- `Divider` - Separator + +**Input:** + +- `TextField` - Text input +- `Checkbox` - Boolean +- `Select` - Dropdown +- `DatePicker` - Date selection +- `Form` - Form wrapper + +**Display:** + +- `Text`, `Heading` - Typography +- `Banner` - Messages +- `Badge` - Status +- `Image` - Images +- `Link` - Hyperlinks +- `List`, `ListItem` - Lists + +**Interactive:** + +- `Button` - Actions +- `Modal` - Overlays +- `Pressable` - Click areas + +## Admin UI Extensions + +Extend Shopify admin interface. + +### Admin Action + +Custom actions on resource pages. + +```bash +shopify app generate extension --type admin_action +``` + +```javascript +import { + reactExtension, + AdminAction, + Button, +} from "@shopify/ui-extensions-react/admin"; + +export default reactExtension("admin.product-details.action.render", () => ( + +)); + +function Extension() { + const { data } = useData(); + + async function handleExport() { + const response = await fetch("/api/export", { + method: "POST", + body: JSON.stringify({ productId: data.product.id }), + }); + console.log("Exported:", await response.json()); + } + + return ( + Export} + /> + ); +} +``` + +**Targets:** + +- `admin.product-details.action.render` +- `admin.order-details.action.render` +- `admin.customer-details.action.render` + +### Admin Block + +Embedded content in admin pages. + +```javascript +import { + reactExtension, + BlockStack, + Text, + Badge, +} from "@shopify/ui-extensions-react/admin"; + +export default reactExtension("admin.product-details.block.render", () => ( + +)); + +function Extension() { + const { data } = useData(); + const [analytics, setAnalytics] = useState(null); + + useEffect(() => { + fetchAnalytics(data.product.id).then(setAnalytics); + }, []); + + return ( + + Product Analytics + Views: {analytics?.views || 0} + Conversions: {analytics?.conversions || 0} + + {analytics?.trending ? "Trending" : "Normal"} + + + ); +} +``` + +**Targets:** + +- `admin.product-details.block.render` +- `admin.order-details.block.render` +- `admin.customer-details.block.render` + +## POS UI Extensions + +Customize Point of Sale experience. + +### Smart Grid Tile + +Quick access action on POS home screen. + +```javascript +import { + reactExtension, + SmartGridTile, +} from "@shopify/ui-extensions-react/pos"; + +export default reactExtension("pos.home.tile.render", () => ); + +function Extension() { + function handlePress() { + // Navigate to custom workflow + } + + return ( + + ); +} +``` + +### POS Modal + +Full-screen workflow. + +```javascript +import { + reactExtension, + Screen, + BlockStack, + Button, + TextField, +} from "@shopify/ui-extensions-react/pos"; + +export default reactExtension("pos.home.modal.render", () => ); + +function Extension() { + const { navigation } = useApi(); + const [amount, setAmount] = useState(""); + + function handleIssue() { + // Issue gift card + navigation.pop(); + } + + return ( + + + + + + + + ); +} +``` + +## Customer Account Extensions + +Customize customer account pages. + +### Order Status Extension + +```javascript +import { + reactExtension, + BlockStack, + Text, + Button, +} from "@shopify/ui-extensions-react/customer-account"; + +export default reactExtension( + "customer-account.order-status.block.render", + () => , +); + +function Extension() { + const { order } = useApi(); + + function handleReturn() { + // Initiate return + } + + return ( + + Need to return? + Start return for order {order.name} + + + ); +} +``` + +**Targets:** + +- `customer-account.order-status.block.render` +- `customer-account.order-index.block.render` +- `customer-account.profile.block.render` + +## Shopify Functions + +Serverless backend customization. + +### Function Types + +**Discounts:** + +- `order_discount` - Order-level discounts +- `product_discount` - Product-specific discounts +- `shipping_discount` - Shipping discounts + +**Payment Customization:** + +- Hide/rename/reorder payment methods + +**Delivery Customization:** + +- Custom shipping options +- Delivery rules + +**Validation:** + +- Cart validation rules +- Checkout validation + +### Create Function + +```bash +shopify app generate extension --type function +``` + +### Order Discount Function + +```javascript +// input.graphql +query Input { + cart { + lines { + quantity + merchandise { + ... on ProductVariant { + product { + hasTag(tag: "bulk-discount") + } + } + } + } + } +} + +// function.js +export default function orderDiscount(input) { + const targets = input.cart.lines + .filter(line => line.merchandise.product.hasTag) + .map(line => ({ + productVariant: { id: line.merchandise.id } + })); + + if (targets.length === 0) { + return { discounts: [] }; + } + + return { + discounts: [{ + targets, + value: { + percentage: { + value: 10 // 10% discount + } + } + }] + }; +} +``` + +### Payment Customization Function + +```javascript +export default function paymentCustomization(input) { + const hidePaymentMethods = input.cart.lines.some( + (line) => line.merchandise.product.hasTag, + ); + + if (!hidePaymentMethods) { + return { operations: [] }; + } + + return { + operations: [ + { + hide: { + paymentMethodId: "gid://shopify/PaymentMethod/123", + }, + }, + ], + }; +} +``` + +### Validation Function + +```javascript +export default function cartValidation(input) { + const errors = []; + + // Max 5 items per cart + if (input.cart.lines.length > 5) { + errors.push({ + localizedMessage: "Maximum 5 items allowed per order", + target: "cart", + }); + } + + // Min $50 for wholesale + const isWholesale = input.cart.lines.some( + (line) => line.merchandise.product.hasTag, + ); + + if (isWholesale && input.cart.cost.totalAmount.amount < 50) { + errors.push({ + localizedMessage: "Wholesale orders require $50 minimum", + target: "cart", + }); + } + + return { errors }; +} +``` + +## Network Requests + +Extensions can call external APIs. + +```javascript +import { useApi } from "@shopify/ui-extensions-react/checkout"; + +function Extension() { + const { sessionToken } = useApi(); + + async function fetchData() { + const token = await sessionToken.get(); + + const response = await fetch("https://your-app.com/api/data", { + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + }); + + return await response.json(); + } +} +``` + +## Best Practices + +**Performance:** + +- Lazy load data +- Memoize expensive computations +- Use loading states +- Minimize re-renders + +**UX:** + +- Provide clear error messages +- Show loading indicators +- Validate inputs +- Support keyboard navigation + +**Security:** + +- Verify session tokens on backend +- Sanitize user input +- Use HTTPS for all requests +- Don't expose sensitive data + +**Testing:** + +- Test on development stores +- Verify mobile/desktop +- Check accessibility +- Test edge cases + +## Resources + +- Checkout Extensions: https://shopify.dev/docs/api/checkout-extensions +- Admin Extensions: https://shopify.dev/docs/apps/admin/extensions +- Functions: https://shopify.dev/docs/apps/functions +- Components: https://shopify.dev/docs/api/checkout-ui-extensions/components diff --git a/.agents/skills/shopify-development/references/themes.md b/.agents/skills/shopify-development/references/themes.md new file mode 100644 index 000000000..2fc1c2b98 --- /dev/null +++ b/.agents/skills/shopify-development/references/themes.md @@ -0,0 +1,498 @@ +# Themes Reference + +Guide for developing Shopify themes with Liquid templating. + +## Liquid Templating + +### Syntax Basics + +**Objects (Output):** +```liquid +{{ product.title }} +{{ product.price | money }} +{{ customer.email }} +``` + +**Tags (Logic):** +```liquid +{% if product.available %} + +{% else %} +

Sold Out

+{% endif %} + +{% for product in collection.products %} + {{ product.title }} +{% endfor %} + +{% case product.type %} + {% when 'Clothing' %} + Apparel + {% when 'Shoes' %} + Footwear + {% else %} + Other +{% endcase %} +``` + +**Filters (Transform):** +```liquid +{{ product.title | upcase }} +{{ product.price | money }} +{{ product.description | strip_html | truncate: 100 }} +{{ product.image | img_url: 'medium' }} +{{ 'now' | date: '%B %d, %Y' }} +``` + +### Common Objects + +**Product:** +```liquid +{{ product.id }} +{{ product.title }} +{{ product.handle }} +{{ product.description }} +{{ product.price }} +{{ product.compare_at_price }} +{{ product.available }} +{{ product.type }} +{{ product.vendor }} +{{ product.tags }} +{{ product.images }} +{{ product.variants }} +{{ product.featured_image }} +{{ product.url }} +``` + +**Collection:** +```liquid +{{ collection.title }} +{{ collection.handle }} +{{ collection.description }} +{{ collection.products }} +{{ collection.products_count }} +{{ collection.image }} +{{ collection.url }} +``` + +**Cart:** +```liquid +{{ cart.item_count }} +{{ cart.total_price }} +{{ cart.items }} +{{ cart.note }} +{{ cart.attributes }} +``` + +**Customer:** +```liquid +{{ customer.email }} +{{ customer.first_name }} +{{ customer.last_name }} +{{ customer.orders_count }} +{{ customer.total_spent }} +{{ customer.addresses }} +{{ customer.default_address }} +``` + +**Shop:** +```liquid +{{ shop.name }} +{{ shop.email }} +{{ shop.domain }} +{{ shop.currency }} +{{ shop.money_format }} +{{ shop.enabled_payment_types }} +``` + +### Common Filters + +**String:** +- `upcase`, `downcase`, `capitalize` +- `strip_html`, `strip_newlines` +- `truncate: 100`, `truncatewords: 20` +- `replace: 'old', 'new'` + +**Number:** +- `money` - Format currency +- `round`, `ceil`, `floor` +- `times`, `divided_by`, `plus`, `minus` + +**Array:** +- `join: ', '` +- `first`, `last` +- `size` +- `map: 'property'` +- `where: 'property', 'value'` + +**URL:** +- `img_url: 'size'` - Image URL +- `url_for_type`, `url_for_vendor` +- `link_to`, `link_to_type` + +**Date:** +- `date: '%B %d, %Y'` + +## Theme Architecture + +### Directory Structure + +``` +theme/ +├── assets/ # CSS, JS, images +├── config/ # Theme settings +│ ├── settings_schema.json +│ └── settings_data.json +├── layout/ # Base templates +│ └── theme.liquid +├── locales/ # Translations +│ └── en.default.json +├── sections/ # Reusable blocks +│ ├── header.liquid +│ ├── footer.liquid +│ └── product-grid.liquid +├── snippets/ # Small components +│ ├── product-card.liquid +│ └── icon.liquid +└── templates/ # Page templates + ├── index.json + ├── product.json + ├── collection.json + └── cart.liquid +``` + +### Layout + +Base template wrapping all pages (`layout/theme.liquid`): + +```liquid + + + + + + {{ page_title }} + + {{ content_for_header }} + + + + + {% section 'header' %} + +
+ {{ content_for_layout }} +
+ + {% section 'footer' %} + + + + +``` + +### Templates + +Page-specific structures (`templates/product.json`): + +```json +{ + "sections": { + "main": { + "type": "product-template", + "settings": { + "show_vendor": true, + "show_quantity_selector": true + } + }, + "recommendations": { + "type": "product-recommendations" + } + }, + "order": ["main", "recommendations"] +} +``` + +Legacy format (`templates/product.liquid`): +```liquid +
+
+ {{ product.title }} +
+ +
+

{{ product.title }}

+

{{ product.price | money }}

+ + {% form 'product', product %} + + + + {% endform %} +
+
+``` + +### Sections + +Reusable content blocks (`sections/product-grid.liquid`): + +```liquid +
+ {% for product in section.settings.collection.products %} + + {% endfor %} +
+ +{% schema %} +{ + "name": "Product Grid", + "settings": [ + { + "type": "collection", + "id": "collection", + "label": "Collection" + }, + { + "type": "range", + "id": "products_per_row", + "min": 2, + "max": 5, + "step": 1, + "default": 4, + "label": "Products per row" + } + ], + "presets": [ + { + "name": "Product Grid" + } + ] +} +{% endschema %} +``` + +### Snippets + +Small reusable components (`snippets/product-card.liquid`): + +```liquid + +``` + +Include snippet: +```liquid +{% render 'product-card', product: product %} +``` + +## Development Workflow + +### Setup + +```bash +# Initialize new theme +shopify theme init + +# Choose Dawn (reference theme) or blank +``` + +### Local Development + +```bash +# Start local server +shopify theme dev + +# Preview at http://localhost:9292 +# Changes auto-sync to development theme +``` + +### Pull Theme + +```bash +# Pull live theme +shopify theme pull --live + +# Pull specific theme +shopify theme pull --theme=123456789 + +# Pull only templates +shopify theme pull --only=templates +``` + +### Push Theme + +```bash +# Push to development theme +shopify theme push --development + +# Create new unpublished theme +shopify theme push --unpublished + +# Push specific files +shopify theme push --only=sections,snippets +``` + +### Theme Check + +Lint theme code: +```bash +shopify theme check +shopify theme check --auto-correct +``` + +## Common Patterns + +### Product Form with Variants + +```liquid +{% form 'product', product %} + {% unless product.has_only_default_variant %} + {% for option in product.options_with_values %} +
+ + +
+ {% endfor %} + {% endunless %} + + + + + +{% endform %} +``` + +### Pagination + +```liquid +{% paginate collection.products by 12 %} + {% for product in collection.products %} + {% render 'product-card', product: product %} + {% endfor %} + + {% if paginate.pages > 1 %} + + {% endif %} +{% endpaginate %} +``` + +### Cart AJAX + +```javascript +// Add to cart +fetch('/cart/add.js', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + id: variantId, + quantity: 1 + }) +}) +.then(res => res.json()) +.then(item => console.log('Added:', item)); + +// Get cart +fetch('/cart.js') + .then(res => res.json()) + .then(cart => console.log('Cart:', cart)); + +// Update cart +fetch('/cart/change.js', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + id: lineItemKey, + quantity: 2 + }) +}) +.then(res => res.json()); +``` + +## Metafields in Themes + +Access custom data: + +```liquid +{{ product.metafields.custom.care_instructions }} +{{ product.metafields.custom.material.value }} + +{% if product.metafields.custom.featured %} + Featured +{% endif %} +``` + +## Best Practices + +**Performance:** +- Optimize images (use appropriate sizes) +- Minimize Liquid logic complexity +- Use lazy loading for images +- Defer non-critical JavaScript + +**Accessibility:** +- Use semantic HTML +- Include alt text for images +- Support keyboard navigation +- Ensure sufficient color contrast + +**SEO:** +- Use descriptive page titles +- Include meta descriptions +- Structure content with headings +- Implement schema markup + +**Code Quality:** +- Follow Shopify theme guidelines +- Use consistent naming conventions +- Comment complex logic +- Keep sections focused and reusable + +## Resources + +- Theme Development: https://shopify.dev/docs/themes +- Liquid Reference: https://shopify.dev/docs/api/liquid +- Dawn Theme: https://github.com/Shopify/dawn +- Theme Check: https://shopify.dev/docs/themes/tools/theme-check diff --git a/.agents/skills/shopify-development/scripts/.gitignore b/.agents/skills/shopify-development/scripts/.gitignore new file mode 100644 index 000000000..8abb6f180 --- /dev/null +++ b/.agents/skills/shopify-development/scripts/.gitignore @@ -0,0 +1,49 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# Testing +.coverage +.pytest_cache/ +htmlcov/ +.tox/ +.nox/ +coverage.xml +*.cover +*.py,cover + +# Environments +.env +.venv +env/ +venv/ +ENV/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db diff --git a/.agents/skills/shopify-development/scripts/requirements.txt b/.agents/skills/shopify-development/scripts/requirements.txt new file mode 100644 index 000000000..4613a2baa --- /dev/null +++ b/.agents/skills/shopify-development/scripts/requirements.txt @@ -0,0 +1,19 @@ +# Shopify Skill Dependencies +# Python 3.10+ required + +# No Python package dependencies - uses only standard library + +# Testing dependencies (dev) +pytest>=8.0.0 +pytest-cov>=4.1.0 +pytest-mock>=3.12.0 + +# Note: This script requires the Shopify CLI tool +# Install Shopify CLI: +# npm install -g @shopify/cli @shopify/theme +# or via Homebrew (macOS): +# brew tap shopify/shopify +# brew install shopify-cli +# +# Authenticate with: +# shopify auth login diff --git a/.agents/skills/shopify-development/scripts/shopify_graphql.py b/.agents/skills/shopify-development/scripts/shopify_graphql.py new file mode 100644 index 000000000..ec8af3cb8 --- /dev/null +++ b/.agents/skills/shopify-development/scripts/shopify_graphql.py @@ -0,0 +1,428 @@ +#!/usr/bin/env python3 +""" +Shopify GraphQL Utilities + +Helper functions for common Shopify GraphQL operations. +Provides query templates, pagination helpers, and rate limit handling. + +Usage: + from shopify_graphql import ShopifyGraphQL + + client = ShopifyGraphQL(shop_domain, access_token) + products = client.get_products(first=10) +""" + +import os +import time +import json +from typing import Dict, List, Optional, Any, Generator +from dataclasses import dataclass +from urllib.request import Request, urlopen +from urllib.error import HTTPError + + +# API Configuration +API_VERSION = "2026-01" +MAX_RETRIES = 3 +RETRY_DELAY = 1.0 # seconds + + +@dataclass +class GraphQLResponse: + """Container for GraphQL response data.""" + data: Optional[Dict[str, Any]] = None + errors: Optional[List[Dict[str, Any]]] = None + extensions: Optional[Dict[str, Any]] = None + + @property + def is_success(self) -> bool: + return self.errors is None or len(self.errors) == 0 + + @property + def query_cost(self) -> Optional[int]: + """Get the actual query cost from extensions.""" + if self.extensions and 'cost' in self.extensions: + return self.extensions['cost'].get('actualQueryCost') + return None + + +class ShopifyGraphQL: + """ + Shopify GraphQL API client with built-in utilities. + + Features: + - Query templates for common operations + - Automatic pagination + - Rate limit handling with exponential backoff + - Response parsing helpers + """ + + def __init__(self, shop_domain: str, access_token: str): + """ + Initialize the GraphQL client. + + Args: + shop_domain: Store domain (e.g., 'my-store.myshopify.com') + access_token: Admin API access token + """ + self.shop_domain = shop_domain.replace('https://', '').replace('http://', '') + self.access_token = access_token + self.base_url = f"https://{self.shop_domain}/admin/api/{API_VERSION}/graphql.json" + + def execute(self, query: str, variables: Optional[Dict] = None) -> GraphQLResponse: + """ + Execute a GraphQL query/mutation. + + Args: + query: GraphQL query string + variables: Query variables + + Returns: + GraphQLResponse object + """ + payload = {"query": query} + if variables: + payload["variables"] = variables + + headers = { + "Content-Type": "application/json", + "X-Shopify-Access-Token": self.access_token + } + + for attempt in range(MAX_RETRIES): + try: + request = Request( + self.base_url, + data=json.dumps(payload).encode('utf-8'), + headers=headers, + method='POST' + ) + + with urlopen(request, timeout=30) as response: + result = json.loads(response.read().decode('utf-8')) + return GraphQLResponse( + data=result.get('data'), + errors=result.get('errors'), + extensions=result.get('extensions') + ) + + except HTTPError as e: + if e.code == 429: # Rate limited + delay = RETRY_DELAY * (2 ** attempt) + print(f"Rate limited. Retrying in {delay}s...") + time.sleep(delay) + continue + raise + except Exception as e: + if attempt == MAX_RETRIES - 1: + raise + time.sleep(RETRY_DELAY) + + return GraphQLResponse(errors=[{"message": "Max retries exceeded"}]) + + # ==================== Query Templates ==================== + + def get_products( + self, + first: int = 10, + query: Optional[str] = None, + after: Optional[str] = None + ) -> GraphQLResponse: + """ + Query products with pagination. + + Args: + first: Number of products to fetch (max 250) + query: Optional search query + after: Cursor for pagination + """ + gql = """ + query GetProducts($first: Int!, $query: String, $after: String) { + products(first: $first, query: $query, after: $after) { + edges { + node { + id + title + handle + status + totalInventory + variants(first: 5) { + edges { + node { + id + title + price + inventoryQuantity + sku + } + } + } + } + cursor + } + pageInfo { + hasNextPage + endCursor + } + } + } + """ + return self.execute(gql, {"first": first, "query": query, "after": after}) + + def get_orders( + self, + first: int = 10, + query: Optional[str] = None, + after: Optional[str] = None + ) -> GraphQLResponse: + """ + Query orders with pagination. + + Args: + first: Number of orders to fetch (max 250) + query: Optional search query (e.g., "financial_status:paid") + after: Cursor for pagination + """ + gql = """ + query GetOrders($first: Int!, $query: String, $after: String) { + orders(first: $first, query: $query, after: $after) { + edges { + node { + id + name + createdAt + displayFinancialStatus + displayFulfillmentStatus + totalPriceSet { + shopMoney { amount currencyCode } + } + customer { + id + firstName + lastName + } + lineItems(first: 5) { + edges { + node { + title + quantity + } + } + } + } + cursor + } + pageInfo { + hasNextPage + endCursor + } + } + } + """ + return self.execute(gql, {"first": first, "query": query, "after": after}) + + def get_customers( + self, + first: int = 10, + query: Optional[str] = None, + after: Optional[str] = None + ) -> GraphQLResponse: + """ + Query customers with pagination. + + Args: + first: Number of customers to fetch (max 250) + query: Optional search query + after: Cursor for pagination + """ + gql = """ + query GetCustomers($first: Int!, $query: String, $after: String) { + customers(first: $first, query: $query, after: $after) { + edges { + node { + id + firstName + lastName + displayName + defaultEmailAddress { + emailAddress + } + numberOfOrders + amountSpent { + amount + currencyCode + } + } + cursor + } + pageInfo { + hasNextPage + endCursor + } + } + } + """ + return self.execute(gql, {"first": first, "query": query, "after": after}) + + def set_metafields(self, metafields: List[Dict]) -> GraphQLResponse: + """ + Set metafields on resources. + + Args: + metafields: List of metafield inputs, each containing: + - ownerId: Resource GID + - namespace: Metafield namespace + - key: Metafield key + - value: Metafield value + - type: Metafield type + """ + gql = """ + mutation SetMetafields($metafields: [MetafieldsSetInput!]!) { + metafieldsSet(metafields: $metafields) { + metafields { + id + namespace + key + value + } + userErrors { + field + message + } + } + } + """ + return self.execute(gql, {"metafields": metafields}) + + # ==================== Pagination Helpers ==================== + + def paginate_products( + self, + batch_size: int = 50, + query: Optional[str] = None + ) -> Generator[Dict, None, None]: + """ + Generator that yields all products with automatic pagination. + + Args: + batch_size: Products per request (max 250) + query: Optional search query + + Yields: + Product dictionaries + """ + cursor = None + while True: + response = self.get_products(first=batch_size, query=query, after=cursor) + + if not response.is_success or not response.data: + break + + products = response.data.get('products', {}) + edges = products.get('edges', []) + + for edge in edges: + yield edge['node'] + + page_info = products.get('pageInfo', {}) + if not page_info.get('hasNextPage'): + break + + cursor = page_info.get('endCursor') + + def paginate_orders( + self, + batch_size: int = 50, + query: Optional[str] = None + ) -> Generator[Dict, None, None]: + """ + Generator that yields all orders with automatic pagination. + + Args: + batch_size: Orders per request (max 250) + query: Optional search query + + Yields: + Order dictionaries + """ + cursor = None + while True: + response = self.get_orders(first=batch_size, query=query, after=cursor) + + if not response.is_success or not response.data: + break + + orders = response.data.get('orders', {}) + edges = orders.get('edges', []) + + for edge in edges: + yield edge['node'] + + page_info = orders.get('pageInfo', {}) + if not page_info.get('hasNextPage'): + break + + cursor = page_info.get('endCursor') + + +# ==================== Utility Functions ==================== + +def extract_id(gid: str) -> str: + """ + Extract numeric ID from Shopify GID. + + Args: + gid: Global ID (e.g., 'gid://shopify/Product/123') + + Returns: + Numeric ID string (e.g., '123') + """ + return gid.split('/')[-1] if gid else '' + + +def build_gid(resource_type: str, id: str) -> str: + """ + Build Shopify GID from resource type and ID. + + Args: + resource_type: Resource type (e.g., 'Product', 'Order') + id: Numeric ID + + Returns: + Global ID (e.g., 'gid://shopify/Product/123') + """ + return f"gid://shopify/{resource_type}/{id}" + + +# ==================== Example Usage ==================== + +def main(): + """Example usage of ShopifyGraphQL client.""" + import os + + # Load from environment + shop = os.environ.get('SHOP_DOMAIN', 'your-store.myshopify.com') + token = os.environ.get('SHOPIFY_ACCESS_TOKEN', '') + + if not token: + print("Set SHOPIFY_ACCESS_TOKEN environment variable") + return + + client = ShopifyGraphQL(shop, token) + + # Example: Get first 5 products + print("Fetching products...") + response = client.get_products(first=5) + + if response.is_success: + products = response.data['products']['edges'] + for edge in products: + product = edge['node'] + print(f" - {product['title']} ({product['status']})") + print(f"\nQuery cost: {response.query_cost}") + else: + print(f"Errors: {response.errors}") + + +if __name__ == '__main__': + main() diff --git a/.agents/skills/shopify-development/scripts/shopify_init.py b/.agents/skills/shopify-development/scripts/shopify_init.py new file mode 100644 index 000000000..f0c664e12 --- /dev/null +++ b/.agents/skills/shopify-development/scripts/shopify_init.py @@ -0,0 +1,441 @@ +#!/usr/bin/env python3 +""" +Shopify Project Initialization Script + +Interactive script to scaffold Shopify apps, extensions, or themes. +Supports environment variable loading from multiple locations. +""" + +import os +import sys +import json +import subprocess +from pathlib import Path +from typing import Dict, Optional, List +from dataclasses import dataclass + + +@dataclass +class EnvConfig: + """Environment configuration container.""" + shopify_api_key: Optional[str] = None + shopify_api_secret: Optional[str] = None + shop_domain: Optional[str] = None + scopes: Optional[str] = None + + +class EnvLoader: + """Load environment variables from multiple sources in priority order.""" + + @staticmethod + def load_env_file(filepath: Path) -> Dict[str, str]: + """ + Load environment variables from .env file. + + Args: + filepath: Path to .env file + + Returns: + Dictionary of environment variables + """ + env_vars = {} + if not filepath.exists(): + return env_vars + + try: + with open(filepath, 'r') as f: + for line in f: + line = line.strip() + if line and not line.startswith('#') and '=' in line: + key, value = line.split('=', 1) + env_vars[key.strip()] = value.strip().strip('"').strip("'") + except Exception as e: + print(f"Warning: Failed to load {filepath}: {e}") + + return env_vars + + @staticmethod + def get_env_paths(skill_dir: Path) -> List[Path]: + """ + Get list of .env file paths in priority order. + + Works with any AI tool directory structure: + - .agent/skills/ (universal) + - .claude/skills/ (Claude Code) + - .gemini/skills/ (Gemini CLI) + - .cursor/skills/ (Cursor) + + Priority: process.env > skill/.env > skills/.env > agent_dir/.env + + Args: + skill_dir: Path to skill directory + + Returns: + List of .env file paths + """ + paths = [] + + # skill/.env + skill_env = skill_dir / '.env' + if skill_env.exists(): + paths.append(skill_env) + + # skills/.env + skills_env = skill_dir.parent / '.env' + if skills_env.exists(): + paths.append(skills_env) + + # agent_dir/.env (e.g., .agent, .claude, .gemini, .cursor) + agent_env = skill_dir.parent.parent / '.env' + if agent_env.exists(): + paths.append(agent_env) + + return paths + + @staticmethod + def load_config(skill_dir: Path) -> EnvConfig: + """ + Load configuration from environment variables. + + Works with any AI tool directory structure. + Priority: process.env > skill/.env > skills/.env > agent_dir/.env + + Args: + skill_dir: Path to skill directory + + Returns: + EnvConfig object + """ + config = EnvConfig() + + # Load from .env files (reverse priority order) + for env_path in reversed(EnvLoader.get_env_paths(skill_dir)): + env_vars = EnvLoader.load_env_file(env_path) + if 'SHOPIFY_API_KEY' in env_vars: + config.shopify_api_key = env_vars['SHOPIFY_API_KEY'] + if 'SHOPIFY_API_SECRET' in env_vars: + config.shopify_api_secret = env_vars['SHOPIFY_API_SECRET'] + if 'SHOP_DOMAIN' in env_vars: + config.shop_domain = env_vars['SHOP_DOMAIN'] + if 'SCOPES' in env_vars: + config.scopes = env_vars['SCOPES'] + + # Override with process environment (highest priority) + if 'SHOPIFY_API_KEY' in os.environ: + config.shopify_api_key = os.environ['SHOPIFY_API_KEY'] + if 'SHOPIFY_API_SECRET' in os.environ: + config.shopify_api_secret = os.environ['SHOPIFY_API_SECRET'] + if 'SHOP_DOMAIN' in os.environ: + config.shop_domain = os.environ['SHOP_DOMAIN'] + if 'SCOPES' in os.environ: + config.scopes = os.environ['SCOPES'] + + return config + + +class ShopifyInitializer: + """Initialize Shopify projects.""" + + def __init__(self, config: EnvConfig): + """ + Initialize ShopifyInitializer. + + Args: + config: Environment configuration + """ + self.config = config + + def prompt(self, message: str, default: Optional[str] = None) -> str: + """ + Prompt user for input. + + Args: + message: Prompt message + default: Default value + + Returns: + User input or default + """ + if default: + message = f"{message} [{default}]" + user_input = input(f"{message}: ").strip() + return user_input if user_input else (default or '') + + def select_option(self, message: str, options: List[str]) -> str: + """ + Prompt user to select from options. + + Args: + message: Prompt message + options: List of options + + Returns: + Selected option + """ + print(f"\n{message}") + for i, option in enumerate(options, 1): + print(f"{i}. {option}") + + while True: + try: + choice = int(input("Select option: ").strip()) + if 1 <= choice <= len(options): + return options[choice - 1] + print(f"Please select 1-{len(options)}") + except (ValueError, KeyboardInterrupt): + print("Invalid input") + + def check_cli_installed(self) -> bool: + """ + Check if Shopify CLI is installed. + + Returns: + True if installed, False otherwise + """ + try: + result = subprocess.run( + ['shopify', 'version'], + capture_output=True, + text=True, + timeout=5 + ) + return result.returncode == 0 + except (subprocess.SubprocessError, FileNotFoundError): + return False + + def create_app_config(self, project_dir: Path, app_name: str, scopes: str) -> None: + """ + Create shopify.app.toml configuration file. + + Args: + project_dir: Project directory + app_name: Application name + scopes: Access scopes + """ + config_content = f"""# Shopify App Configuration +name = "{app_name}" +client_id = "{self.config.shopify_api_key or 'YOUR_API_KEY'}" +application_url = "https://your-app.com" +embedded = true + +[build] +automatically_update_urls_on_dev = true +dev_store_url = "{self.config.shop_domain or 'your-store.myshopify.com'}" + +[access_scopes] +scopes = "{scopes}" + +[webhooks] +api_version = "2026-01" + +[[webhooks.subscriptions]] +topics = ["app/uninstalled"] +uri = "/webhooks/app/uninstalled" + +[webhooks.privacy_compliance] +customer_data_request_url = "/webhooks/gdpr/data-request" +customer_deletion_url = "/webhooks/gdpr/customer-deletion" +shop_deletion_url = "/webhooks/gdpr/shop-deletion" +""" + config_path = project_dir / 'shopify.app.toml' + config_path.write_text(config_content) + print(f"✓ Created {config_path}") + + def create_extension_config(self, project_dir: Path, extension_name: str, extension_type: str) -> None: + """ + Create shopify.extension.toml configuration file. + + Args: + project_dir: Project directory + extension_name: Extension name + extension_type: Extension type + """ + target_map = { + 'checkout': 'purchase.checkout.block.render', + 'admin_action': 'admin.product-details.action.render', + 'admin_block': 'admin.product-details.block.render', + 'pos': 'pos.home.tile.render', + 'function': 'function', + 'customer_account': 'customer-account.order-status.block.render', + 'theme_app': 'theme-app-extension' + } + + config_content = f"""name = "{extension_name}" +type = "ui_extension" +handle = "{extension_name.lower().replace(' ', '-')}" + +[extension_points] +api_version = "2026-01" + +[[extension_points.targets]] +target = "{target_map.get(extension_type, 'purchase.checkout.block.render')}" + +[capabilities] +network_access = true +api_access = true +""" + config_path = project_dir / 'shopify.extension.toml' + config_path.write_text(config_content) + print(f"✓ Created {config_path}") + + def create_readme(self, project_dir: Path, project_type: str, project_name: str) -> None: + """ + Create README.md file. + + Args: + project_dir: Project directory + project_type: Project type (app/extension/theme) + project_name: Project name + """ + content = f"""# {project_name} + +Shopify {project_type.capitalize()} project. + +## Setup + +```bash +# Install dependencies +npm install + +# Start development +shopify {project_type} dev +``` + +## Deployment + +```bash +# Deploy to Shopify +shopify {project_type} deploy +``` + +## Resources + +- [Shopify Documentation](https://shopify.dev/docs) +- [Shopify CLI](https://shopify.dev/docs/api/shopify-cli) +""" + readme_path = project_dir / 'README.md' + readme_path.write_text(content) + print(f"✓ Created {readme_path}") + + def init_app(self) -> None: + """Initialize Shopify app project.""" + print("\n=== Shopify App Initialization ===\n") + + app_name = self.prompt("App name", "my-shopify-app") + scopes = self.prompt("Access scopes", self.config.scopes or "read_products,write_products") + + project_dir = Path.cwd() / app_name + project_dir.mkdir(exist_ok=True) + + print(f"\nCreating app in {project_dir}...") + + self.create_app_config(project_dir, app_name, scopes) + self.create_readme(project_dir, "app", app_name) + + # Create basic package.json + package_json = { + "name": app_name.lower().replace(' ', '-'), + "version": "1.0.0", + "scripts": { + "dev": "shopify app dev", + "deploy": "shopify app deploy" + } + } + (project_dir / 'package.json').write_text(json.dumps(package_json, indent=2)) + print(f"✓ Created package.json") + + print(f"\n✓ App '{app_name}' initialized successfully!") + print(f"\nNext steps:") + print(f" cd {app_name}") + print(f" npm install") + print(f" shopify app dev") + + def init_extension(self) -> None: + """Initialize Shopify extension project.""" + print("\n=== Shopify Extension Initialization ===\n") + + extension_types = [ + 'checkout', + 'admin_action', + 'admin_block', + 'pos', + 'function', + 'customer_account', + 'theme_app' + ] + extension_type = self.select_option("Select extension type", extension_types) + + extension_name = self.prompt("Extension name", "my-extension") + + project_dir = Path.cwd() / extension_name + project_dir.mkdir(exist_ok=True) + + print(f"\nCreating extension in {project_dir}...") + + self.create_extension_config(project_dir, extension_name, extension_type) + self.create_readme(project_dir, "extension", extension_name) + + print(f"\n✓ Extension '{extension_name}' initialized successfully!") + print(f"\nNext steps:") + print(f" cd {extension_name}") + print(f" shopify app dev") + + def init_theme(self) -> None: + """Initialize Shopify theme project.""" + print("\n=== Shopify Theme Initialization ===\n") + + theme_name = self.prompt("Theme name", "my-theme") + + print(f"\nInitializing theme '{theme_name}'...") + print("\nRecommended: Use 'shopify theme init' for full theme scaffolding") + print(f"\nRun: shopify theme init {theme_name}") + + def run(self) -> None: + """Run interactive initialization.""" + print("=" * 60) + print("Shopify Project Initializer") + print("=" * 60) + + # Check CLI + if not self.check_cli_installed(): + print("\n⚠ Shopify CLI not found!") + print("Install: npm install -g @shopify/cli@latest") + sys.exit(1) + + # Select project type + project_types = ['app', 'extension', 'theme'] + project_type = self.select_option("Select project type", project_types) + + # Initialize based on type + if project_type == 'app': + self.init_app() + elif project_type == 'extension': + self.init_extension() + elif project_type == 'theme': + self.init_theme() + + +def main() -> None: + """Main entry point.""" + try: + # Get skill directory + script_dir = Path(__file__).parent + skill_dir = script_dir.parent + + # Load configuration + config = EnvLoader.load_config(skill_dir) + + # Initialize project + initializer = ShopifyInitializer(config) + initializer.run() + + except KeyboardInterrupt: + print("\n\nAborted.") + sys.exit(0) + except Exception as e: + print(f"\n✗ Error: {e}", file=sys.stderr) + sys.exit(1) + + +if __name__ == '__main__': + main() diff --git a/.agents/skills/shopify-development/scripts/tests/test_shopify_init.py b/.agents/skills/shopify-development/scripts/tests/test_shopify_init.py new file mode 100644 index 000000000..bcebb7902 --- /dev/null +++ b/.agents/skills/shopify-development/scripts/tests/test_shopify_init.py @@ -0,0 +1,379 @@ +""" +Tests for shopify_init.py + +Run with: pytest test_shopify_init.py -v --cov=shopify_init --cov-report=term-missing +""" + +import os +import sys +import json +import pytest +import subprocess +from pathlib import Path +from unittest.mock import Mock, patch, mock_open, MagicMock + +sys.path.insert(0, str(Path(__file__).parent.parent)) + +from shopify_init import EnvLoader, EnvConfig, ShopifyInitializer + + +class TestEnvLoader: + """Test EnvLoader class.""" + + def test_load_env_file_success(self, tmp_path): + """Test loading valid .env file.""" + env_file = tmp_path / ".env" + env_file.write_text(""" +SHOPIFY_API_KEY=test_key +SHOPIFY_API_SECRET=test_secret +SHOP_DOMAIN=test.myshopify.com +# Comment line +SCOPES=read_products,write_products +""") + + result = EnvLoader.load_env_file(env_file) + + assert result['SHOPIFY_API_KEY'] == 'test_key' + assert result['SHOPIFY_API_SECRET'] == 'test_secret' + assert result['SHOP_DOMAIN'] == 'test.myshopify.com' + assert result['SCOPES'] == 'read_products,write_products' + + def test_load_env_file_with_quotes(self, tmp_path): + """Test loading .env file with quoted values.""" + env_file = tmp_path / ".env" + env_file.write_text(""" +SHOPIFY_API_KEY="test_key" +SHOPIFY_API_SECRET='test_secret' +""") + + result = EnvLoader.load_env_file(env_file) + + assert result['SHOPIFY_API_KEY'] == 'test_key' + assert result['SHOPIFY_API_SECRET'] == 'test_secret' + + def test_load_env_file_nonexistent(self, tmp_path): + """Test loading non-existent .env file.""" + result = EnvLoader.load_env_file(tmp_path / "nonexistent.env") + assert result == {} + + def test_load_env_file_invalid_format(self, tmp_path): + """Test loading .env file with invalid lines.""" + env_file = tmp_path / ".env" + env_file.write_text(""" +VALID_KEY=value +INVALID_LINE_NO_EQUALS +ANOTHER_VALID=test +""") + + result = EnvLoader.load_env_file(env_file) + + assert result['VALID_KEY'] == 'value' + assert result['ANOTHER_VALID'] == 'test' + assert 'INVALID_LINE_NO_EQUALS' not in result + + def test_get_env_paths(self, tmp_path): + """Test getting .env file paths from universal directory structure.""" + # Create directory structure (works with .agent, .claude, .gemini, .cursor) + agent_dir = tmp_path / ".agent" + skills_dir = agent_dir / "skills" + skill_dir = skills_dir / "shopify" + + skill_dir.mkdir(parents=True) + + # Create .env files at each level + (skill_dir / ".env").write_text("SKILL=1") + (skills_dir / ".env").write_text("SKILLS=1") + (agent_dir / ".env").write_text("AGENT=1") + + paths = EnvLoader.get_env_paths(skill_dir) + + assert len(paths) == 3 + assert skill_dir / ".env" in paths + assert skills_dir / ".env" in paths + assert agent_dir / ".env" in paths + + def test_load_config_priority(self, tmp_path, monkeypatch): + """Test configuration loading priority across different AI tool directories.""" + skill_dir = tmp_path / "skill" + skills_dir = tmp_path + agent_dir = tmp_path.parent # Could be .agent, .claude, .gemini, .cursor + + skill_dir.mkdir(parents=True) + + (skill_dir / ".env").write_text("SHOPIFY_API_KEY=skill_key") + (skills_dir / ".env").write_text("SHOPIFY_API_KEY=skills_key\nSHOP_DOMAIN=skills.myshopify.com") + + monkeypatch.setenv("SHOPIFY_API_KEY", "process_key") + + config = EnvLoader.load_config(skill_dir) + + assert config.shopify_api_key == "process_key" + # Shop domain from skills/.env + assert config.shop_domain == "skills.myshopify.com" + + def test_load_config_no_files(self, tmp_path): + """Test configuration loading with no .env files.""" + config = EnvLoader.load_config(tmp_path) + + assert config.shopify_api_key is None + assert config.shopify_api_secret is None + assert config.shop_domain is None + assert config.scopes is None + + +class TestShopifyInitializer: + """Test ShopifyInitializer class.""" + + @pytest.fixture + def config(self): + """Create test config.""" + return EnvConfig( + shopify_api_key="test_key", + shopify_api_secret="test_secret", + shop_domain="test.myshopify.com", + scopes="read_products,write_products" + ) + + @pytest.fixture + def initializer(self, config): + """Create initializer instance.""" + return ShopifyInitializer(config) + + def test_prompt_with_default(self, initializer): + """Test prompt with default value.""" + with patch('builtins.input', return_value=''): + result = initializer.prompt("Test", "default_value") + assert result == "default_value" + + def test_prompt_with_input(self, initializer): + """Test prompt with user input.""" + with patch('builtins.input', return_value='user_input'): + result = initializer.prompt("Test", "default_value") + assert result == "user_input" + + def test_select_option_valid(self, initializer): + """Test select option with valid choice.""" + options = ['app', 'extension', 'theme'] + with patch('builtins.input', return_value='2'): + result = initializer.select_option("Choose", options) + assert result == 'extension' + + def test_select_option_invalid_then_valid(self, initializer): + """Test select option with invalid then valid choice.""" + options = ['app', 'extension'] + with patch('builtins.input', side_effect=['5', 'invalid', '1']): + result = initializer.select_option("Choose", options) + assert result == 'app' + + def test_check_cli_installed_success(self, initializer): + """Test CLI installed check - success.""" + mock_result = Mock() + mock_result.returncode = 0 + + with patch('subprocess.run', return_value=mock_result): + assert initializer.check_cli_installed() is True + + def test_check_cli_installed_failure(self, initializer): + """Test CLI installed check - failure.""" + with patch('subprocess.run', side_effect=FileNotFoundError): + assert initializer.check_cli_installed() is False + + def test_create_app_config(self, initializer, tmp_path): + """Test creating app configuration file.""" + initializer.create_app_config(tmp_path, "test-app", "read_products") + + config_file = tmp_path / "shopify.app.toml" + assert config_file.exists() + + content = config_file.read_text() + assert 'name = "test-app"' in content + assert 'scopes = "read_products"' in content + assert 'client_id = "test_key"' in content + + def test_create_extension_config(self, initializer, tmp_path): + """Test creating extension configuration file.""" + initializer.create_extension_config(tmp_path, "test-ext", "checkout") + + config_file = tmp_path / "shopify.extension.toml" + assert config_file.exists() + + content = config_file.read_text() + assert 'name = "test-ext"' in content + assert 'purchase.checkout.block.render' in content + + def test_create_extension_config_admin_action(self, initializer, tmp_path): + """Test creating admin action extension config.""" + initializer.create_extension_config(tmp_path, "admin-ext", "admin_action") + + config_file = tmp_path / "shopify.extension.toml" + content = config_file.read_text() + assert 'admin.product-details.action.render' in content + + def test_create_readme(self, initializer, tmp_path): + """Test creating README file.""" + initializer.create_readme(tmp_path, "app", "Test App") + + readme_file = tmp_path / "README.md" + assert readme_file.exists() + + content = readme_file.read_text() + assert '# Test App' in content + assert 'shopify app dev' in content + + @patch('builtins.input') + @patch('builtins.print') + def test_init_app(self, mock_print, mock_input, initializer, tmp_path, monkeypatch): + """Test app initialization.""" + monkeypatch.chdir(tmp_path) + + # Mock user inputs + mock_input.side_effect = ['my-app', 'read_products,write_products'] + + initializer.init_app() + + # Check directory created + app_dir = tmp_path / "my-app" + assert app_dir.exists() + + # Check files created + assert (app_dir / "shopify.app.toml").exists() + assert (app_dir / "README.md").exists() + assert (app_dir / "package.json").exists() + + # Check package.json content + package_json = json.loads((app_dir / "package.json").read_text()) + assert package_json['name'] == 'my-app' + assert 'dev' in package_json['scripts'] + + @patch('builtins.input') + @patch('builtins.print') + def test_init_extension(self, mock_print, mock_input, initializer, tmp_path, monkeypatch): + """Test extension initialization.""" + monkeypatch.chdir(tmp_path) + + # Mock user inputs: type selection (1 = checkout), name + mock_input.side_effect = ['1', 'my-extension'] + + initializer.init_extension() + + # Check directory and files created + ext_dir = tmp_path / "my-extension" + assert ext_dir.exists() + assert (ext_dir / "shopify.extension.toml").exists() + assert (ext_dir / "README.md").exists() + + @patch('builtins.input') + @patch('builtins.print') + def test_init_theme(self, mock_print, mock_input, initializer): + """Test theme initialization.""" + mock_input.return_value = 'my-theme' + + initializer.init_theme() + + assert mock_print.called + + @patch('builtins.print') + def test_run_no_cli(self, mock_print, initializer): + """Test run when CLI not installed.""" + with patch.object(initializer, 'check_cli_installed', return_value=False): + with pytest.raises(SystemExit) as exc_info: + initializer.run() + assert exc_info.value.code == 1 + + @patch.object(ShopifyInitializer, 'check_cli_installed', return_value=True) + @patch.object(ShopifyInitializer, 'init_app') + @patch('builtins.input') + @patch('builtins.print') + def test_run_app_selected(self, mock_print, mock_input, mock_init_app, mock_cli_check, initializer): + """Test run with app selection.""" + mock_input.return_value = '1' # Select app + + initializer.run() + + mock_init_app.assert_called_once() + + @patch.object(ShopifyInitializer, 'check_cli_installed', return_value=True) + @patch.object(ShopifyInitializer, 'init_extension') + @patch('builtins.input') + @patch('builtins.print') + def test_run_extension_selected(self, mock_print, mock_input, mock_init_ext, mock_cli_check, initializer): + """Test run with extension selection.""" + mock_input.return_value = '2' # Select extension + + initializer.run() + + mock_init_ext.assert_called_once() + + +class TestMain: + """Test main function.""" + + @patch('shopify_init.ShopifyInitializer') + @patch('shopify_init.EnvLoader') + def test_main_success(self, mock_loader, mock_initializer): + """Test main function success path.""" + from shopify_init import main + + mock_config = Mock() + mock_loader.load_config.return_value = mock_config + + mock_init_instance = Mock() + mock_initializer.return_value = mock_init_instance + + with patch('builtins.print'): + main() + + mock_init_instance.run.assert_called_once() + + @patch('shopify_init.ShopifyInitializer') + @patch('sys.exit') + def test_main_keyboard_interrupt(self, mock_exit, mock_initializer): + """Test main function with keyboard interrupt.""" + from shopify_init import main + + mock_initializer.return_value.run.side_effect = KeyboardInterrupt + + with patch('builtins.print'): + main() + + mock_exit.assert_called_with(0) + + @patch('shopify_init.ShopifyInitializer') + @patch('sys.exit') + def test_main_exception(self, mock_exit, mock_initializer): + """Test main function with exception.""" + from shopify_init import main + + mock_initializer.return_value.run.side_effect = Exception("Test error") + + with patch('builtins.print'): + main() + + mock_exit.assert_called_with(1) + + +class TestEnvConfig: + """Test EnvConfig dataclass.""" + + def test_env_config_defaults(self): + """Test EnvConfig default values.""" + config = EnvConfig() + + assert config.shopify_api_key is None + assert config.shopify_api_secret is None + assert config.shop_domain is None + assert config.scopes is None + + def test_env_config_with_values(self): + """Test EnvConfig with values.""" + config = EnvConfig( + shopify_api_key="key", + shopify_api_secret="secret", + shop_domain="test.myshopify.com", + scopes="read_products" + ) + + assert config.shopify_api_key == "key" + assert config.shopify_api_secret == "secret" + assert config.shop_domain == "test.myshopify.com" + assert config.scopes == "read_products" diff --git a/.agents/skills/tailwind/SKILL.md b/.agents/skills/tailwind/SKILL.md new file mode 100644 index 000000000..b86774848 --- /dev/null +++ b/.agents/skills/tailwind/SKILL.md @@ -0,0 +1,80 @@ +--- +name: tailwind +description: >- + Tailwind CSS v4 best-practices skill covering utility-first patterns, @theme + variables, responsive design, dark mode, custom styles, performance, + accessibility, and a Figma-to-Tailwind theme generation workflow. Use when + the user mentions Tailwind, tailwindcss, @theme, utility classes, Tailwind + config, Figma design tokens, or asks to build, configure, audit, or migrate + a Tailwind CSS project (including v3 to v4 migrations). +--- + +# Tailwind CSS Best Practices + +Tailwind CSS v4 guide organized as modular rules. Covers the utility-first model, `@theme` variables, responsive/state variants, custom styles, performance, accessibility, and the **Figma → Tailwind theme workflow** for generating design tokens directly from Figma variables. + +## ROUTING: Which rule file to load + +**IF setting up Tailwind or understanding how utility classes work:** +→ Read `rules/core-utility-model.md` + +**IF working with theme variables (`@theme`), design tokens, colors, fonts, spacing:** +→ Read `rules/core-theme-variables.md` + +**IF working with responsive design, hover/focus states, dark mode, or custom variants:** +→ Read `rules/core-responsive-and-states.md` + +**IF adding custom CSS, component classes, base styles, or custom utilities:** +→ Read `rules/core-custom-styles.md` + +**IF optimizing build size, purging unused classes, or configuring content detection:** +→ Read `rules/perf-purging-and-scanning.md` + +**IF working on accessibility or dark mode strategies:** +→ Read `rules/a11y-and-dark-mode.md` + +**IF translating Figma variables/design tokens into Tailwind v4 theme CSS:** +→ Read `rules/figma-to-theme-workflow.md` + see `figma-tokens/` templates + +## Rule index + +| Topic | Description | File | +|-------|-------------|------| +| Sections overview | Categories and reading order | [rules/_sections.md](rules/_sections.md) | +| Utility model | Utility-first principles, composing classes, arbitrary values | [rules/core-utility-model.md](rules/core-utility-model.md) | +| Theme variables | `@theme` directive, namespaces, extend/override, `inline`/`static` | [rules/core-theme-variables.md](rules/core-theme-variables.md) | +| Responsive & states | Breakpoints, hover/focus/dark variants, custom variants | [rules/core-responsive-and-states.md](rules/core-responsive-and-states.md) | +| Custom styles | `@layer`, `@utility`, `@variant`, component classes | [rules/core-custom-styles.md](rules/core-custom-styles.md) | +| Performance | Content detection, JIT, build optimization | [rules/perf-purging-and-scanning.md](rules/perf-purging-and-scanning.md) | +| A11y & dark mode | Accessibility utilities, dark mode patterns | [rules/a11y-and-dark-mode.md](rules/a11y-and-dark-mode.md) | +| **Figma workflow** | **Agent workflow: Figma variables → Tailwind `@theme` CSS** | [rules/figma-to-theme-workflow.md](rules/figma-to-theme-workflow.md) | + +## Figma → Tailwind theme workflow + +This skill includes a dedicated agent workflow to convert Figma design variables into Tailwind v4 `@theme` CSS files. The workflow is described in `rules/figma-to-theme-workflow.md` and uses the annotated templates in `figma-tokens/` as output targets. + +**How to trigger it:** paste your Figma CSS variables into chat and ask the agent to generate the Tailwind theme files. + +Template files in `figma-tokens/`: +- `colors.css` — `--color-*` namespace +- `typography.css` — `--font-*`, `--text-*`, `--font-weight-*`, `--tracking-*`, `--leading-*` +- `spacing.css` — `--spacing` and `--spacing-*` +- `radius-shadows.css` — `--radius-*`, `--shadow-*` +- `breakpoints.css` — `--breakpoint-*` + +## Rule categories by priority + +| Priority | Category | Impact | Prefix | +|----------|----------|--------|--------| +| 1 | Utility model & theme | CRITICAL | `core-` | +| 2 | Responsive & states | HIGH | `core-` | +| 3 | Custom styles | HIGH | `core-` | +| 4 | Figma workflow | HIGH | (standalone) | +| 5 | Performance | MEDIUM-HIGH | `perf-` | +| 6 | Accessibility | MEDIUM | `a11y-` | + +## Coverage and maintenance + +- Coverage map: `rules/_coverage-map.md` +- Source: https://tailwindcss.com/docs (v4.2) +- Update when Tailwind releases a new major/minor version with breaking `@theme` changes. diff --git a/.agents/skills/tailwind/figma-tokens/breakpoints.css b/.agents/skills/tailwind/figma-tokens/breakpoints.css new file mode 100644 index 000000000..79b5ee319 --- /dev/null +++ b/.agents/skills/tailwind/figma-tokens/breakpoints.css @@ -0,0 +1,39 @@ +/* Figma Design Tokens — Breakpoints + * + * This file is generated by the agent from Figma CSS variables. + * See: skills/tailwind/rules/figma-to-theme-workflow.md + * + * Namespace: --breakpoint-* + * Generates: responsive variant prefixes (sm:*, md:*, lg:*, xl:*, etc.) + * + * Usage: + * @import "./figma-tokens/breakpoints.css"; + * + * Notes: + * - Tailwind breakpoints are MINIMUM widths (mobile-first). + * - Convert px to rem (÷ 16) for accessibility (respects browser font size scaling). + * - Overriding a default breakpoint (e.g. --breakpoint-sm) replaces the default value. + * - Adding a new name (e.g. --breakpoint-3xl) adds a new responsive variant. + * - Figma "Desktop" frame widths are typically MAX widths — map them to Tailwind's + * MIN-width convention: e.g. Figma "Tablet" at 768px → --breakpoint-md: 48rem + * + * Figma frame → Tailwind breakpoint mapping (common): + * Mobile: 360px → not usually a breakpoint (base/mobile-first) + * Tablet: 768px → --breakpoint-md: 48rem + * Desktop: 1024px → --breakpoint-lg: 64rem + * Wide: 1280px → --breakpoint-xl: 80rem + * + * Example output — replace with your actual tokens: + */ + +@theme { + /* Standard breakpoints (override Tailwind defaults or leave as-is) */ + --breakpoint-sm: 40rem; /* 640px */ + --breakpoint-md: 48rem; /* 768px */ + --breakpoint-lg: 64rem; /* 1024px */ + --breakpoint-xl: 80rem; /* 1280px */ + --breakpoint-2xl: 96rem; /* 1536px */ + + /* Custom breakpoints (add as needed) */ + /* --breakpoint-3xl: 120rem; */ /* 1920px */ +} diff --git a/.agents/skills/tailwind/figma-tokens/colors.css b/.agents/skills/tailwind/figma-tokens/colors.css new file mode 100644 index 000000000..b994e5764 --- /dev/null +++ b/.agents/skills/tailwind/figma-tokens/colors.css @@ -0,0 +1,42 @@ +/* Figma Design Tokens — Colors + * + * This file is generated by the agent from Figma CSS variables. + * See: skills/tailwind/rules/figma-to-theme-workflow.md + * + * Namespace: --color-* + * Generates: bg-*, text-*, border-*, ring-*, fill-*, stroke-*, decoration-*, etc. + * + * Usage: + * @import "./figma-tokens/colors.css"; + * in your main app.css (after @import "tailwindcss") + * + * Notes: + * - Use OKLCH for perceptually uniform colors when possible. + * - HEX/RGB values are also valid. + * - To completely replace Tailwind's default palette, add: --color-*: initial; + * as the first declaration inside @theme (removes ALL default color utilities). + * + * Example output — replace with your actual tokens: + */ + +@theme { + /* Brand */ + --color-brand-50: oklch(0.97 0.02 40); + --color-brand-100: oklch(0.93 0.05 40); + --color-brand-200: oklch(0.87 0.10 40); + --color-brand-300: oklch(0.79 0.15 40); + --color-brand-400: oklch(0.72 0.18 40); + --color-brand-500: oklch(0.66 0.20 40); /* primary */ + --color-brand-600: oklch(0.58 0.19 40); + --color-brand-700: oklch(0.49 0.17 40); + --color-brand-800: oklch(0.38 0.13 40); + --color-brand-900: oklch(0.27 0.09 40); + --color-brand-950: oklch(0.18 0.06 40); + + /* Neutral (surface, text, borders) */ + --color-surface: oklch(0.98 0 0); + --color-surface-muted: oklch(0.95 0 0); + --color-text: oklch(0.15 0 0); + --color-text-muted: oklch(0.45 0 0); + --color-border: oklch(0.88 0 0); +} diff --git a/.agents/skills/tailwind/figma-tokens/radius-shadows.css b/.agents/skills/tailwind/figma-tokens/radius-shadows.css new file mode 100644 index 000000000..bbd935efc --- /dev/null +++ b/.agents/skills/tailwind/figma-tokens/radius-shadows.css @@ -0,0 +1,44 @@ +/* Figma Design Tokens — Border Radius & Shadows + * + * This file is generated by the agent from Figma CSS variables. + * See: skills/tailwind/rules/figma-to-theme-workflow.md + * + * Namespaces: + * --radius-* → rounded-* utilities + * --shadow-* → shadow-* utilities + * --inset-shadow-* → inset shadow utilities + * --drop-shadow-* → drop-shadow filter utilities + * + * Usage: + * @import "./figma-tokens/radius-shadows.css"; + * + * Notes: + * - Convert px radius values to rem (÷ 16). + * - Shadow values: use rgb(0 0 0 / 0.1) modern syntax instead of rgba(). + * - Multiple shadow layers: separate with comma in the value string. + * + * Example output — replace with your actual tokens: + */ + +@theme { + /* Border radius */ + --radius-xs: 0.125rem; /* 2px */ + --radius-sm: 0.25rem; /* 4px */ + --radius-md: 0.5rem; /* 8px */ + --radius-lg: 0.75rem; /* 12px */ + --radius-xl: 1rem; /* 16px */ + --radius-2xl: 1.5rem; /* 24px */ + --radius-full: 9999px; /* pill */ + + /* Box shadows */ + --shadow-xs: 0 1px 2px 0 rgb(0 0 0 / 0.05); + --shadow-sm: 0 1px 3px 0 rgb(0 0 0 / 0.10), 0 1px 2px -1px rgb(0 0 0 / 0.10); + --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.10), 0 2px 4px -2px rgb(0 0 0 / 0.10); + --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.10), 0 4px 6px -4px rgb(0 0 0 / 0.10); + --shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.10), 0 8px 10px -6px rgb(0 0 0 / 0.10); + --shadow-2xl: 0 25px 50px -12px rgb(0 0 0 / 0.25); + + /* Inset shadows */ + --inset-shadow-xs: inset 0 1px 1px rgb(0 0 0 / 0.05); + --inset-shadow-sm: inset 0 2px 4px rgb(0 0 0 / 0.05); +} diff --git a/.agents/skills/tailwind/figma-tokens/spacing.css b/.agents/skills/tailwind/figma-tokens/spacing.css new file mode 100644 index 000000000..96d952c11 --- /dev/null +++ b/.agents/skills/tailwind/figma-tokens/spacing.css @@ -0,0 +1,35 @@ +/* Figma Design Tokens — Spacing + * + * This file is generated by the agent from Figma CSS variables. + * See: skills/tailwind/rules/figma-to-theme-workflow.md + * + * Namespace: --spacing-* and --spacing (base unit) + * Generates: p-*, m-*, gap-*, w-*, h-*, inset-*, space-*, translate-*, etc. + * + * Usage: + * @import "./figma-tokens/spacing.css"; + * + * Notes: + * - If Figma has a single base grid unit (e.g. 4px), set --spacing to that value. + * Tailwind then uses multipliers: p-4 = 4 × --spacing. + * - If Figma exports named steps, define them as --spacing-{name} tokens. + * - Convert px to rem (÷ 16) for scalable layouts. + * - Named tokens like --spacing-md generate utilities: p-md, m-md, gap-md, etc. + * + * Example output — replace with your actual tokens: + */ + +@theme { + /* Base unit — all numeric spacing utilities are multiples of this value. + * With --spacing: 0.25rem, then: p-1 = 0.25rem, p-4 = 1rem, p-8 = 2rem, etc. */ + --spacing: 0.25rem; /* 4px base grid */ + + /* Named spacing tokens (optional — use when Figma defines semantic steps) */ + --spacing-xs: 0.25rem; /* 4px */ + --spacing-sm: 0.5rem; /* 8px */ + --spacing-md: 1rem; /* 16px */ + --spacing-lg: 1.5rem; /* 24px */ + --spacing-xl: 2.5rem; /* 40px */ + --spacing-2xl: 4rem; /* 64px */ + --spacing-3xl: 6rem; /* 96px */ +} diff --git a/.agents/skills/tailwind/figma-tokens/typography.css b/.agents/skills/tailwind/figma-tokens/typography.css new file mode 100644 index 000000000..1aaf50b91 --- /dev/null +++ b/.agents/skills/tailwind/figma-tokens/typography.css @@ -0,0 +1,65 @@ +/* Figma Design Tokens — Typography + * + * This file is generated by the agent from Figma CSS variables. + * See: skills/tailwind/rules/figma-to-theme-workflow.md + * + * Namespaces: + * --font-* → font-family utilities (font-sans, font-heading, etc.) + * --text-* → font-size utilities (text-sm, text-xl, etc.) + * --font-weight-* → font-weight utilities (font-normal, font-bold, etc.) + * --tracking-* → letter-spacing utilities (tracking-tight, etc.) + * --leading-* → line-height utilities (leading-normal, etc.) + * + * Usage: + * @import "./figma-tokens/typography.css"; + * + * Notes: + * - px values should be converted to rem (÷ 16). + * - Include --text-*--line-height companion vars when Figma provides line-height. + * - Font stacks: always include a generic fallback (sans-serif, monospace, etc.) + * + * Example output — replace with your actual tokens: + */ + +@theme { + /* Font families */ + --font-heading: "Neue Haas Grotesk Display", ui-sans-serif, sans-serif; + --font-body: "Inter", ui-sans-serif, sans-serif; + --font-mono: "JetBrains Mono", ui-monospace, monospace; + + /* Font sizes (px → rem: value / 16) */ + --text-xs: 0.75rem; /* 12px */ + --text-sm: 0.875rem; /* 14px */ + --text-base: 1rem; /* 16px */ + --text-lg: 1.125rem; /* 18px */ + --text-xl: 1.25rem; /* 20px */ + --text-2xl: 1.5rem; /* 24px */ + --text-3xl: 2rem; /* 32px */ + --text-4xl: 3rem; /* 48px */ + --text-5xl: 4rem; /* 64px */ + + /* Line heights (companion vars — optional but recommended) */ + --text-xs--line-height: 1.5; + --text-sm--line-height: 1.5; + --text-base--line-height: 1.5; + --text-lg--line-height: 1.6; + --text-xl--line-height: 1.4; + --text-2xl--line-height: 1.3; + --text-3xl--line-height: 1.2; + --text-4xl--line-height: 1.1; + --text-5xl--line-height: 1; + + /* Letter spacing */ + --tracking-tight: -0.03em; + --tracking-normal: 0; + --tracking-wide: 0.04em; + --tracking-widest: 0.1em; + + /* Line heights (standalone) */ + --leading-none: 1; + --leading-tight: 1.25; + --leading-snug: 1.375; + --leading-normal: 1.5; + --leading-relaxed: 1.625; + --leading-loose: 2; +} diff --git a/.agents/skills/tailwind/rules/a11y-and-dark-mode.md b/.agents/skills/tailwind/rules/a11y-and-dark-mode.md new file mode 100644 index 000000000..1c5336b60 --- /dev/null +++ b/.agents/skills/tailwind/rules/a11y-and-dark-mode.md @@ -0,0 +1,135 @@ +# a11y-and-dark-mode + +Why it matters: accessibility and color scheme support are not afterthoughts — they affect usability for a large portion of users and are increasingly required by law (WCAG compliance). + +## Screen Reader Utilities + +```html + + + + + + Skip to main content + +``` + +## Focus Styles + +Never remove focus rings without a proper replacement: + +```html + + + + + +``` + +`focus-visible:` only shows the ring for keyboard navigation, not mouse clicks. + +## Color Contrast + +Use Tailwind's palette with contrast in mind: + +```html + +

Hard to read

+ + +

Easy to read

+

Also fine

+``` + +Verify contrast ratios when defining custom `@theme` colors. Prefer OKLCH for predictable perceptual lightness. + +## Reduced Motion + +```html + +
+``` + +```css +/* In custom CSS */ +.animated-element { + transition: transform 300ms ease; + + @variant motion-reduce { + transition: none; + } +} +``` + +## Forced Colors (High Contrast Mode) + +```html + +
+ Important UI element +
+ + +
+ Regular content +
+``` + +## Dark Mode — Design Guidance + +When implementing dark mode, ensure: + +1. **All interactive states have dark variants:** `hover:`, `focus:`, `active:` need `dark:` counterparts. + +```html + +
Larger shadow
+``` + +## Adding custom utilities with `@utility` + +```css +/* Simple custom utility */ +@utility content-auto { + content-visibility: auto; +} + +/* Complex utility with nested selectors */ +@utility scrollbar-hidden { + &::-webkit-scrollbar { display: none; } + scrollbar-width: none; +} +``` + +```html + +
+
+``` + +## Using `@variant` in custom CSS + +```css +.custom-element { + background: var(--color-white); + + @variant dark { + background: var(--color-gray-900); + } + + @variant hover { + background: var(--color-gray-50); + } +} +``` + +## Custom variants with `@custom-variant` + +```css +/* Shorthand (no nesting needed) */ +@custom-variant hocus (&:hover, &:focus); + +/* With nesting */ +@custom-variant any-hover { + @media (any-hover: hover) { + &:hover { @slot; } + } +} +``` + +```html + +Only underlines on hover-capable devices +``` + +## When NOT to use `@apply` + +```css +/* Incorrect — using @apply for one-off element styles */ +.hero { @apply text-4xl font-bold mb-8 text-gray-900; } + +/* Correct — write utilities in HTML directly */ +/*

*/ + +/* @apply is appropriate for genuinely reusable component abstractions */ +@layer components { + .form-input { + @apply w-full rounded-md border border-gray-300 px-3 py-2 text-sm + focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500; + } +} +``` + +## Docs + +- https://tailwindcss.com/docs/adding-custom-styles +- https://tailwindcss.com/docs/functions-and-directives diff --git a/.agents/skills/tailwind/rules/core-responsive-and-states.md b/.agents/skills/tailwind/rules/core-responsive-and-states.md new file mode 100644 index 000000000..3df9e2c2e --- /dev/null +++ b/.agents/skills/tailwind/rules/core-responsive-and-states.md @@ -0,0 +1,164 @@ +# core-responsive-and-states + +Why it matters: Tailwind's modifier system is the primary way to handle responsive design, interactive states, and dark mode — understanding how modifiers compose prevents redundant CSS and ensures consistent behavior. + +## Responsive Design (mobile-first) + +```html + +
+``` + +Default breakpoints (from `@theme`): + +| Modifier | Min-width | +|----------|-----------| +| `sm:` | 40rem (640px) | +| `md:` | 48rem (768px) | +| `lg:` | 64rem (1024px) | +| `xl:` | 80rem (1280px) | +| `2xl:` | 96rem (1536px) | + +Custom breakpoint: + +```css +@theme { + --breakpoint-3xl: 120rem; +} +``` + +```html +
...
+``` + +## State Modifiers + +### Hover, Focus, Active + +```html + +``` + +### Group and Peer + +```html + +
+

Title

+

Description

+
+ + + + + +``` + +### First, Last, Odd, Even + +```html +
    +
  • Item
  • +
+``` + +## Dark Mode + +### Strategy 1: media query (default) + +```css +/* app.css */ +@import "tailwindcss"; +/* dark: variant uses prefers-color-scheme by default */ +``` + +```html +
+ Adapts to OS preference +
+``` + +### Strategy 2: class-based (manual toggle) + +```css +@import "tailwindcss"; + +@variant dark (&:where(.dark, .dark *)); +``` + +```html + + + ... + +``` + +```js +// Toggle dark mode +document.documentElement.classList.toggle('dark') +``` + +### Strategy 3: data attribute + +```css +@variant dark (&:where([data-theme="dark"], [data-theme="dark"] *)); +``` + +```html +... +``` + +## Custom Variants + +```css +/* @custom-variant for data attributes */ +@custom-variant theme-brand (&:where([data-theme="brand"] *)); +``` + +```html + +
...
+ +``` + +## Stacking modifiers + +```html + +} + /> + ); +} +``` + +**Targets:** + +- `admin.product-details.action.render` +- `admin.order-details.action.render` +- `admin.customer-details.action.render` + +### Admin Block + +Embedded content in admin pages. + +```javascript +import { + reactExtension, + BlockStack, + Text, + Badge, +} from "@shopify/ui-extensions-react/admin"; + +export default reactExtension("admin.product-details.block.render", () => ( + +)); + +function Extension() { + const { data } = useData(); + const [analytics, setAnalytics] = useState(null); + + useEffect(() => { + fetchAnalytics(data.product.id).then(setAnalytics); + }, []); + + return ( + + Product Analytics + Views: {analytics?.views || 0} + Conversions: {analytics?.conversions || 0} + + {analytics?.trending ? "Trending" : "Normal"} + + + ); +} +``` + +**Targets:** + +- `admin.product-details.block.render` +- `admin.order-details.block.render` +- `admin.customer-details.block.render` + +## POS UI Extensions + +Customize Point of Sale experience. + +### Smart Grid Tile + +Quick access action on POS home screen. + +```javascript +import { + reactExtension, + SmartGridTile, +} from "@shopify/ui-extensions-react/pos"; + +export default reactExtension("pos.home.tile.render", () => ); + +function Extension() { + function handlePress() { + // Navigate to custom workflow + } + + return ( + + ); +} +``` + +### POS Modal + +Full-screen workflow. + +```javascript +import { + reactExtension, + Screen, + BlockStack, + Button, + TextField, +} from "@shopify/ui-extensions-react/pos"; + +export default reactExtension("pos.home.modal.render", () => ); + +function Extension() { + const { navigation } = useApi(); + const [amount, setAmount] = useState(""); + + function handleIssue() { + // Issue gift card + navigation.pop(); + } + + return ( + + + + + + + + ); +} +``` + +## Customer Account Extensions + +Customize customer account pages. + +### Order Status Extension + +```javascript +import { + reactExtension, + BlockStack, + Text, + Button, +} from "@shopify/ui-extensions-react/customer-account"; + +export default reactExtension( + "customer-account.order-status.block.render", + () => , +); + +function Extension() { + const { order } = useApi(); + + function handleReturn() { + // Initiate return + } + + return ( + + Need to return? + Start return for order {order.name} + + + ); +} +``` + +**Targets:** + +- `customer-account.order-status.block.render` +- `customer-account.order-index.block.render` +- `customer-account.profile.block.render` + +## Shopify Functions + +Serverless backend customization. + +### Function Types + +**Discounts:** + +- `order_discount` - Order-level discounts +- `product_discount` - Product-specific discounts +- `shipping_discount` - Shipping discounts + +**Payment Customization:** + +- Hide/rename/reorder payment methods + +**Delivery Customization:** + +- Custom shipping options +- Delivery rules + +**Validation:** + +- Cart validation rules +- Checkout validation + +### Create Function + +```bash +shopify app generate extension --type function +``` + +### Order Discount Function + +```javascript +// input.graphql +query Input { + cart { + lines { + quantity + merchandise { + ... on ProductVariant { + product { + hasTag(tag: "bulk-discount") + } + } + } + } + } +} + +// function.js +export default function orderDiscount(input) { + const targets = input.cart.lines + .filter(line => line.merchandise.product.hasTag) + .map(line => ({ + productVariant: { id: line.merchandise.id } + })); + + if (targets.length === 0) { + return { discounts: [] }; + } + + return { + discounts: [{ + targets, + value: { + percentage: { + value: 10 // 10% discount + } + } + }] + }; +} +``` + +### Payment Customization Function + +```javascript +export default function paymentCustomization(input) { + const hidePaymentMethods = input.cart.lines.some( + (line) => line.merchandise.product.hasTag, + ); + + if (!hidePaymentMethods) { + return { operations: [] }; + } + + return { + operations: [ + { + hide: { + paymentMethodId: "gid://shopify/PaymentMethod/123", + }, + }, + ], + }; +} +``` + +### Validation Function + +```javascript +export default function cartValidation(input) { + const errors = []; + + // Max 5 items per cart + if (input.cart.lines.length > 5) { + errors.push({ + localizedMessage: "Maximum 5 items allowed per order", + target: "cart", + }); + } + + // Min $50 for wholesale + const isWholesale = input.cart.lines.some( + (line) => line.merchandise.product.hasTag, + ); + + if (isWholesale && input.cart.cost.totalAmount.amount < 50) { + errors.push({ + localizedMessage: "Wholesale orders require $50 minimum", + target: "cart", + }); + } + + return { errors }; +} +``` + +## Network Requests + +Extensions can call external APIs. + +```javascript +import { useApi } from "@shopify/ui-extensions-react/checkout"; + +function Extension() { + const { sessionToken } = useApi(); + + async function fetchData() { + const token = await sessionToken.get(); + + const response = await fetch("https://your-app.com/api/data", { + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + }); + + return await response.json(); + } +} +``` + +## Best Practices + +**Performance:** + +- Lazy load data +- Memoize expensive computations +- Use loading states +- Minimize re-renders + +**UX:** + +- Provide clear error messages +- Show loading indicators +- Validate inputs +- Support keyboard navigation + +**Security:** + +- Verify session tokens on backend +- Sanitize user input +- Use HTTPS for all requests +- Don't expose sensitive data + +**Testing:** + +- Test on development stores +- Verify mobile/desktop +- Check accessibility +- Test edge cases + +## Resources + +- Checkout Extensions: https://shopify.dev/docs/api/checkout-extensions +- Admin Extensions: https://shopify.dev/docs/apps/admin/extensions +- Functions: https://shopify.dev/docs/apps/functions +- Components: https://shopify.dev/docs/api/checkout-ui-extensions/components diff --git a/.claude/skills/shopify-development/references/themes.md b/.claude/skills/shopify-development/references/themes.md new file mode 100644 index 000000000..2fc1c2b98 --- /dev/null +++ b/.claude/skills/shopify-development/references/themes.md @@ -0,0 +1,498 @@ +# Themes Reference + +Guide for developing Shopify themes with Liquid templating. + +## Liquid Templating + +### Syntax Basics + +**Objects (Output):** +```liquid +{{ product.title }} +{{ product.price | money }} +{{ customer.email }} +``` + +**Tags (Logic):** +```liquid +{% if product.available %} + +{% else %} +

Sold Out

+{% endif %} + +{% for product in collection.products %} + {{ product.title }} +{% endfor %} + +{% case product.type %} + {% when 'Clothing' %} + Apparel + {% when 'Shoes' %} + Footwear + {% else %} + Other +{% endcase %} +``` + +**Filters (Transform):** +```liquid +{{ product.title | upcase }} +{{ product.price | money }} +{{ product.description | strip_html | truncate: 100 }} +{{ product.image | img_url: 'medium' }} +{{ 'now' | date: '%B %d, %Y' }} +``` + +### Common Objects + +**Product:** +```liquid +{{ product.id }} +{{ product.title }} +{{ product.handle }} +{{ product.description }} +{{ product.price }} +{{ product.compare_at_price }} +{{ product.available }} +{{ product.type }} +{{ product.vendor }} +{{ product.tags }} +{{ product.images }} +{{ product.variants }} +{{ product.featured_image }} +{{ product.url }} +``` + +**Collection:** +```liquid +{{ collection.title }} +{{ collection.handle }} +{{ collection.description }} +{{ collection.products }} +{{ collection.products_count }} +{{ collection.image }} +{{ collection.url }} +``` + +**Cart:** +```liquid +{{ cart.item_count }} +{{ cart.total_price }} +{{ cart.items }} +{{ cart.note }} +{{ cart.attributes }} +``` + +**Customer:** +```liquid +{{ customer.email }} +{{ customer.first_name }} +{{ customer.last_name }} +{{ customer.orders_count }} +{{ customer.total_spent }} +{{ customer.addresses }} +{{ customer.default_address }} +``` + +**Shop:** +```liquid +{{ shop.name }} +{{ shop.email }} +{{ shop.domain }} +{{ shop.currency }} +{{ shop.money_format }} +{{ shop.enabled_payment_types }} +``` + +### Common Filters + +**String:** +- `upcase`, `downcase`, `capitalize` +- `strip_html`, `strip_newlines` +- `truncate: 100`, `truncatewords: 20` +- `replace: 'old', 'new'` + +**Number:** +- `money` - Format currency +- `round`, `ceil`, `floor` +- `times`, `divided_by`, `plus`, `minus` + +**Array:** +- `join: ', '` +- `first`, `last` +- `size` +- `map: 'property'` +- `where: 'property', 'value'` + +**URL:** +- `img_url: 'size'` - Image URL +- `url_for_type`, `url_for_vendor` +- `link_to`, `link_to_type` + +**Date:** +- `date: '%B %d, %Y'` + +## Theme Architecture + +### Directory Structure + +``` +theme/ +├── assets/ # CSS, JS, images +├── config/ # Theme settings +│ ├── settings_schema.json +│ └── settings_data.json +├── layout/ # Base templates +│ └── theme.liquid +├── locales/ # Translations +│ └── en.default.json +├── sections/ # Reusable blocks +│ ├── header.liquid +│ ├── footer.liquid +│ └── product-grid.liquid +├── snippets/ # Small components +│ ├── product-card.liquid +│ └── icon.liquid +└── templates/ # Page templates + ├── index.json + ├── product.json + ├── collection.json + └── cart.liquid +``` + +### Layout + +Base template wrapping all pages (`layout/theme.liquid`): + +```liquid + + + + + + {{ page_title }} + + {{ content_for_header }} + + + + + {% section 'header' %} + +
+ {{ content_for_layout }} +
+ + {% section 'footer' %} + + + + +``` + +### Templates + +Page-specific structures (`templates/product.json`): + +```json +{ + "sections": { + "main": { + "type": "product-template", + "settings": { + "show_vendor": true, + "show_quantity_selector": true + } + }, + "recommendations": { + "type": "product-recommendations" + } + }, + "order": ["main", "recommendations"] +} +``` + +Legacy format (`templates/product.liquid`): +```liquid +
+
+ {{ product.title }} +
+ +
+

{{ product.title }}

+

{{ product.price | money }}

+ + {% form 'product', product %} + + + + {% endform %} +
+
+``` + +### Sections + +Reusable content blocks (`sections/product-grid.liquid`): + +```liquid +
+ {% for product in section.settings.collection.products %} + + {% endfor %} +
+ +{% schema %} +{ + "name": "Product Grid", + "settings": [ + { + "type": "collection", + "id": "collection", + "label": "Collection" + }, + { + "type": "range", + "id": "products_per_row", + "min": 2, + "max": 5, + "step": 1, + "default": 4, + "label": "Products per row" + } + ], + "presets": [ + { + "name": "Product Grid" + } + ] +} +{% endschema %} +``` + +### Snippets + +Small reusable components (`snippets/product-card.liquid`): + +```liquid + +``` + +Include snippet: +```liquid +{% render 'product-card', product: product %} +``` + +## Development Workflow + +### Setup + +```bash +# Initialize new theme +shopify theme init + +# Choose Dawn (reference theme) or blank +``` + +### Local Development + +```bash +# Start local server +shopify theme dev + +# Preview at http://localhost:9292 +# Changes auto-sync to development theme +``` + +### Pull Theme + +```bash +# Pull live theme +shopify theme pull --live + +# Pull specific theme +shopify theme pull --theme=123456789 + +# Pull only templates +shopify theme pull --only=templates +``` + +### Push Theme + +```bash +# Push to development theme +shopify theme push --development + +# Create new unpublished theme +shopify theme push --unpublished + +# Push specific files +shopify theme push --only=sections,snippets +``` + +### Theme Check + +Lint theme code: +```bash +shopify theme check +shopify theme check --auto-correct +``` + +## Common Patterns + +### Product Form with Variants + +```liquid +{% form 'product', product %} + {% unless product.has_only_default_variant %} + {% for option in product.options_with_values %} +
+ + +
+ {% endfor %} + {% endunless %} + + + + + +{% endform %} +``` + +### Pagination + +```liquid +{% paginate collection.products by 12 %} + {% for product in collection.products %} + {% render 'product-card', product: product %} + {% endfor %} + + {% if paginate.pages > 1 %} + + {% endif %} +{% endpaginate %} +``` + +### Cart AJAX + +```javascript +// Add to cart +fetch('/cart/add.js', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + id: variantId, + quantity: 1 + }) +}) +.then(res => res.json()) +.then(item => console.log('Added:', item)); + +// Get cart +fetch('/cart.js') + .then(res => res.json()) + .then(cart => console.log('Cart:', cart)); + +// Update cart +fetch('/cart/change.js', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + id: lineItemKey, + quantity: 2 + }) +}) +.then(res => res.json()); +``` + +## Metafields in Themes + +Access custom data: + +```liquid +{{ product.metafields.custom.care_instructions }} +{{ product.metafields.custom.material.value }} + +{% if product.metafields.custom.featured %} + Featured +{% endif %} +``` + +## Best Practices + +**Performance:** +- Optimize images (use appropriate sizes) +- Minimize Liquid logic complexity +- Use lazy loading for images +- Defer non-critical JavaScript + +**Accessibility:** +- Use semantic HTML +- Include alt text for images +- Support keyboard navigation +- Ensure sufficient color contrast + +**SEO:** +- Use descriptive page titles +- Include meta descriptions +- Structure content with headings +- Implement schema markup + +**Code Quality:** +- Follow Shopify theme guidelines +- Use consistent naming conventions +- Comment complex logic +- Keep sections focused and reusable + +## Resources + +- Theme Development: https://shopify.dev/docs/themes +- Liquid Reference: https://shopify.dev/docs/api/liquid +- Dawn Theme: https://github.com/Shopify/dawn +- Theme Check: https://shopify.dev/docs/themes/tools/theme-check diff --git a/.claude/skills/shopify-development/scripts/.gitignore b/.claude/skills/shopify-development/scripts/.gitignore new file mode 100644 index 000000000..8abb6f180 --- /dev/null +++ b/.claude/skills/shopify-development/scripts/.gitignore @@ -0,0 +1,49 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# Testing +.coverage +.pytest_cache/ +htmlcov/ +.tox/ +.nox/ +coverage.xml +*.cover +*.py,cover + +# Environments +.env +.venv +env/ +venv/ +ENV/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db diff --git a/.claude/skills/shopify-development/scripts/requirements.txt b/.claude/skills/shopify-development/scripts/requirements.txt new file mode 100644 index 000000000..4613a2baa --- /dev/null +++ b/.claude/skills/shopify-development/scripts/requirements.txt @@ -0,0 +1,19 @@ +# Shopify Skill Dependencies +# Python 3.10+ required + +# No Python package dependencies - uses only standard library + +# Testing dependencies (dev) +pytest>=8.0.0 +pytest-cov>=4.1.0 +pytest-mock>=3.12.0 + +# Note: This script requires the Shopify CLI tool +# Install Shopify CLI: +# npm install -g @shopify/cli @shopify/theme +# or via Homebrew (macOS): +# brew tap shopify/shopify +# brew install shopify-cli +# +# Authenticate with: +# shopify auth login diff --git a/.claude/skills/shopify-development/scripts/shopify_graphql.py b/.claude/skills/shopify-development/scripts/shopify_graphql.py new file mode 100644 index 000000000..ec8af3cb8 --- /dev/null +++ b/.claude/skills/shopify-development/scripts/shopify_graphql.py @@ -0,0 +1,428 @@ +#!/usr/bin/env python3 +""" +Shopify GraphQL Utilities + +Helper functions for common Shopify GraphQL operations. +Provides query templates, pagination helpers, and rate limit handling. + +Usage: + from shopify_graphql import ShopifyGraphQL + + client = ShopifyGraphQL(shop_domain, access_token) + products = client.get_products(first=10) +""" + +import os +import time +import json +from typing import Dict, List, Optional, Any, Generator +from dataclasses import dataclass +from urllib.request import Request, urlopen +from urllib.error import HTTPError + + +# API Configuration +API_VERSION = "2026-01" +MAX_RETRIES = 3 +RETRY_DELAY = 1.0 # seconds + + +@dataclass +class GraphQLResponse: + """Container for GraphQL response data.""" + data: Optional[Dict[str, Any]] = None + errors: Optional[List[Dict[str, Any]]] = None + extensions: Optional[Dict[str, Any]] = None + + @property + def is_success(self) -> bool: + return self.errors is None or len(self.errors) == 0 + + @property + def query_cost(self) -> Optional[int]: + """Get the actual query cost from extensions.""" + if self.extensions and 'cost' in self.extensions: + return self.extensions['cost'].get('actualQueryCost') + return None + + +class ShopifyGraphQL: + """ + Shopify GraphQL API client with built-in utilities. + + Features: + - Query templates for common operations + - Automatic pagination + - Rate limit handling with exponential backoff + - Response parsing helpers + """ + + def __init__(self, shop_domain: str, access_token: str): + """ + Initialize the GraphQL client. + + Args: + shop_domain: Store domain (e.g., 'my-store.myshopify.com') + access_token: Admin API access token + """ + self.shop_domain = shop_domain.replace('https://', '').replace('http://', '') + self.access_token = access_token + self.base_url = f"https://{self.shop_domain}/admin/api/{API_VERSION}/graphql.json" + + def execute(self, query: str, variables: Optional[Dict] = None) -> GraphQLResponse: + """ + Execute a GraphQL query/mutation. + + Args: + query: GraphQL query string + variables: Query variables + + Returns: + GraphQLResponse object + """ + payload = {"query": query} + if variables: + payload["variables"] = variables + + headers = { + "Content-Type": "application/json", + "X-Shopify-Access-Token": self.access_token + } + + for attempt in range(MAX_RETRIES): + try: + request = Request( + self.base_url, + data=json.dumps(payload).encode('utf-8'), + headers=headers, + method='POST' + ) + + with urlopen(request, timeout=30) as response: + result = json.loads(response.read().decode('utf-8')) + return GraphQLResponse( + data=result.get('data'), + errors=result.get('errors'), + extensions=result.get('extensions') + ) + + except HTTPError as e: + if e.code == 429: # Rate limited + delay = RETRY_DELAY * (2 ** attempt) + print(f"Rate limited. Retrying in {delay}s...") + time.sleep(delay) + continue + raise + except Exception as e: + if attempt == MAX_RETRIES - 1: + raise + time.sleep(RETRY_DELAY) + + return GraphQLResponse(errors=[{"message": "Max retries exceeded"}]) + + # ==================== Query Templates ==================== + + def get_products( + self, + first: int = 10, + query: Optional[str] = None, + after: Optional[str] = None + ) -> GraphQLResponse: + """ + Query products with pagination. + + Args: + first: Number of products to fetch (max 250) + query: Optional search query + after: Cursor for pagination + """ + gql = """ + query GetProducts($first: Int!, $query: String, $after: String) { + products(first: $first, query: $query, after: $after) { + edges { + node { + id + title + handle + status + totalInventory + variants(first: 5) { + edges { + node { + id + title + price + inventoryQuantity + sku + } + } + } + } + cursor + } + pageInfo { + hasNextPage + endCursor + } + } + } + """ + return self.execute(gql, {"first": first, "query": query, "after": after}) + + def get_orders( + self, + first: int = 10, + query: Optional[str] = None, + after: Optional[str] = None + ) -> GraphQLResponse: + """ + Query orders with pagination. + + Args: + first: Number of orders to fetch (max 250) + query: Optional search query (e.g., "financial_status:paid") + after: Cursor for pagination + """ + gql = """ + query GetOrders($first: Int!, $query: String, $after: String) { + orders(first: $first, query: $query, after: $after) { + edges { + node { + id + name + createdAt + displayFinancialStatus + displayFulfillmentStatus + totalPriceSet { + shopMoney { amount currencyCode } + } + customer { + id + firstName + lastName + } + lineItems(first: 5) { + edges { + node { + title + quantity + } + } + } + } + cursor + } + pageInfo { + hasNextPage + endCursor + } + } + } + """ + return self.execute(gql, {"first": first, "query": query, "after": after}) + + def get_customers( + self, + first: int = 10, + query: Optional[str] = None, + after: Optional[str] = None + ) -> GraphQLResponse: + """ + Query customers with pagination. + + Args: + first: Number of customers to fetch (max 250) + query: Optional search query + after: Cursor for pagination + """ + gql = """ + query GetCustomers($first: Int!, $query: String, $after: String) { + customers(first: $first, query: $query, after: $after) { + edges { + node { + id + firstName + lastName + displayName + defaultEmailAddress { + emailAddress + } + numberOfOrders + amountSpent { + amount + currencyCode + } + } + cursor + } + pageInfo { + hasNextPage + endCursor + } + } + } + """ + return self.execute(gql, {"first": first, "query": query, "after": after}) + + def set_metafields(self, metafields: List[Dict]) -> GraphQLResponse: + """ + Set metafields on resources. + + Args: + metafields: List of metafield inputs, each containing: + - ownerId: Resource GID + - namespace: Metafield namespace + - key: Metafield key + - value: Metafield value + - type: Metafield type + """ + gql = """ + mutation SetMetafields($metafields: [MetafieldsSetInput!]!) { + metafieldsSet(metafields: $metafields) { + metafields { + id + namespace + key + value + } + userErrors { + field + message + } + } + } + """ + return self.execute(gql, {"metafields": metafields}) + + # ==================== Pagination Helpers ==================== + + def paginate_products( + self, + batch_size: int = 50, + query: Optional[str] = None + ) -> Generator[Dict, None, None]: + """ + Generator that yields all products with automatic pagination. + + Args: + batch_size: Products per request (max 250) + query: Optional search query + + Yields: + Product dictionaries + """ + cursor = None + while True: + response = self.get_products(first=batch_size, query=query, after=cursor) + + if not response.is_success or not response.data: + break + + products = response.data.get('products', {}) + edges = products.get('edges', []) + + for edge in edges: + yield edge['node'] + + page_info = products.get('pageInfo', {}) + if not page_info.get('hasNextPage'): + break + + cursor = page_info.get('endCursor') + + def paginate_orders( + self, + batch_size: int = 50, + query: Optional[str] = None + ) -> Generator[Dict, None, None]: + """ + Generator that yields all orders with automatic pagination. + + Args: + batch_size: Orders per request (max 250) + query: Optional search query + + Yields: + Order dictionaries + """ + cursor = None + while True: + response = self.get_orders(first=batch_size, query=query, after=cursor) + + if not response.is_success or not response.data: + break + + orders = response.data.get('orders', {}) + edges = orders.get('edges', []) + + for edge in edges: + yield edge['node'] + + page_info = orders.get('pageInfo', {}) + if not page_info.get('hasNextPage'): + break + + cursor = page_info.get('endCursor') + + +# ==================== Utility Functions ==================== + +def extract_id(gid: str) -> str: + """ + Extract numeric ID from Shopify GID. + + Args: + gid: Global ID (e.g., 'gid://shopify/Product/123') + + Returns: + Numeric ID string (e.g., '123') + """ + return gid.split('/')[-1] if gid else '' + + +def build_gid(resource_type: str, id: str) -> str: + """ + Build Shopify GID from resource type and ID. + + Args: + resource_type: Resource type (e.g., 'Product', 'Order') + id: Numeric ID + + Returns: + Global ID (e.g., 'gid://shopify/Product/123') + """ + return f"gid://shopify/{resource_type}/{id}" + + +# ==================== Example Usage ==================== + +def main(): + """Example usage of ShopifyGraphQL client.""" + import os + + # Load from environment + shop = os.environ.get('SHOP_DOMAIN', 'your-store.myshopify.com') + token = os.environ.get('SHOPIFY_ACCESS_TOKEN', '') + + if not token: + print("Set SHOPIFY_ACCESS_TOKEN environment variable") + return + + client = ShopifyGraphQL(shop, token) + + # Example: Get first 5 products + print("Fetching products...") + response = client.get_products(first=5) + + if response.is_success: + products = response.data['products']['edges'] + for edge in products: + product = edge['node'] + print(f" - {product['title']} ({product['status']})") + print(f"\nQuery cost: {response.query_cost}") + else: + print(f"Errors: {response.errors}") + + +if __name__ == '__main__': + main() diff --git a/.claude/skills/shopify-development/scripts/shopify_init.py b/.claude/skills/shopify-development/scripts/shopify_init.py new file mode 100644 index 000000000..f0c664e12 --- /dev/null +++ b/.claude/skills/shopify-development/scripts/shopify_init.py @@ -0,0 +1,441 @@ +#!/usr/bin/env python3 +""" +Shopify Project Initialization Script + +Interactive script to scaffold Shopify apps, extensions, or themes. +Supports environment variable loading from multiple locations. +""" + +import os +import sys +import json +import subprocess +from pathlib import Path +from typing import Dict, Optional, List +from dataclasses import dataclass + + +@dataclass +class EnvConfig: + """Environment configuration container.""" + shopify_api_key: Optional[str] = None + shopify_api_secret: Optional[str] = None + shop_domain: Optional[str] = None + scopes: Optional[str] = None + + +class EnvLoader: + """Load environment variables from multiple sources in priority order.""" + + @staticmethod + def load_env_file(filepath: Path) -> Dict[str, str]: + """ + Load environment variables from .env file. + + Args: + filepath: Path to .env file + + Returns: + Dictionary of environment variables + """ + env_vars = {} + if not filepath.exists(): + return env_vars + + try: + with open(filepath, 'r') as f: + for line in f: + line = line.strip() + if line and not line.startswith('#') and '=' in line: + key, value = line.split('=', 1) + env_vars[key.strip()] = value.strip().strip('"').strip("'") + except Exception as e: + print(f"Warning: Failed to load {filepath}: {e}") + + return env_vars + + @staticmethod + def get_env_paths(skill_dir: Path) -> List[Path]: + """ + Get list of .env file paths in priority order. + + Works with any AI tool directory structure: + - .agent/skills/ (universal) + - .claude/skills/ (Claude Code) + - .gemini/skills/ (Gemini CLI) + - .cursor/skills/ (Cursor) + + Priority: process.env > skill/.env > skills/.env > agent_dir/.env + + Args: + skill_dir: Path to skill directory + + Returns: + List of .env file paths + """ + paths = [] + + # skill/.env + skill_env = skill_dir / '.env' + if skill_env.exists(): + paths.append(skill_env) + + # skills/.env + skills_env = skill_dir.parent / '.env' + if skills_env.exists(): + paths.append(skills_env) + + # agent_dir/.env (e.g., .agent, .claude, .gemini, .cursor) + agent_env = skill_dir.parent.parent / '.env' + if agent_env.exists(): + paths.append(agent_env) + + return paths + + @staticmethod + def load_config(skill_dir: Path) -> EnvConfig: + """ + Load configuration from environment variables. + + Works with any AI tool directory structure. + Priority: process.env > skill/.env > skills/.env > agent_dir/.env + + Args: + skill_dir: Path to skill directory + + Returns: + EnvConfig object + """ + config = EnvConfig() + + # Load from .env files (reverse priority order) + for env_path in reversed(EnvLoader.get_env_paths(skill_dir)): + env_vars = EnvLoader.load_env_file(env_path) + if 'SHOPIFY_API_KEY' in env_vars: + config.shopify_api_key = env_vars['SHOPIFY_API_KEY'] + if 'SHOPIFY_API_SECRET' in env_vars: + config.shopify_api_secret = env_vars['SHOPIFY_API_SECRET'] + if 'SHOP_DOMAIN' in env_vars: + config.shop_domain = env_vars['SHOP_DOMAIN'] + if 'SCOPES' in env_vars: + config.scopes = env_vars['SCOPES'] + + # Override with process environment (highest priority) + if 'SHOPIFY_API_KEY' in os.environ: + config.shopify_api_key = os.environ['SHOPIFY_API_KEY'] + if 'SHOPIFY_API_SECRET' in os.environ: + config.shopify_api_secret = os.environ['SHOPIFY_API_SECRET'] + if 'SHOP_DOMAIN' in os.environ: + config.shop_domain = os.environ['SHOP_DOMAIN'] + if 'SCOPES' in os.environ: + config.scopes = os.environ['SCOPES'] + + return config + + +class ShopifyInitializer: + """Initialize Shopify projects.""" + + def __init__(self, config: EnvConfig): + """ + Initialize ShopifyInitializer. + + Args: + config: Environment configuration + """ + self.config = config + + def prompt(self, message: str, default: Optional[str] = None) -> str: + """ + Prompt user for input. + + Args: + message: Prompt message + default: Default value + + Returns: + User input or default + """ + if default: + message = f"{message} [{default}]" + user_input = input(f"{message}: ").strip() + return user_input if user_input else (default or '') + + def select_option(self, message: str, options: List[str]) -> str: + """ + Prompt user to select from options. + + Args: + message: Prompt message + options: List of options + + Returns: + Selected option + """ + print(f"\n{message}") + for i, option in enumerate(options, 1): + print(f"{i}. {option}") + + while True: + try: + choice = int(input("Select option: ").strip()) + if 1 <= choice <= len(options): + return options[choice - 1] + print(f"Please select 1-{len(options)}") + except (ValueError, KeyboardInterrupt): + print("Invalid input") + + def check_cli_installed(self) -> bool: + """ + Check if Shopify CLI is installed. + + Returns: + True if installed, False otherwise + """ + try: + result = subprocess.run( + ['shopify', 'version'], + capture_output=True, + text=True, + timeout=5 + ) + return result.returncode == 0 + except (subprocess.SubprocessError, FileNotFoundError): + return False + + def create_app_config(self, project_dir: Path, app_name: str, scopes: str) -> None: + """ + Create shopify.app.toml configuration file. + + Args: + project_dir: Project directory + app_name: Application name + scopes: Access scopes + """ + config_content = f"""# Shopify App Configuration +name = "{app_name}" +client_id = "{self.config.shopify_api_key or 'YOUR_API_KEY'}" +application_url = "https://your-app.com" +embedded = true + +[build] +automatically_update_urls_on_dev = true +dev_store_url = "{self.config.shop_domain or 'your-store.myshopify.com'}" + +[access_scopes] +scopes = "{scopes}" + +[webhooks] +api_version = "2026-01" + +[[webhooks.subscriptions]] +topics = ["app/uninstalled"] +uri = "/webhooks/app/uninstalled" + +[webhooks.privacy_compliance] +customer_data_request_url = "/webhooks/gdpr/data-request" +customer_deletion_url = "/webhooks/gdpr/customer-deletion" +shop_deletion_url = "/webhooks/gdpr/shop-deletion" +""" + config_path = project_dir / 'shopify.app.toml' + config_path.write_text(config_content) + print(f"✓ Created {config_path}") + + def create_extension_config(self, project_dir: Path, extension_name: str, extension_type: str) -> None: + """ + Create shopify.extension.toml configuration file. + + Args: + project_dir: Project directory + extension_name: Extension name + extension_type: Extension type + """ + target_map = { + 'checkout': 'purchase.checkout.block.render', + 'admin_action': 'admin.product-details.action.render', + 'admin_block': 'admin.product-details.block.render', + 'pos': 'pos.home.tile.render', + 'function': 'function', + 'customer_account': 'customer-account.order-status.block.render', + 'theme_app': 'theme-app-extension' + } + + config_content = f"""name = "{extension_name}" +type = "ui_extension" +handle = "{extension_name.lower().replace(' ', '-')}" + +[extension_points] +api_version = "2026-01" + +[[extension_points.targets]] +target = "{target_map.get(extension_type, 'purchase.checkout.block.render')}" + +[capabilities] +network_access = true +api_access = true +""" + config_path = project_dir / 'shopify.extension.toml' + config_path.write_text(config_content) + print(f"✓ Created {config_path}") + + def create_readme(self, project_dir: Path, project_type: str, project_name: str) -> None: + """ + Create README.md file. + + Args: + project_dir: Project directory + project_type: Project type (app/extension/theme) + project_name: Project name + """ + content = f"""# {project_name} + +Shopify {project_type.capitalize()} project. + +## Setup + +```bash +# Install dependencies +npm install + +# Start development +shopify {project_type} dev +``` + +## Deployment + +```bash +# Deploy to Shopify +shopify {project_type} deploy +``` + +## Resources + +- [Shopify Documentation](https://shopify.dev/docs) +- [Shopify CLI](https://shopify.dev/docs/api/shopify-cli) +""" + readme_path = project_dir / 'README.md' + readme_path.write_text(content) + print(f"✓ Created {readme_path}") + + def init_app(self) -> None: + """Initialize Shopify app project.""" + print("\n=== Shopify App Initialization ===\n") + + app_name = self.prompt("App name", "my-shopify-app") + scopes = self.prompt("Access scopes", self.config.scopes or "read_products,write_products") + + project_dir = Path.cwd() / app_name + project_dir.mkdir(exist_ok=True) + + print(f"\nCreating app in {project_dir}...") + + self.create_app_config(project_dir, app_name, scopes) + self.create_readme(project_dir, "app", app_name) + + # Create basic package.json + package_json = { + "name": app_name.lower().replace(' ', '-'), + "version": "1.0.0", + "scripts": { + "dev": "shopify app dev", + "deploy": "shopify app deploy" + } + } + (project_dir / 'package.json').write_text(json.dumps(package_json, indent=2)) + print(f"✓ Created package.json") + + print(f"\n✓ App '{app_name}' initialized successfully!") + print(f"\nNext steps:") + print(f" cd {app_name}") + print(f" npm install") + print(f" shopify app dev") + + def init_extension(self) -> None: + """Initialize Shopify extension project.""" + print("\n=== Shopify Extension Initialization ===\n") + + extension_types = [ + 'checkout', + 'admin_action', + 'admin_block', + 'pos', + 'function', + 'customer_account', + 'theme_app' + ] + extension_type = self.select_option("Select extension type", extension_types) + + extension_name = self.prompt("Extension name", "my-extension") + + project_dir = Path.cwd() / extension_name + project_dir.mkdir(exist_ok=True) + + print(f"\nCreating extension in {project_dir}...") + + self.create_extension_config(project_dir, extension_name, extension_type) + self.create_readme(project_dir, "extension", extension_name) + + print(f"\n✓ Extension '{extension_name}' initialized successfully!") + print(f"\nNext steps:") + print(f" cd {extension_name}") + print(f" shopify app dev") + + def init_theme(self) -> None: + """Initialize Shopify theme project.""" + print("\n=== Shopify Theme Initialization ===\n") + + theme_name = self.prompt("Theme name", "my-theme") + + print(f"\nInitializing theme '{theme_name}'...") + print("\nRecommended: Use 'shopify theme init' for full theme scaffolding") + print(f"\nRun: shopify theme init {theme_name}") + + def run(self) -> None: + """Run interactive initialization.""" + print("=" * 60) + print("Shopify Project Initializer") + print("=" * 60) + + # Check CLI + if not self.check_cli_installed(): + print("\n⚠ Shopify CLI not found!") + print("Install: npm install -g @shopify/cli@latest") + sys.exit(1) + + # Select project type + project_types = ['app', 'extension', 'theme'] + project_type = self.select_option("Select project type", project_types) + + # Initialize based on type + if project_type == 'app': + self.init_app() + elif project_type == 'extension': + self.init_extension() + elif project_type == 'theme': + self.init_theme() + + +def main() -> None: + """Main entry point.""" + try: + # Get skill directory + script_dir = Path(__file__).parent + skill_dir = script_dir.parent + + # Load configuration + config = EnvLoader.load_config(skill_dir) + + # Initialize project + initializer = ShopifyInitializer(config) + initializer.run() + + except KeyboardInterrupt: + print("\n\nAborted.") + sys.exit(0) + except Exception as e: + print(f"\n✗ Error: {e}", file=sys.stderr) + sys.exit(1) + + +if __name__ == '__main__': + main() diff --git a/.claude/skills/shopify-development/scripts/tests/test_shopify_init.py b/.claude/skills/shopify-development/scripts/tests/test_shopify_init.py new file mode 100644 index 000000000..bcebb7902 --- /dev/null +++ b/.claude/skills/shopify-development/scripts/tests/test_shopify_init.py @@ -0,0 +1,379 @@ +""" +Tests for shopify_init.py + +Run with: pytest test_shopify_init.py -v --cov=shopify_init --cov-report=term-missing +""" + +import os +import sys +import json +import pytest +import subprocess +from pathlib import Path +from unittest.mock import Mock, patch, mock_open, MagicMock + +sys.path.insert(0, str(Path(__file__).parent.parent)) + +from shopify_init import EnvLoader, EnvConfig, ShopifyInitializer + + +class TestEnvLoader: + """Test EnvLoader class.""" + + def test_load_env_file_success(self, tmp_path): + """Test loading valid .env file.""" + env_file = tmp_path / ".env" + env_file.write_text(""" +SHOPIFY_API_KEY=test_key +SHOPIFY_API_SECRET=test_secret +SHOP_DOMAIN=test.myshopify.com +# Comment line +SCOPES=read_products,write_products +""") + + result = EnvLoader.load_env_file(env_file) + + assert result['SHOPIFY_API_KEY'] == 'test_key' + assert result['SHOPIFY_API_SECRET'] == 'test_secret' + assert result['SHOP_DOMAIN'] == 'test.myshopify.com' + assert result['SCOPES'] == 'read_products,write_products' + + def test_load_env_file_with_quotes(self, tmp_path): + """Test loading .env file with quoted values.""" + env_file = tmp_path / ".env" + env_file.write_text(""" +SHOPIFY_API_KEY="test_key" +SHOPIFY_API_SECRET='test_secret' +""") + + result = EnvLoader.load_env_file(env_file) + + assert result['SHOPIFY_API_KEY'] == 'test_key' + assert result['SHOPIFY_API_SECRET'] == 'test_secret' + + def test_load_env_file_nonexistent(self, tmp_path): + """Test loading non-existent .env file.""" + result = EnvLoader.load_env_file(tmp_path / "nonexistent.env") + assert result == {} + + def test_load_env_file_invalid_format(self, tmp_path): + """Test loading .env file with invalid lines.""" + env_file = tmp_path / ".env" + env_file.write_text(""" +VALID_KEY=value +INVALID_LINE_NO_EQUALS +ANOTHER_VALID=test +""") + + result = EnvLoader.load_env_file(env_file) + + assert result['VALID_KEY'] == 'value' + assert result['ANOTHER_VALID'] == 'test' + assert 'INVALID_LINE_NO_EQUALS' not in result + + def test_get_env_paths(self, tmp_path): + """Test getting .env file paths from universal directory structure.""" + # Create directory structure (works with .agent, .claude, .gemini, .cursor) + agent_dir = tmp_path / ".agent" + skills_dir = agent_dir / "skills" + skill_dir = skills_dir / "shopify" + + skill_dir.mkdir(parents=True) + + # Create .env files at each level + (skill_dir / ".env").write_text("SKILL=1") + (skills_dir / ".env").write_text("SKILLS=1") + (agent_dir / ".env").write_text("AGENT=1") + + paths = EnvLoader.get_env_paths(skill_dir) + + assert len(paths) == 3 + assert skill_dir / ".env" in paths + assert skills_dir / ".env" in paths + assert agent_dir / ".env" in paths + + def test_load_config_priority(self, tmp_path, monkeypatch): + """Test configuration loading priority across different AI tool directories.""" + skill_dir = tmp_path / "skill" + skills_dir = tmp_path + agent_dir = tmp_path.parent # Could be .agent, .claude, .gemini, .cursor + + skill_dir.mkdir(parents=True) + + (skill_dir / ".env").write_text("SHOPIFY_API_KEY=skill_key") + (skills_dir / ".env").write_text("SHOPIFY_API_KEY=skills_key\nSHOP_DOMAIN=skills.myshopify.com") + + monkeypatch.setenv("SHOPIFY_API_KEY", "process_key") + + config = EnvLoader.load_config(skill_dir) + + assert config.shopify_api_key == "process_key" + # Shop domain from skills/.env + assert config.shop_domain == "skills.myshopify.com" + + def test_load_config_no_files(self, tmp_path): + """Test configuration loading with no .env files.""" + config = EnvLoader.load_config(tmp_path) + + assert config.shopify_api_key is None + assert config.shopify_api_secret is None + assert config.shop_domain is None + assert config.scopes is None + + +class TestShopifyInitializer: + """Test ShopifyInitializer class.""" + + @pytest.fixture + def config(self): + """Create test config.""" + return EnvConfig( + shopify_api_key="test_key", + shopify_api_secret="test_secret", + shop_domain="test.myshopify.com", + scopes="read_products,write_products" + ) + + @pytest.fixture + def initializer(self, config): + """Create initializer instance.""" + return ShopifyInitializer(config) + + def test_prompt_with_default(self, initializer): + """Test prompt with default value.""" + with patch('builtins.input', return_value=''): + result = initializer.prompt("Test", "default_value") + assert result == "default_value" + + def test_prompt_with_input(self, initializer): + """Test prompt with user input.""" + with patch('builtins.input', return_value='user_input'): + result = initializer.prompt("Test", "default_value") + assert result == "user_input" + + def test_select_option_valid(self, initializer): + """Test select option with valid choice.""" + options = ['app', 'extension', 'theme'] + with patch('builtins.input', return_value='2'): + result = initializer.select_option("Choose", options) + assert result == 'extension' + + def test_select_option_invalid_then_valid(self, initializer): + """Test select option with invalid then valid choice.""" + options = ['app', 'extension'] + with patch('builtins.input', side_effect=['5', 'invalid', '1']): + result = initializer.select_option("Choose", options) + assert result == 'app' + + def test_check_cli_installed_success(self, initializer): + """Test CLI installed check - success.""" + mock_result = Mock() + mock_result.returncode = 0 + + with patch('subprocess.run', return_value=mock_result): + assert initializer.check_cli_installed() is True + + def test_check_cli_installed_failure(self, initializer): + """Test CLI installed check - failure.""" + with patch('subprocess.run', side_effect=FileNotFoundError): + assert initializer.check_cli_installed() is False + + def test_create_app_config(self, initializer, tmp_path): + """Test creating app configuration file.""" + initializer.create_app_config(tmp_path, "test-app", "read_products") + + config_file = tmp_path / "shopify.app.toml" + assert config_file.exists() + + content = config_file.read_text() + assert 'name = "test-app"' in content + assert 'scopes = "read_products"' in content + assert 'client_id = "test_key"' in content + + def test_create_extension_config(self, initializer, tmp_path): + """Test creating extension configuration file.""" + initializer.create_extension_config(tmp_path, "test-ext", "checkout") + + config_file = tmp_path / "shopify.extension.toml" + assert config_file.exists() + + content = config_file.read_text() + assert 'name = "test-ext"' in content + assert 'purchase.checkout.block.render' in content + + def test_create_extension_config_admin_action(self, initializer, tmp_path): + """Test creating admin action extension config.""" + initializer.create_extension_config(tmp_path, "admin-ext", "admin_action") + + config_file = tmp_path / "shopify.extension.toml" + content = config_file.read_text() + assert 'admin.product-details.action.render' in content + + def test_create_readme(self, initializer, tmp_path): + """Test creating README file.""" + initializer.create_readme(tmp_path, "app", "Test App") + + readme_file = tmp_path / "README.md" + assert readme_file.exists() + + content = readme_file.read_text() + assert '# Test App' in content + assert 'shopify app dev' in content + + @patch('builtins.input') + @patch('builtins.print') + def test_init_app(self, mock_print, mock_input, initializer, tmp_path, monkeypatch): + """Test app initialization.""" + monkeypatch.chdir(tmp_path) + + # Mock user inputs + mock_input.side_effect = ['my-app', 'read_products,write_products'] + + initializer.init_app() + + # Check directory created + app_dir = tmp_path / "my-app" + assert app_dir.exists() + + # Check files created + assert (app_dir / "shopify.app.toml").exists() + assert (app_dir / "README.md").exists() + assert (app_dir / "package.json").exists() + + # Check package.json content + package_json = json.loads((app_dir / "package.json").read_text()) + assert package_json['name'] == 'my-app' + assert 'dev' in package_json['scripts'] + + @patch('builtins.input') + @patch('builtins.print') + def test_init_extension(self, mock_print, mock_input, initializer, tmp_path, monkeypatch): + """Test extension initialization.""" + monkeypatch.chdir(tmp_path) + + # Mock user inputs: type selection (1 = checkout), name + mock_input.side_effect = ['1', 'my-extension'] + + initializer.init_extension() + + # Check directory and files created + ext_dir = tmp_path / "my-extension" + assert ext_dir.exists() + assert (ext_dir / "shopify.extension.toml").exists() + assert (ext_dir / "README.md").exists() + + @patch('builtins.input') + @patch('builtins.print') + def test_init_theme(self, mock_print, mock_input, initializer): + """Test theme initialization.""" + mock_input.return_value = 'my-theme' + + initializer.init_theme() + + assert mock_print.called + + @patch('builtins.print') + def test_run_no_cli(self, mock_print, initializer): + """Test run when CLI not installed.""" + with patch.object(initializer, 'check_cli_installed', return_value=False): + with pytest.raises(SystemExit) as exc_info: + initializer.run() + assert exc_info.value.code == 1 + + @patch.object(ShopifyInitializer, 'check_cli_installed', return_value=True) + @patch.object(ShopifyInitializer, 'init_app') + @patch('builtins.input') + @patch('builtins.print') + def test_run_app_selected(self, mock_print, mock_input, mock_init_app, mock_cli_check, initializer): + """Test run with app selection.""" + mock_input.return_value = '1' # Select app + + initializer.run() + + mock_init_app.assert_called_once() + + @patch.object(ShopifyInitializer, 'check_cli_installed', return_value=True) + @patch.object(ShopifyInitializer, 'init_extension') + @patch('builtins.input') + @patch('builtins.print') + def test_run_extension_selected(self, mock_print, mock_input, mock_init_ext, mock_cli_check, initializer): + """Test run with extension selection.""" + mock_input.return_value = '2' # Select extension + + initializer.run() + + mock_init_ext.assert_called_once() + + +class TestMain: + """Test main function.""" + + @patch('shopify_init.ShopifyInitializer') + @patch('shopify_init.EnvLoader') + def test_main_success(self, mock_loader, mock_initializer): + """Test main function success path.""" + from shopify_init import main + + mock_config = Mock() + mock_loader.load_config.return_value = mock_config + + mock_init_instance = Mock() + mock_initializer.return_value = mock_init_instance + + with patch('builtins.print'): + main() + + mock_init_instance.run.assert_called_once() + + @patch('shopify_init.ShopifyInitializer') + @patch('sys.exit') + def test_main_keyboard_interrupt(self, mock_exit, mock_initializer): + """Test main function with keyboard interrupt.""" + from shopify_init import main + + mock_initializer.return_value.run.side_effect = KeyboardInterrupt + + with patch('builtins.print'): + main() + + mock_exit.assert_called_with(0) + + @patch('shopify_init.ShopifyInitializer') + @patch('sys.exit') + def test_main_exception(self, mock_exit, mock_initializer): + """Test main function with exception.""" + from shopify_init import main + + mock_initializer.return_value.run.side_effect = Exception("Test error") + + with patch('builtins.print'): + main() + + mock_exit.assert_called_with(1) + + +class TestEnvConfig: + """Test EnvConfig dataclass.""" + + def test_env_config_defaults(self): + """Test EnvConfig default values.""" + config = EnvConfig() + + assert config.shopify_api_key is None + assert config.shopify_api_secret is None + assert config.shop_domain is None + assert config.scopes is None + + def test_env_config_with_values(self): + """Test EnvConfig with values.""" + config = EnvConfig( + shopify_api_key="key", + shopify_api_secret="secret", + shop_domain="test.myshopify.com", + scopes="read_products" + ) + + assert config.shopify_api_key == "key" + assert config.shopify_api_secret == "secret" + assert config.shop_domain == "test.myshopify.com" + assert config.scopes == "read_products" diff --git a/.claude/skills/tailwind/SKILL.md b/.claude/skills/tailwind/SKILL.md new file mode 100644 index 000000000..b86774848 --- /dev/null +++ b/.claude/skills/tailwind/SKILL.md @@ -0,0 +1,80 @@ +--- +name: tailwind +description: >- + Tailwind CSS v4 best-practices skill covering utility-first patterns, @theme + variables, responsive design, dark mode, custom styles, performance, + accessibility, and a Figma-to-Tailwind theme generation workflow. Use when + the user mentions Tailwind, tailwindcss, @theme, utility classes, Tailwind + config, Figma design tokens, or asks to build, configure, audit, or migrate + a Tailwind CSS project (including v3 to v4 migrations). +--- + +# Tailwind CSS Best Practices + +Tailwind CSS v4 guide organized as modular rules. Covers the utility-first model, `@theme` variables, responsive/state variants, custom styles, performance, accessibility, and the **Figma → Tailwind theme workflow** for generating design tokens directly from Figma variables. + +## ROUTING: Which rule file to load + +**IF setting up Tailwind or understanding how utility classes work:** +→ Read `rules/core-utility-model.md` + +**IF working with theme variables (`@theme`), design tokens, colors, fonts, spacing:** +→ Read `rules/core-theme-variables.md` + +**IF working with responsive design, hover/focus states, dark mode, or custom variants:** +→ Read `rules/core-responsive-and-states.md` + +**IF adding custom CSS, component classes, base styles, or custom utilities:** +→ Read `rules/core-custom-styles.md` + +**IF optimizing build size, purging unused classes, or configuring content detection:** +→ Read `rules/perf-purging-and-scanning.md` + +**IF working on accessibility or dark mode strategies:** +→ Read `rules/a11y-and-dark-mode.md` + +**IF translating Figma variables/design tokens into Tailwind v4 theme CSS:** +→ Read `rules/figma-to-theme-workflow.md` + see `figma-tokens/` templates + +## Rule index + +| Topic | Description | File | +|-------|-------------|------| +| Sections overview | Categories and reading order | [rules/_sections.md](rules/_sections.md) | +| Utility model | Utility-first principles, composing classes, arbitrary values | [rules/core-utility-model.md](rules/core-utility-model.md) | +| Theme variables | `@theme` directive, namespaces, extend/override, `inline`/`static` | [rules/core-theme-variables.md](rules/core-theme-variables.md) | +| Responsive & states | Breakpoints, hover/focus/dark variants, custom variants | [rules/core-responsive-and-states.md](rules/core-responsive-and-states.md) | +| Custom styles | `@layer`, `@utility`, `@variant`, component classes | [rules/core-custom-styles.md](rules/core-custom-styles.md) | +| Performance | Content detection, JIT, build optimization | [rules/perf-purging-and-scanning.md](rules/perf-purging-and-scanning.md) | +| A11y & dark mode | Accessibility utilities, dark mode patterns | [rules/a11y-and-dark-mode.md](rules/a11y-and-dark-mode.md) | +| **Figma workflow** | **Agent workflow: Figma variables → Tailwind `@theme` CSS** | [rules/figma-to-theme-workflow.md](rules/figma-to-theme-workflow.md) | + +## Figma → Tailwind theme workflow + +This skill includes a dedicated agent workflow to convert Figma design variables into Tailwind v4 `@theme` CSS files. The workflow is described in `rules/figma-to-theme-workflow.md` and uses the annotated templates in `figma-tokens/` as output targets. + +**How to trigger it:** paste your Figma CSS variables into chat and ask the agent to generate the Tailwind theme files. + +Template files in `figma-tokens/`: +- `colors.css` — `--color-*` namespace +- `typography.css` — `--font-*`, `--text-*`, `--font-weight-*`, `--tracking-*`, `--leading-*` +- `spacing.css` — `--spacing` and `--spacing-*` +- `radius-shadows.css` — `--radius-*`, `--shadow-*` +- `breakpoints.css` — `--breakpoint-*` + +## Rule categories by priority + +| Priority | Category | Impact | Prefix | +|----------|----------|--------|--------| +| 1 | Utility model & theme | CRITICAL | `core-` | +| 2 | Responsive & states | HIGH | `core-` | +| 3 | Custom styles | HIGH | `core-` | +| 4 | Figma workflow | HIGH | (standalone) | +| 5 | Performance | MEDIUM-HIGH | `perf-` | +| 6 | Accessibility | MEDIUM | `a11y-` | + +## Coverage and maintenance + +- Coverage map: `rules/_coverage-map.md` +- Source: https://tailwindcss.com/docs (v4.2) +- Update when Tailwind releases a new major/minor version with breaking `@theme` changes. diff --git a/.claude/skills/tailwind/figma-tokens/breakpoints.css b/.claude/skills/tailwind/figma-tokens/breakpoints.css new file mode 100644 index 000000000..79b5ee319 --- /dev/null +++ b/.claude/skills/tailwind/figma-tokens/breakpoints.css @@ -0,0 +1,39 @@ +/* Figma Design Tokens — Breakpoints + * + * This file is generated by the agent from Figma CSS variables. + * See: skills/tailwind/rules/figma-to-theme-workflow.md + * + * Namespace: --breakpoint-* + * Generates: responsive variant prefixes (sm:*, md:*, lg:*, xl:*, etc.) + * + * Usage: + * @import "./figma-tokens/breakpoints.css"; + * + * Notes: + * - Tailwind breakpoints are MINIMUM widths (mobile-first). + * - Convert px to rem (÷ 16) for accessibility (respects browser font size scaling). + * - Overriding a default breakpoint (e.g. --breakpoint-sm) replaces the default value. + * - Adding a new name (e.g. --breakpoint-3xl) adds a new responsive variant. + * - Figma "Desktop" frame widths are typically MAX widths — map them to Tailwind's + * MIN-width convention: e.g. Figma "Tablet" at 768px → --breakpoint-md: 48rem + * + * Figma frame → Tailwind breakpoint mapping (common): + * Mobile: 360px → not usually a breakpoint (base/mobile-first) + * Tablet: 768px → --breakpoint-md: 48rem + * Desktop: 1024px → --breakpoint-lg: 64rem + * Wide: 1280px → --breakpoint-xl: 80rem + * + * Example output — replace with your actual tokens: + */ + +@theme { + /* Standard breakpoints (override Tailwind defaults or leave as-is) */ + --breakpoint-sm: 40rem; /* 640px */ + --breakpoint-md: 48rem; /* 768px */ + --breakpoint-lg: 64rem; /* 1024px */ + --breakpoint-xl: 80rem; /* 1280px */ + --breakpoint-2xl: 96rem; /* 1536px */ + + /* Custom breakpoints (add as needed) */ + /* --breakpoint-3xl: 120rem; */ /* 1920px */ +} diff --git a/.claude/skills/tailwind/figma-tokens/colors.css b/.claude/skills/tailwind/figma-tokens/colors.css new file mode 100644 index 000000000..b994e5764 --- /dev/null +++ b/.claude/skills/tailwind/figma-tokens/colors.css @@ -0,0 +1,42 @@ +/* Figma Design Tokens — Colors + * + * This file is generated by the agent from Figma CSS variables. + * See: skills/tailwind/rules/figma-to-theme-workflow.md + * + * Namespace: --color-* + * Generates: bg-*, text-*, border-*, ring-*, fill-*, stroke-*, decoration-*, etc. + * + * Usage: + * @import "./figma-tokens/colors.css"; + * in your main app.css (after @import "tailwindcss") + * + * Notes: + * - Use OKLCH for perceptually uniform colors when possible. + * - HEX/RGB values are also valid. + * - To completely replace Tailwind's default palette, add: --color-*: initial; + * as the first declaration inside @theme (removes ALL default color utilities). + * + * Example output — replace with your actual tokens: + */ + +@theme { + /* Brand */ + --color-brand-50: oklch(0.97 0.02 40); + --color-brand-100: oklch(0.93 0.05 40); + --color-brand-200: oklch(0.87 0.10 40); + --color-brand-300: oklch(0.79 0.15 40); + --color-brand-400: oklch(0.72 0.18 40); + --color-brand-500: oklch(0.66 0.20 40); /* primary */ + --color-brand-600: oklch(0.58 0.19 40); + --color-brand-700: oklch(0.49 0.17 40); + --color-brand-800: oklch(0.38 0.13 40); + --color-brand-900: oklch(0.27 0.09 40); + --color-brand-950: oklch(0.18 0.06 40); + + /* Neutral (surface, text, borders) */ + --color-surface: oklch(0.98 0 0); + --color-surface-muted: oklch(0.95 0 0); + --color-text: oklch(0.15 0 0); + --color-text-muted: oklch(0.45 0 0); + --color-border: oklch(0.88 0 0); +} diff --git a/.claude/skills/tailwind/figma-tokens/radius-shadows.css b/.claude/skills/tailwind/figma-tokens/radius-shadows.css new file mode 100644 index 000000000..bbd935efc --- /dev/null +++ b/.claude/skills/tailwind/figma-tokens/radius-shadows.css @@ -0,0 +1,44 @@ +/* Figma Design Tokens — Border Radius & Shadows + * + * This file is generated by the agent from Figma CSS variables. + * See: skills/tailwind/rules/figma-to-theme-workflow.md + * + * Namespaces: + * --radius-* → rounded-* utilities + * --shadow-* → shadow-* utilities + * --inset-shadow-* → inset shadow utilities + * --drop-shadow-* → drop-shadow filter utilities + * + * Usage: + * @import "./figma-tokens/radius-shadows.css"; + * + * Notes: + * - Convert px radius values to rem (÷ 16). + * - Shadow values: use rgb(0 0 0 / 0.1) modern syntax instead of rgba(). + * - Multiple shadow layers: separate with comma in the value string. + * + * Example output — replace with your actual tokens: + */ + +@theme { + /* Border radius */ + --radius-xs: 0.125rem; /* 2px */ + --radius-sm: 0.25rem; /* 4px */ + --radius-md: 0.5rem; /* 8px */ + --radius-lg: 0.75rem; /* 12px */ + --radius-xl: 1rem; /* 16px */ + --radius-2xl: 1.5rem; /* 24px */ + --radius-full: 9999px; /* pill */ + + /* Box shadows */ + --shadow-xs: 0 1px 2px 0 rgb(0 0 0 / 0.05); + --shadow-sm: 0 1px 3px 0 rgb(0 0 0 / 0.10), 0 1px 2px -1px rgb(0 0 0 / 0.10); + --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.10), 0 2px 4px -2px rgb(0 0 0 / 0.10); + --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.10), 0 4px 6px -4px rgb(0 0 0 / 0.10); + --shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.10), 0 8px 10px -6px rgb(0 0 0 / 0.10); + --shadow-2xl: 0 25px 50px -12px rgb(0 0 0 / 0.25); + + /* Inset shadows */ + --inset-shadow-xs: inset 0 1px 1px rgb(0 0 0 / 0.05); + --inset-shadow-sm: inset 0 2px 4px rgb(0 0 0 / 0.05); +} diff --git a/.claude/skills/tailwind/figma-tokens/spacing.css b/.claude/skills/tailwind/figma-tokens/spacing.css new file mode 100644 index 000000000..96d952c11 --- /dev/null +++ b/.claude/skills/tailwind/figma-tokens/spacing.css @@ -0,0 +1,35 @@ +/* Figma Design Tokens — Spacing + * + * This file is generated by the agent from Figma CSS variables. + * See: skills/tailwind/rules/figma-to-theme-workflow.md + * + * Namespace: --spacing-* and --spacing (base unit) + * Generates: p-*, m-*, gap-*, w-*, h-*, inset-*, space-*, translate-*, etc. + * + * Usage: + * @import "./figma-tokens/spacing.css"; + * + * Notes: + * - If Figma has a single base grid unit (e.g. 4px), set --spacing to that value. + * Tailwind then uses multipliers: p-4 = 4 × --spacing. + * - If Figma exports named steps, define them as --spacing-{name} tokens. + * - Convert px to rem (÷ 16) for scalable layouts. + * - Named tokens like --spacing-md generate utilities: p-md, m-md, gap-md, etc. + * + * Example output — replace with your actual tokens: + */ + +@theme { + /* Base unit — all numeric spacing utilities are multiples of this value. + * With --spacing: 0.25rem, then: p-1 = 0.25rem, p-4 = 1rem, p-8 = 2rem, etc. */ + --spacing: 0.25rem; /* 4px base grid */ + + /* Named spacing tokens (optional — use when Figma defines semantic steps) */ + --spacing-xs: 0.25rem; /* 4px */ + --spacing-sm: 0.5rem; /* 8px */ + --spacing-md: 1rem; /* 16px */ + --spacing-lg: 1.5rem; /* 24px */ + --spacing-xl: 2.5rem; /* 40px */ + --spacing-2xl: 4rem; /* 64px */ + --spacing-3xl: 6rem; /* 96px */ +} diff --git a/.claude/skills/tailwind/figma-tokens/typography.css b/.claude/skills/tailwind/figma-tokens/typography.css new file mode 100644 index 000000000..1aaf50b91 --- /dev/null +++ b/.claude/skills/tailwind/figma-tokens/typography.css @@ -0,0 +1,65 @@ +/* Figma Design Tokens — Typography + * + * This file is generated by the agent from Figma CSS variables. + * See: skills/tailwind/rules/figma-to-theme-workflow.md + * + * Namespaces: + * --font-* → font-family utilities (font-sans, font-heading, etc.) + * --text-* → font-size utilities (text-sm, text-xl, etc.) + * --font-weight-* → font-weight utilities (font-normal, font-bold, etc.) + * --tracking-* → letter-spacing utilities (tracking-tight, etc.) + * --leading-* → line-height utilities (leading-normal, etc.) + * + * Usage: + * @import "./figma-tokens/typography.css"; + * + * Notes: + * - px values should be converted to rem (÷ 16). + * - Include --text-*--line-height companion vars when Figma provides line-height. + * - Font stacks: always include a generic fallback (sans-serif, monospace, etc.) + * + * Example output — replace with your actual tokens: + */ + +@theme { + /* Font families */ + --font-heading: "Neue Haas Grotesk Display", ui-sans-serif, sans-serif; + --font-body: "Inter", ui-sans-serif, sans-serif; + --font-mono: "JetBrains Mono", ui-monospace, monospace; + + /* Font sizes (px → rem: value / 16) */ + --text-xs: 0.75rem; /* 12px */ + --text-sm: 0.875rem; /* 14px */ + --text-base: 1rem; /* 16px */ + --text-lg: 1.125rem; /* 18px */ + --text-xl: 1.25rem; /* 20px */ + --text-2xl: 1.5rem; /* 24px */ + --text-3xl: 2rem; /* 32px */ + --text-4xl: 3rem; /* 48px */ + --text-5xl: 4rem; /* 64px */ + + /* Line heights (companion vars — optional but recommended) */ + --text-xs--line-height: 1.5; + --text-sm--line-height: 1.5; + --text-base--line-height: 1.5; + --text-lg--line-height: 1.6; + --text-xl--line-height: 1.4; + --text-2xl--line-height: 1.3; + --text-3xl--line-height: 1.2; + --text-4xl--line-height: 1.1; + --text-5xl--line-height: 1; + + /* Letter spacing */ + --tracking-tight: -0.03em; + --tracking-normal: 0; + --tracking-wide: 0.04em; + --tracking-widest: 0.1em; + + /* Line heights (standalone) */ + --leading-none: 1; + --leading-tight: 1.25; + --leading-snug: 1.375; + --leading-normal: 1.5; + --leading-relaxed: 1.625; + --leading-loose: 2; +} diff --git a/.claude/skills/tailwind/rules/a11y-and-dark-mode.md b/.claude/skills/tailwind/rules/a11y-and-dark-mode.md new file mode 100644 index 000000000..1c5336b60 --- /dev/null +++ b/.claude/skills/tailwind/rules/a11y-and-dark-mode.md @@ -0,0 +1,135 @@ +# a11y-and-dark-mode + +Why it matters: accessibility and color scheme support are not afterthoughts — they affect usability for a large portion of users and are increasingly required by law (WCAG compliance). + +## Screen Reader Utilities + +```html + + + + + + Skip to main content + +``` + +## Focus Styles + +Never remove focus rings without a proper replacement: + +```html + + + + + +``` + +`focus-visible:` only shows the ring for keyboard navigation, not mouse clicks. + +## Color Contrast + +Use Tailwind's palette with contrast in mind: + +```html + +

Hard to read

+ + +

Easy to read

+

Also fine

+``` + +Verify contrast ratios when defining custom `@theme` colors. Prefer OKLCH for predictable perceptual lightness. + +## Reduced Motion + +```html + +
+``` + +```css +/* In custom CSS */ +.animated-element { + transition: transform 300ms ease; + + @variant motion-reduce { + transition: none; + } +} +``` + +## Forced Colors (High Contrast Mode) + +```html + +
+ Important UI element +
+ + +
+ Regular content +
+``` + +## Dark Mode — Design Guidance + +When implementing dark mode, ensure: + +1. **All interactive states have dark variants:** `hover:`, `focus:`, `active:` need `dark:` counterparts. + +```html + +
Larger shadow
+``` + +## Adding custom utilities with `@utility` + +```css +/* Simple custom utility */ +@utility content-auto { + content-visibility: auto; +} + +/* Complex utility with nested selectors */ +@utility scrollbar-hidden { + &::-webkit-scrollbar { display: none; } + scrollbar-width: none; +} +``` + +```html + +
+
+``` + +## Using `@variant` in custom CSS + +```css +.custom-element { + background: var(--color-white); + + @variant dark { + background: var(--color-gray-900); + } + + @variant hover { + background: var(--color-gray-50); + } +} +``` + +## Custom variants with `@custom-variant` + +```css +/* Shorthand (no nesting needed) */ +@custom-variant hocus (&:hover, &:focus); + +/* With nesting */ +@custom-variant any-hover { + @media (any-hover: hover) { + &:hover { @slot; } + } +} +``` + +```html + +Only underlines on hover-capable devices +``` + +## When NOT to use `@apply` + +```css +/* Incorrect — using @apply for one-off element styles */ +.hero { @apply text-4xl font-bold mb-8 text-gray-900; } + +/* Correct — write utilities in HTML directly */ +/*

*/ + +/* @apply is appropriate for genuinely reusable component abstractions */ +@layer components { + .form-input { + @apply w-full rounded-md border border-gray-300 px-3 py-2 text-sm + focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500; + } +} +``` + +## Docs + +- https://tailwindcss.com/docs/adding-custom-styles +- https://tailwindcss.com/docs/functions-and-directives diff --git a/.claude/skills/tailwind/rules/core-responsive-and-states.md b/.claude/skills/tailwind/rules/core-responsive-and-states.md new file mode 100644 index 000000000..3df9e2c2e --- /dev/null +++ b/.claude/skills/tailwind/rules/core-responsive-and-states.md @@ -0,0 +1,164 @@ +# core-responsive-and-states + +Why it matters: Tailwind's modifier system is the primary way to handle responsive design, interactive states, and dark mode — understanding how modifiers compose prevents redundant CSS and ensures consistent behavior. + +## Responsive Design (mobile-first) + +```html + +
+``` + +Default breakpoints (from `@theme`): + +| Modifier | Min-width | +|----------|-----------| +| `sm:` | 40rem (640px) | +| `md:` | 48rem (768px) | +| `lg:` | 64rem (1024px) | +| `xl:` | 80rem (1280px) | +| `2xl:` | 96rem (1536px) | + +Custom breakpoint: + +```css +@theme { + --breakpoint-3xl: 120rem; +} +``` + +```html +
...
+``` + +## State Modifiers + +### Hover, Focus, Active + +```html + +``` + +### Group and Peer + +```html + +
+

Title

+

Description

+
+ + + + + +``` + +### First, Last, Odd, Even + +```html +
    +
  • Item
  • +
+``` + +## Dark Mode + +### Strategy 1: media query (default) + +```css +/* app.css */ +@import "tailwindcss"; +/* dark: variant uses prefers-color-scheme by default */ +``` + +```html +
+ Adapts to OS preference +
+``` + +### Strategy 2: class-based (manual toggle) + +```css +@import "tailwindcss"; + +@variant dark (&:where(.dark, .dark *)); +``` + +```html + + + ... + +``` + +```js +// Toggle dark mode +document.documentElement.classList.toggle('dark') +``` + +### Strategy 3: data attribute + +```css +@variant dark (&:where([data-theme="dark"], [data-theme="dark"] *)); +``` + +```html +... +``` + +## Custom Variants + +```css +/* @custom-variant for data attributes */ +@custom-variant theme-brand (&:where([data-theme="brand"] *)); +``` + +```html + +
...
+ +``` + +## Stacking modifiers + +```html + + + ${a.quantity} + + +
+

${T(G(a),r)}

+

+ + `}).join("")},_=async()=>{d("");try{const t=await P();L(t)}catch{d("Could not load your cart. Redirecting to cart page..."),window.location.assign(F)}},b=async(t,r)=>{if(!m){m=!0,d(""),u("Updating cart..."),A(!0);try{const n=await O(t,r);L(n),u(r===0?"Item removed.":"Cart updated.")}catch{d("Could not update cart. Please try again."),u("Cart update failed.")}finally{m=!1,A(!1)}}},H=()=>Array.from(C.querySelectorAll(Q)).filter(t=>!t.hasAttribute("disabled")&&!t.getAttribute("aria-hidden")),R=t=>{if(!s)return;if(t.key==="Escape"){t.preventDefault(),v();return}if(t.key!=="Tab")return;const r=H();if(r.length===0)return;const n=r[0],a=r[r.length-1];if(t.shiftKey&&document.activeElement===n){t.preventDefault(),a.focus();return}!t.shiftKey&&document.activeElement===a&&(t.preventDefault(),n.focus())};function K(t){s||(s=!0,p=t??(document.activeElement instanceof HTMLElement?document.activeElement:null),o.hidden=!1,o.setAttribute("aria-hidden","false"),document.body.style.overflow="hidden",document.addEventListener("keydown",R),x.focus(),_())}function v(){s&&(s=!1,o.hidden=!0,o.setAttribute("aria-hidden","true"),document.body.style.overflow="",document.removeEventListener("keydown",R),p&&p.focus())}document.querySelectorAll('[data-js="cart-open"]').forEach(t=>{t.addEventListener("click",r=>{r.preventDefault(),K(t)})}),x.addEventListener("click",v),B.addEventListener("click",v),o.addEventListener("click",t=>{const n=t.target.closest('[data-js="cart-qty-dec"], [data-js="cart-qty-inc"], [data-js="cart-remove"]');if(!n)return;const a=Number(n.dataset.line);if(!a)return;const f=y.querySelector(`[data-line="${a}"]`)?.querySelector('[data-js="cart-qty-value"]'),i=Number(f?.textContent??"1");if(n.dataset.js==="cart-remove"){b(a,0);return}if(n.dataset.js==="cart-qty-inc"){b(a,i+1);return}n.dataset.js==="cart-qty-dec"&&b(a,Math.max(0,i-1))}),_()}export{V as initCartDrawer}; diff --git a/assets/handlers-D9xx4MJz.js b/assets/handlers-D9xx4MJz.js new file mode 100644 index 000000000..41e413461 --- /dev/null +++ b/assets/handlers-D9xx4MJz.js @@ -0,0 +1 @@ +import{f as c}from"./variant-picker-DRrQ71gT.js";import{a as u}from"./cart-CmG_L9o1.js";import{s as t}from"./state-Bj51Sk4P.js";import{s as r}from"./sync-DkV0cu7G.js";function f(n){const e=n.target.closest('[data-js="option-value"]');if(!e||e.disabled||e.getAttribute("aria-disabled")==="true")return;const a=Number(e.dataset.optionPosition)-1,i=e.dataset.optionValue??"";if(a<0||!i)return;t.selectedOptions[a]=i;const o=c(t.productData.variants,t.selectedOptions);if(!o){t.selectedOptions[a]=t.currentVariant.options[a]??t.selectedOptions[a];return}t.currentVariant=o,t.currentMediaId=o.featured_media?.id??null;const s=new URL(window.location.href);s.searchParams.set("variant",String(o.id)),history.replaceState({},"",`${s.pathname}${s.search}${s.hash}`),r()}function b(n){const e=n.target.closest('[data-js="thumbnail"]');if(!e)return;const a=Number(e.dataset.thumbnail);a&&(t.currentMediaId=a,r())}async function h(n){if(n.preventDefault(),t.cartState==="loading")return;const a=n.target.querySelector('input[name="quantity"]'),i=a?Math.max(1,Number(a.value)||1):1;t.cartState="loading",r();try{await u(t.currentVariant.id,i),t.cartState="success",r(),setTimeout(()=>{t.cartState="idle",r()},2e3)}catch{t.cartState="error",r(),setTimeout(()=>{t.cartState="idle",r()},3e3)}}export{h as a,b,f as o}; diff --git a/assets/main-C4sk1vw8.css b/assets/main-C4sk1vw8.css deleted file mode 100644 index 1c6cfcd0b..000000000 --- a/assets/main-C4sk1vw8.css +++ /dev/null @@ -1 +0,0 @@ -@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components,utilities; diff --git a/assets/main-CZyZ8laC.css b/assets/main-CZyZ8laC.css new file mode 100644 index 000000000..0a29125fc --- /dev/null +++ b/assets/main-CZyZ8laC.css @@ -0,0 +1 @@ +@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-border-style:solid;--tw-font-weight:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-duration:initial;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-500:oklch(63.7% .237 25.331);--color-red-700:oklch(50.5% .213 27.518);--color-yellow-500:oklch(79.5% .184 86.047);--color-green-500:oklch(72.3% .219 149.579);--color-blue-50:oklch(97% .014 254.604);--color-blue-400:oklch(70.7% .165 254.624);--color-blue-500:oklch(62.3% .214 259.815);--color-blue-600:oklch(54.6% .245 262.881);--color-blue-700:oklch(48.8% .243 264.376);--color-gray-50:oklch(98.5% .002 247.839);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-300:oklch(87.2% .01 258.338);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-600:oklch(44.6% .03 256.802);--color-gray-700:oklch(37.3% .034 259.733);--color-gray-800:oklch(27.8% .033 256.848);--color-gray-900:oklch(21% .034 264.665);--color-neutral-100:oklch(97% 0 0);--color-neutral-900:oklch(20.5% 0 0);--color-black:#000;--color-white:#fff;--spacing:.25rem;--breakpoint-sm:40rem;--breakpoint-md:48rem;--breakpoint-lg:64rem;--breakpoint-xl:80rem;--container-md:28rem;--container-xl:36rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height: 1.5 ;--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-3xl:1.875rem;--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5 / 2.25);--font-weight-thin:100;--font-weight-extralight:200;--font-weight-light:300;--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--font-weight-extrabold:800;--font-weight-black:900;--radius-sm:.25rem;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--shadow-sm:0 1px 3px 0 #0000001a, 0 1px 2px -1px #0000001a;--shadow-md:0 4px 6px -1px #0000001a, 0 2px 4px -2px #0000001a;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.invisible{visibility:hidden}.visible{visibility:visible}.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.inset-0{inset:calc(var(--spacing) * 0)}.start{inset-inline-start:var(--spacing)}.end{inset-inline-end:var(--spacing)}.top-0{top:calc(var(--spacing) * 0)}.top-\[117px\]{top:117px}.right-0{right:calc(var(--spacing) * 0)}.bottom-0{bottom:calc(var(--spacing) * 0)}.left-0{left:calc(var(--spacing) * 0)}.z-20{z-index:20}.z-50{z-index:50}.container{width:100%}@media(min-width:40rem){.container{max-width:40rem}}@media(min-width:48rem){.container{max-width:48rem}}@media(min-width:64rem){.container{max-width:64rem}}@media(min-width:80rem){.container{max-width:80rem}}@media(min-width:96rem){.container{max-width:96rem}}.mx-2{margin-inline:calc(var(--spacing) * 2)}.mt-1{margin-top:calc(var(--spacing) * 1)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-8{margin-bottom:calc(var(--spacing) * 8)}.ml-auto{margin-left:auto}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.aspect-square{aspect-ratio:1}.h-8{height:calc(var(--spacing) * 8)}.h-9{height:calc(var(--spacing) * 9)}.h-10{height:calc(var(--spacing) * 10)}.h-16{height:calc(var(--spacing) * 16)}.h-20{height:calc(var(--spacing) * 20)}.h-full{height:100%}.max-h-\[calc\(100vh-240px\)\]{max-height:calc(100vh - 240px)}.min-h-svh{min-height:100svh}.w-8{width:calc(var(--spacing) * 8)}.w-9{width:calc(var(--spacing) * 9)}.w-10{width:calc(var(--spacing) * 10)}.w-16{width:calc(var(--spacing) * 16)}.w-20{width:calc(var(--spacing) * 20)}.w-\[calc\(100\%-2rem\)\]{width:calc(100% - 2rem)}.w-full{width:100%}.max-w-md{max-width:var(--container-md)}.max-w-xl{max-width:var(--container-xl)}.min-w-6{min-width:calc(var(--spacing) * 6)}.flex-1{flex:1}.shrink-0{flex-shrink:0}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.cursor-pointer{cursor:pointer}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-\[64px_1fr\]{grid-template-columns:64px 1fr}.grid-cols-\[80px_1fr\]{grid-template-columns:80px 1fr}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-end{align-items:flex-end}.justify-between{justify-content:space-between}.gap-1{gap:calc(var(--spacing) * 1)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 5) * calc(1 - var(--tw-space-y-reverse)))}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-xl{border-radius:var(--radius-xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-gray-300{border-color:var(--color-gray-300)}.bg-\[\#FF5A00\]{background-color:#ff5a00}.bg-black\/40{background-color:#0006}@supports (color:color-mix(in lab,red,red)){.bg-black\/40{background-color:color-mix(in oklab,var(--color-black) 40%,transparent)}}.bg-blue-400{background-color:var(--color-blue-400)}.bg-blue-500{background-color:var(--color-blue-500)}.bg-blue-600{background-color:var(--color-blue-600)}.bg-gray-800{background-color:var(--color-gray-800)}.bg-white{background-color:var(--color-white)}.\[mask-type\:luminance\]{mask-type:luminance}.fill-\(--brand-color\){fill:var(--brand-color)}.object-cover{object-fit:cover}.p-2{padding:calc(var(--spacing) * 2)}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.p-6{padding:calc(var(--spacing) * 6)}.p-8{padding:calc(var(--spacing) * 8)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.py-1{padding-block:calc(var(--spacing) * 1)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-8{padding-block:calc(var(--spacing) * 8)}.pb-3{padding-bottom:calc(var(--spacing) * 3)}.pb-4{padding-bottom:calc(var(--spacing) * 4)}.text-center{text-align:center}.text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.text-\(color\:--brand-text\){color:var(--brand-text)}.text-blue-500{color:var(--color-blue-500)}.text-gray-300{color:var(--color-gray-300)}.text-gray-500{color:var(--color-gray-500)}.text-gray-600{color:var(--color-gray-600)}.text-gray-700{color:var(--color-gray-700)}.text-gray-800{color:var(--color-gray-800)}.text-gray-900{color:var(--color-gray-900)}.text-green-500{color:var(--color-green-500)}.text-red-500{color:var(--color-red-500)}.text-red-700{color:var(--color-red-700)}.text-white{color:var(--color-white)}.text-yellow-500{color:var(--color-yellow-500)}.capitalize{text-transform:capitalize}.underline{text-decoration-line:underline}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.opacity-100{opacity:1}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-2xl{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a), 0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a), 0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-300{--tw-duration:.3s;transition-duration:.3s}.forced-color-adjust-auto{forced-color-adjust:auto}.forced-color-adjust-none{forced-color-adjust:none}@media(hover:hover){.group-hover\:text-blue-600:is(:where(.group):hover *){color:var(--color-blue-600)}.group-hover\:text-gray-700:is(:where(.group):hover *){color:var(--color-gray-700)}.group-hover\:underline:is(:where(.group):hover *){text-decoration-line:underline}}.peer-checked\:block:is(:where(.peer):checked~*){display:block}.first\:pt-0:first-child{padding-top:calc(var(--spacing) * 0)}.last\:pb-0:last-child{padding-bottom:calc(var(--spacing) * 0)}.odd\:bg-gray-50:nth-child(odd){background-color:var(--color-gray-50)}.even\:bg-white:nth-child(2n){background-color:var(--color-white)}.focus-within\:ring-2:focus-within{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}@media(hover:hover){.hover\:-translate-y-1:hover{--tw-translate-y:calc(var(--spacing) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.hover\:bg-blue-50:hover{background-color:var(--color-blue-50)}.hover\:bg-blue-600:hover{background-color:var(--color-blue-600)}.hover\:bg-blue-700:hover{background-color:var(--color-blue-700)}.hover\:bg-gray-50:hover{background-color:var(--color-gray-50)}.hover\:shadow-lg:hover{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}}.focus\:not-sr-only:focus{clip-path:none;white-space:normal;width:auto;height:auto;margin:0;padding:0;position:static;overflow:visible}.focus\:absolute:focus{position:absolute}.focus\:top-4:focus{top:calc(var(--spacing) * 4)}.focus\:left-4:focus{left:calc(var(--spacing) * 4)}.focus\:z-50:focus{z-index:50}.focus\:rounded:focus{border-radius:.25rem}.focus\:border-blue-500:focus{border-color:var(--color-blue-500)}.focus\:bg-white:focus{background-color:var(--color-white)}.focus\:px-4:focus{padding-inline:calc(var(--spacing) * 4)}.focus\:py-2:focus{padding-block:calc(var(--spacing) * 2)}.focus\:ring-1:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-blue-500:focus{--tw-ring-color:var(--color-blue-500)}.focus\:ring-offset-2:focus{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.focus-visible\:ring-2:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus-visible\:ring-blue-500:focus-visible{--tw-ring-color:var(--color-blue-500)}.focus-visible\:ring-offset-2:focus-visible{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.active\:bg-blue-700:active{background-color:var(--color-blue-700)}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}@media(prefers-reduced-motion:reduce){.motion-reduce\:transform-none{transform:none}.motion-reduce\:transition-none{transition-property:none}}@media(min-width:40rem){.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(min-width:48rem){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:p-8{padding:calc(var(--spacing) * 8)}.md\:px-8{padding-inline:calc(var(--spacing) * 8)}@media(hover:hover){.md\:hover\:px-10:hover{padding-inline:calc(var(--spacing) * 10)}}}@media(min-width:64rem){.lg\:w-16{width:calc(var(--spacing) * 16)}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:flex-col{flex-direction:column}.lg\:flex-row-reverse{flex-direction:row-reverse}.lg\:p-12{padding:calc(var(--spacing) * 12)}}@media(min-width:80rem){.xl\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}}@media(prefers-color-scheme:dark){.dark\:bg-blue-400{background-color:var(--color-blue-400)}.dark\:bg-blue-500{background-color:var(--color-blue-500)}.dark\:bg-gray-900{background-color:var(--color-gray-900)}.dark\:text-gray-100{color:var(--color-gray-100)}.dark\:opacity-90{opacity:.9}.dark\:shadow-none{--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark\:ring-1{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark\:ring-white\/10{--tw-ring-color:#ffffff1a}@supports (color:color-mix(in lab,red,red)){.dark\:ring-white\/10{--tw-ring-color:color-mix(in oklab, var(--color-white) 10%, transparent)}}@media(hover:hover){.dark\:hover\:bg-blue-400:hover{background-color:var(--color-blue-400)}.dark\:hover\:bg-blue-500:hover{background-color:var(--color-blue-500)}}}}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0} diff --git a/assets/page-CWMiLRqk.js b/assets/page-CWMiLRqk.js new file mode 100644 index 000000000..9ede39648 --- /dev/null +++ b/assets/page-CWMiLRqk.js @@ -0,0 +1,18 @@ +import{c as _,g as x}from"./cart-CmG_L9o1.js";function q(t){return t.replaceAll("&","&").replaceAll("<","<").replaceAll(">",">").replaceAll('"',""").replaceAll("'","'")}function $(t,c){try{return new Intl.NumberFormat(void 0,{style:"currency",currency:c}).format(t/100)}catch{return`${(t/100).toFixed(2)} ${c}`}}function A(t){return t.image??t.featured_image?.url??null}function I(t){return t.product_title??t.title??"Product"}function U(t){return t.final_line_price??t.line_price??0}function N(){const t=document.querySelector('[data-js="cart-page"]');if(!t)return;const c=t.querySelector('[data-js="cart-page-items"]'),g=t.querySelector('[data-js="cart-page-empty"]'),f=t.querySelector('[data-js="cart-page-footer"]'),y=t.querySelector('[data-js="cart-page-subtotal"]'),m=t.querySelector('[data-js="cart-page-status"]'),b=t.querySelector('[data-js="cart-page-error"]'),S=document.querySelectorAll('[data-js="cart-count"]');if(!c||!g||!f||!y||!m||!b)return;const w="/cart";let u=!1;const l=e=>{m.textContent=e},d=e=>{b.textContent=e},j=e=>{t.setAttribute("aria-busy",String(e)),t.querySelectorAll("button").forEach(n=>{n.type!=="submit"&&(n.disabled=e)})},h=e=>{const n=e.currency||t.dataset.currency||"USD",r=e.item_count>0;if(S.forEach(a=>{a.textContent=String(e.item_count),a.hidden=e.item_count<1}),g.hidden=r,c.hidden=!r,f.hidden=!r,y.textContent=$(e.total_price,n),!r){c.innerHTML="",l("Your cart is empty.");return}c.innerHTML=e.items.map((a,o)=>{const i=A(a),s=q(I(a)),v=a.variant_title?q(a.variant_title):"";return` +
  • + + ${i?`${s}`:""} + +
    + ${s} + ${v?`

    ${v}

    `:""} +
    + + ${a.quantity} + + +
    +

    ${$(U(a),n)}

    +
    +
  • + `}).join("")},C=async()=>{try{const e=await x();h(e)}catch{d("Could not load cart data. Reloading..."),window.location.assign(w)}},p=async(e,n)=>{if(!u){u=!0,j(!0),d(""),l("Updating cart...");try{const r=await _(e,n);h(r),l(n===0?"Item removed.":"Cart updated.")}catch{d("Could not update cart. Please try again."),l("Cart update failed.")}finally{u=!1,j(!1)}}};t.addEventListener("click",e=>{const r=e.target.closest('[data-js="cart-page-dec"], [data-js="cart-page-inc"], [data-js="cart-page-remove"]');if(!r)return;const a=Number(r.dataset.line);if(!a)return;const i=c.querySelector(`[data-line="${a}"]`)?.querySelector('[data-js="cart-page-qty"]'),s=Number(i?.textContent??"1");if(r.dataset.js==="cart-page-remove"){p(a,0);return}if(r.dataset.js==="cart-page-inc"){p(a,s+1);return}r.dataset.js==="cart-page-dec"&&p(a,Math.max(0,s-1))}),C()}export{N as i}; diff --git a/assets/product-DPYfuql-.js b/assets/product-DPYfuql-.js new file mode 100644 index 000000000..ab3309a34 --- /dev/null +++ b/assets/product-DPYfuql-.js @@ -0,0 +1 @@ +import{s as e}from"./state-Bj51Sk4P.js";import{s as r}from"./sync-DkV0cu7G.js";import{o as s,a as c,b as m}from"./handlers-D9xx4MJz.js";import{l as u}from"./recommendations-H0stnrna.js";import"./variant-picker-DRrQ71gT.js";import"./cart-CmG_L9o1.js";function i(t){const n=new URLSearchParams(window.location.search),o=Number(n.get("variant"));return t.variants.find(a=>a.id===o)??t.variants.find(a=>a.available)??t.variants[0]}function d(t){e.currentVariant=t,e.selectedOptions=[...t.options],e.currentMediaId=t.featured_media?.id??null}function l(){const t=document.querySelector('[data-js="product-data"]');if(!t?.textContent)return;e.productData=JSON.parse(t.textContent);const n=document.querySelector('[data-js="variant-prices"]');e.variantPrices=n?.textContent?JSON.parse(n.textContent):{},d(i(e.productData));const o=document.querySelector('[data-js="product-form"]');o?.addEventListener("click",s),o?.addEventListener("submit",a=>{c(a)}),document.addEventListener("click",m),window.addEventListener("popstate",()=>{d(i(e.productData)),e.cartState="idle",r()}),r(),u()}document.addEventListener("DOMContentLoaded",l); diff --git a/assets/product-l0sNRNKZ.js b/assets/product-l0sNRNKZ.js deleted file mode 100644 index 8b1378917..000000000 --- a/assets/product-l0sNRNKZ.js +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/recommendations-H0stnrna.js b/assets/recommendations-H0stnrna.js new file mode 100644 index 000000000..387451d8f --- /dev/null +++ b/assets/recommendations-H0stnrna.js @@ -0,0 +1 @@ +function s(){const t=document.querySelector('[data-js="recommendations"]');if(!t)return;const n=t.dataset.productId,o=t.dataset.sectionId;if(!n||!o)return;const c=`${window.Shopify.routes.root}recommendations/products?section_id=${o}&product_id=${n}&intent=related`;fetch(c).then(e=>e.text()).then(e=>{const r=new DOMParser().parseFromString(e,"text/html").querySelector('[data-js="product-recommendations-root"]');r&&(t.innerHTML=r.innerHTML)}).catch(()=>{})}export{s as l}; diff --git a/assets/search-Dt-ufTnA.js b/assets/search-Dt-ufTnA.js new file mode 100644 index 000000000..cddef7a18 --- /dev/null +++ b/assets/search-Dt-ufTnA.js @@ -0,0 +1,8 @@ +function v(t){return t.replaceAll("&","&").replaceAll("<","<").replaceAll(">",">").replaceAll('"',""").replaceAll("'","'")}function q(t,r=250){let n=null;return(...a)=>{n!==null&&window.clearTimeout(n),n=window.setTimeout(()=>{t(...a)},r)}}document.addEventListener("DOMContentLoaded",()=>{const t=document.querySelector('[data-js="search-root"]');if(!t)return;const r=t.querySelector('[data-js="search-input"]'),n=t.querySelector('[data-js="predictive-results"]'),a=t.querySelector('[data-js="predictive-list"]'),p=t.querySelector('[data-js="predictive-status"]'),h=t.querySelector('[data-js="search-status"]');if(!r||!n||!a||!p||!h)return;let u=null;const f=e=>{h.textContent=e},c=e=>{p.textContent=e},d=()=>{n.hidden=!0,r.setAttribute("aria-expanded","false")},i=()=>{n.hidden=!1,r.setAttribute("aria-expanded","true")},g=e=>{if(e.length===0){a.innerHTML="",c("No quick matches. Press Enter for full results."),i();return}a.innerHTML=e.map(s=>{const o=v(s.title),l=s.image?.url?`${v(s.image.alt??s.title)}`:"";return` +
  • + + ${l} + ${o} + +
  • + `}).join(""),c(`${e.length} quick matches`),i()},m=async e=>{if(e.length<2){d(),c("");return}u?.abort(),u=new AbortController,c("Searching..."),f("Predictive search active"),i();const s=`${window.Shopify.routes.root}search/suggest.json?q=${encodeURIComponent(e)}&resources[type]=product,page,article&resources[limit]=6&resources[options][unavailable_products]=last`;try{const o=await fetch(s,{signal:u.signal,headers:{Accept:"application/json","X-Requested-With":"XMLHttpRequest"}});if(!o.ok)throw new Error("Predictive endpoint unavailable");const l=await o.json(),y=l.resources?.results?.products??[],S=l.resources?.results?.pages??[],b=l.resources?.results?.articles??[];g([...y,...S,...b].slice(0,6))}catch(o){if(o instanceof DOMException&&o.name==="AbortError")return;d(),c(""),f("Predictive search unavailable. Full search still works.")}},w=q(e=>{m(e.trim())});r.addEventListener("input",()=>{w(r.value)}),r.addEventListener("focus",()=>{a.children.length>0&&i()}),t.addEventListener("focusout",()=>{window.setTimeout(()=>{const e=document.activeElement;e&&t.contains(e)||d()},100)})}); diff --git a/assets/search-l0sNRNKZ.js b/assets/search-l0sNRNKZ.js deleted file mode 100644 index 8b1378917..000000000 --- a/assets/search-l0sNRNKZ.js +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/state-Bj51Sk4P.js b/assets/state-Bj51Sk4P.js new file mode 100644 index 000000000..51036a0fa --- /dev/null +++ b/assets/state-Bj51Sk4P.js @@ -0,0 +1 @@ +const t={productData:null,currentVariant:null,selectedOptions:[],variantPrices:{},cartState:"idle",currentMediaId:null};export{t as s}; diff --git a/assets/sync-DkV0cu7G.js b/assets/sync-DkV0cu7G.js new file mode 100644 index 000000000..04498afdc --- /dev/null +++ b/assets/sync-DkV0cu7G.js @@ -0,0 +1 @@ +import{g as c}from"./variant-picker-DRrQ71gT.js";import{s as e}from"./state-Bj51Sk4P.js";function g(){u(),l(),S(),m(),f(),p(),y()}function u(){const t=document.querySelector('[data-js="product-price"]');if(!t)return;const a=e.variantPrices[String(e.currentVariant.id)];a&&(t.textContent=a)}function l(){const t=document.querySelector('[data-js="product-availability"]');t&&(t.textContent=e.currentVariant.available?"":"Sold out")}function S(){const t=document.querySelectorAll('[data-js="media-item"]'),a=String(e.currentVariant.id),n=e.currentMediaId!==null?String(e.currentMediaId):e.currentVariant.featured_media!==null?String(e.currentVariant.featured_media.id):null;t.forEach(r=>{const i=r.dataset.variantMedia,s=!i,d=i===a;let o;n!==null?o=r.dataset.mediaId===n:o=s||d,o?r.removeAttribute("hidden"):(r.setAttribute("hidden",""),r.querySelector("video")?.pause())}),document.querySelectorAll('[data-js="thumbnail"]').forEach(r=>{const i=r.dataset.variantMedia;!i||i===a?r.removeAttribute("hidden"):r.setAttribute("hidden",""),r.setAttribute("aria-pressed",String(r.dataset.thumbnail===n))})}function f(){const t=document.querySelector('[data-js="add-to-cart"]');if(!t)return;const a={idle:e.currentVariant.available?"Add to cart":"Sold out",loading:"Adding...",success:"Added!",error:"Try again"};t.disabled=!e.currentVariant.available||e.cartState==="loading",t.setAttribute("aria-busy",String(e.cartState==="loading")),t.textContent=a[e.cartState]}function m(){const t=document.querySelector('[data-js="variant-id"]');t&&(t.value=String(e.currentVariant.id))}function p(){const t=document.querySelector('[data-js="cart-status"]');if(!t)return;const a={idle:e.currentVariant.available?"":"This variant is sold out.",loading:"Adding item to cart...",success:"Added to cart.",error:"Could not add to cart. Please try again."};t.textContent=a[e.cartState]}function y(){document.querySelectorAll('[data-js="option-value"]').forEach(t=>{const a=Number(t.dataset.optionPosition)-1,n=t.dataset.optionValue??"",r=e.selectedOptions[a]===n;t.setAttribute("aria-pressed",String(r));const s=c(e.productData.variants,e.selectedOptions,a).has(n);t.setAttribute("aria-disabled",String(!s)),t.disabled=!s}),document.querySelectorAll('[data-js="option-label"]').forEach(t=>{const a=Number(t.dataset.optionLabel)-1;t.textContent=e.selectedOptions[a]??""})}export{g as s}; diff --git a/assets/theme-B5Qt9EMX.js b/assets/theme-B5Qt9EMX.js deleted file mode 100644 index 3e0a78a5c..000000000 --- a/assets/theme-B5Qt9EMX.js +++ /dev/null @@ -1 +0,0 @@ -(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const e of document.querySelectorAll('link[rel="modulepreload"]'))i(e);new MutationObserver(e=>{for(const r of e)if(r.type==="childList")for(const o of r.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&i(o)}).observe(document,{childList:!0,subtree:!0});function s(e){const r={};return e.integrity&&(r.integrity=e.integrity),e.referrerPolicy&&(r.referrerPolicy=e.referrerPolicy),e.crossOrigin==="use-credentials"?r.credentials="include":e.crossOrigin==="anonymous"?r.credentials="omit":r.credentials="same-origin",r}function i(e){if(e.ep)return;e.ep=!0;const r=s(e);fetch(e.href,r)}})(); diff --git a/assets/theme-BHxQweo7.js b/assets/theme-BHxQweo7.js new file mode 100644 index 000000000..c70124fe9 --- /dev/null +++ b/assets/theme-BHxQweo7.js @@ -0,0 +1,2 @@ +const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["./drawer-HPI4d66D.js","./cart-CmG_L9o1.js"])))=>i.map(i=>d[i]); +const g="modulepreload",v=function(a,o){return new URL(a,o).href},p={},E=function(o,s,u){let e=Promise.resolve();if(s&&s.length>0){let y=function(n){return Promise.all(n.map(l=>Promise.resolve(l).then(d=>({status:"fulfilled",value:d}),d=>({status:"rejected",reason:d}))))};const r=document.getElementsByTagName("link"),c=document.querySelector("meta[property=csp-nonce]"),h=c?.nonce||c?.getAttribute("nonce");e=y(s.map(n=>{if(n=v(n,u),n in p)return;p[n]=!0;const l=n.endsWith(".css"),d=l?'[rel="stylesheet"]':"";if(u)for(let f=r.length-1;f>=0;f--){const m=r[f];if(m.href===n&&(!l||m.rel==="stylesheet"))return}else if(document.querySelector(`link[href="${n}"]${d}`))return;const i=document.createElement("link");if(i.rel=l?"stylesheet":g,l||(i.as="script"),i.crossOrigin="",i.href=n,h&&i.setAttribute("nonce",h),document.head.appendChild(i),l)return new Promise((f,m)=>{i.addEventListener("load",f),i.addEventListener("error",()=>m(new Error(`Unable to preload CSS for ${n}`)))})}))}function t(r){const c=new Event("vite:preloadError",{cancelable:!0});if(c.payload=r,window.dispatchEvent(c),!c.defaultPrevented)throw r}return e.then(r=>{for(const c of r||[])c.status==="rejected"&&t(c.reason);return o().catch(t)})};(function(){const o=document.createElement("link").relList;if(o&&o.supports&&o.supports("modulepreload"))return;for(const e of document.querySelectorAll('link[rel="modulepreload"]'))u(e);new MutationObserver(e=>{for(const t of e)if(t.type==="childList")for(const r of t.addedNodes)r.tagName==="LINK"&&r.rel==="modulepreload"&&u(r)}).observe(document,{childList:!0,subtree:!0});function s(e){const t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),e.crossOrigin==="use-credentials"?t.credentials="include":e.crossOrigin==="anonymous"?t.credentials="omit":t.credentials="same-origin",t}function u(e){if(e.ep)return;e.ep=!0;const t=s(e);fetch(e.href,t)}})();document.addEventListener("DOMContentLoaded",()=>{const a=document.querySelector('[data-js="cart-drawer"]'),o=document.querySelector('[data-js="cart-open"]');!a&&!o||E(async()=>{const{initCartDrawer:s}=await import("./drawer-HPI4d66D.js");return{initCartDrawer:s}},__vite__mapDeps([0,1]),import.meta.url).then(({initCartDrawer:s})=>{s()})}); diff --git a/assets/variant-picker-DRrQ71gT.js b/assets/variant-picker-DRrQ71gT.js new file mode 100644 index 000000000..f4573220c --- /dev/null +++ b/assets/variant-picker-DRrQ71gT.js @@ -0,0 +1 @@ +function o(t,a){return t.find(n=>n.options.every((e,i)=>e===a[i]))}function f(t,a,n){return new Set(t.filter(e=>e.options.every((i,r)=>r===n||i===a[r])).filter(e=>e.available).map(e=>e.options[n]))}export{o as f,f as g}; diff --git a/frontend/entrypoints/css/main.css b/frontend/entrypoints/css/main.css index 8254e8772..f298ffa02 100644 --- a/frontend/entrypoints/css/main.css +++ b/frontend/entrypoints/css/main.css @@ -1 +1,10 @@ -@import "tailwindcss" source("../.."); +@import "tailwindcss"; + +/* Register sources */ +@source "../../../snippets" +@source "../../../blocks"; +@source "../../../section"; +@source "../../../templates"; + +@theme { +} diff --git a/frontend/entrypoints/ts/cart.ts b/frontend/entrypoints/ts/cart.ts index e69de29bb..d1765a0e1 100644 --- a/frontend/entrypoints/ts/cart.ts +++ b/frontend/entrypoints/ts/cart.ts @@ -0,0 +1,5 @@ +import { initCartPage } from './cart/page'; + +document.addEventListener('DOMContentLoaded', () => { + initCartPage(); +}); diff --git a/frontend/entrypoints/ts/cart/drawer.ts b/frontend/entrypoints/ts/cart/drawer.ts new file mode 100644 index 000000000..1f060cda8 --- /dev/null +++ b/frontend/entrypoints/ts/cart/drawer.ts @@ -0,0 +1,285 @@ +import { changeCartLine, getCart, type CartItem, type CartResponse } from '../utils/cart'; + +const FOCUSABLE_SELECTOR = + 'a[href], button:not([disabled]), textarea, input, select, [tabindex]:not([tabindex="-1"])'; + +function escapeHtml(value: string): string { + return value + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll("'", '''); +} + +function formatMoney(cents: number, currency: string): string { + try { + return new Intl.NumberFormat(undefined, { + style: 'currency', + currency, + }).format(cents / 100); + } catch { + return `${(cents / 100).toFixed(2)} ${currency}`; + } +} + +function getImageUrl(item: CartItem): string | null { + return item.image ?? item.featured_image?.url ?? null; +} + +function getItemTitle(item: CartItem): string { + return item.product_title ?? item.title ?? 'Product'; +} + +function lineTotal(item: CartItem): number { + return item.final_line_price ?? item.line_price ?? 0; +} + +export function initCartDrawer(): void { + const drawerRoot = document.querySelector('[data-js="cart-drawer"]'); + if (!drawerRoot) return; + + const overlayRoot = drawerRoot.querySelector('[data-js="cart-drawer-overlay"]'); + const panelRoot = drawerRoot.querySelector('[data-js="cart-drawer-panel"]'); + const closeButtonRoot = drawerRoot.querySelector('[data-js="cart-close"]'); + const itemsContainerRoot = drawerRoot.querySelector('[data-js="cart-items"]'); + const emptyStateRoot = drawerRoot.querySelector('[data-js="cart-empty"]'); + const subtotalRoot = drawerRoot.querySelector('[data-js="cart-subtotal"]'); + const statusRoot = drawerRoot.querySelector('[data-js="cart-drawer-status"]'); + const errorRoot = drawerRoot.querySelector('[data-js="cart-drawer-error"]'); + const countNodes = document.querySelectorAll('[data-js="cart-count"]'); + + if ( + !overlayRoot || + !panelRoot || + !closeButtonRoot || + !itemsContainerRoot || + !emptyStateRoot || + !subtotalRoot || + !statusRoot || + !errorRoot + ) { + return; + } + + const drawer = drawerRoot; + const overlay = overlayRoot; + const panel = panelRoot; + const closeButton = closeButtonRoot; + const itemsContainer = itemsContainerRoot; + const emptyState = emptyStateRoot; + const subtotal = subtotalRoot; + const status = statusRoot; + const error = errorRoot; + + const fallbackUrl = drawer.dataset.cartUrl ?? '/cart'; + + let isOpen = false; + let isUpdating = false; + let lastFocused: HTMLElement | null = null; + + const setStatus = (message: string): void => { + status.textContent = message; + }; + + const setError = (message: string): void => { + error.textContent = message; + }; + + const setBusyState = (busy: boolean): void => { + panel.setAttribute('aria-busy', String(busy)); + drawer.querySelectorAll('button').forEach((button) => { + const isCloseButton = button.dataset.js === 'cart-close'; + if (!isCloseButton) { + button.disabled = busy; + } + }); + }; + + const renderItems = (cart: CartResponse): void => { + const currency = cart.currency || drawer.dataset.currency || 'USD'; + const isEmpty = cart.item_count === 0; + + countNodes.forEach((node) => { + node.textContent = String(cart.item_count); + node.hidden = cart.item_count < 1; + }); + + subtotal.textContent = formatMoney(cart.total_price, currency); + + if (isEmpty) { + itemsContainer.innerHTML = ''; + emptyState.hidden = false; + setStatus('Your cart is empty.'); + return; + } + + emptyState.hidden = true; + itemsContainer.innerHTML = cart.items + .map((item, index) => { + const imageUrl = getImageUrl(item); + const itemTitle = escapeHtml(getItemTitle(item)); + const variantTitle = item.variant_title ? escapeHtml(item.variant_title) : ''; + + return ` +
  • + + ${ + imageUrl + ? `${itemTitle}` + : '' + } + +
    + ${itemTitle} + ${variantTitle ? `

    ${variantTitle}

    ` : ''} +
    + + ${item.quantity} + + +
    +

    ${formatMoney(lineTotal(item), currency)}

    +
    +
  • + `; + }) + .join(''); + }; + + const refresh = async (): Promise => { + setError(''); + + try { + const cart = await getCart(); + renderItems(cart); + } catch { + setError('Could not load your cart. Redirecting to cart page...'); + window.location.assign(fallbackUrl); + } + }; + + const updateLine = async (line: number, quantity: number): Promise => { + if (isUpdating) return; + isUpdating = true; + setError(''); + setStatus('Updating cart...'); + setBusyState(true); + + try { + const cart = await changeCartLine(line, quantity); + renderItems(cart); + setStatus(quantity === 0 ? 'Item removed.' : 'Cart updated.'); + } catch { + setError('Could not update cart. Please try again.'); + setStatus('Cart update failed.'); + } finally { + isUpdating = false; + setBusyState(false); + } + }; + + const focusables = (): HTMLElement[] => + Array.from(panel.querySelectorAll(FOCUSABLE_SELECTOR)).filter( + (el) => !el.hasAttribute('disabled') && !el.getAttribute('aria-hidden'), + ); + + const onKeyDown = (event: KeyboardEvent): void => { + if (!isOpen) return; + + if (event.key === 'Escape') { + event.preventDefault(); + closeDrawer(); + return; + } + + if (event.key !== 'Tab') return; + + const nodes = focusables(); + if (nodes.length === 0) return; + + const first = nodes[0]; + const last = nodes[nodes.length - 1]; + + if (event.shiftKey && document.activeElement === first) { + event.preventDefault(); + last.focus(); + return; + } + + if (!event.shiftKey && document.activeElement === last) { + event.preventDefault(); + first.focus(); + } + }; + + function openDrawer(trigger?: HTMLElement): void { + if (isOpen) return; + isOpen = true; + lastFocused = trigger ?? (document.activeElement instanceof HTMLElement ? document.activeElement : null); + + drawer.hidden = false; + drawer.setAttribute('aria-hidden', 'false'); + document.body.style.overflow = 'hidden'; + document.addEventListener('keydown', onKeyDown); + + closeButton.focus(); + void refresh(); + } + + function closeDrawer(): void { + if (!isOpen) return; + isOpen = false; + + drawer.hidden = true; + drawer.setAttribute('aria-hidden', 'true'); + document.body.style.overflow = ''; + document.removeEventListener('keydown', onKeyDown); + + if (lastFocused) { + lastFocused.focus(); + } + } + + document.querySelectorAll('[data-js="cart-open"]').forEach((trigger) => { + trigger.addEventListener('click', (event) => { + event.preventDefault(); + openDrawer(trigger); + }); + }); + + closeButton.addEventListener('click', closeDrawer); + overlay.addEventListener('click', closeDrawer); + + drawer.addEventListener('click', (event) => { + const target = event.target as HTMLElement; + const actionButton = target.closest( + '[data-js="cart-qty-dec"], [data-js="cart-qty-inc"], [data-js="cart-remove"]', + ); + + if (!actionButton) return; + + const line = Number(actionButton.dataset.line); + if (!line) return; + + const row = itemsContainer.querySelector(`[data-line="${line}"]`); + const quantityNode = row?.querySelector('[data-js="cart-qty-value"]'); + const currentQty = Number(quantityNode?.textContent ?? '1'); + + if (actionButton.dataset.js === 'cart-remove') { + void updateLine(line, 0); + return; + } + + if (actionButton.dataset.js === 'cart-qty-inc') { + void updateLine(line, currentQty + 1); + return; + } + + if (actionButton.dataset.js === 'cart-qty-dec') { + void updateLine(line, Math.max(0, currentQty - 1)); + } + }); + + void refresh(); +} diff --git a/frontend/entrypoints/ts/cart/page.ts b/frontend/entrypoints/ts/cart/page.ts new file mode 100644 index 000000000..62a48a41a --- /dev/null +++ b/frontend/entrypoints/ts/cart/page.ts @@ -0,0 +1,179 @@ +import { changeCartLine, getCart, type CartItem, type CartResponse } from '../utils/cart'; + +function escapeHtml(value: string): string { + return value + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll("'", '''); +} + +function formatMoney(cents: number, currency: string): string { + try { + return new Intl.NumberFormat(undefined, { + style: 'currency', + currency, + }).format(cents / 100); + } catch { + return `${(cents / 100).toFixed(2)} ${currency}`; + } +} + +function getImageUrl(item: CartItem): string | null { + return item.image ?? item.featured_image?.url ?? null; +} + +function getItemTitle(item: CartItem): string { + return item.product_title ?? item.title ?? 'Product'; +} + +function lineTotal(item: CartItem): number { + return item.final_line_price ?? item.line_price ?? 0; +} + +export function initCartPage(): void { + const root = document.querySelector('[data-js="cart-page"]'); + if (!root) return; + + const items = root.querySelector('[data-js="cart-page-items"]'); + const empty = root.querySelector('[data-js="cart-page-empty"]'); + const footer = root.querySelector('[data-js="cart-page-footer"]'); + const subtotal = root.querySelector('[data-js="cart-page-subtotal"]'); + const status = root.querySelector('[data-js="cart-page-status"]'); + const error = root.querySelector('[data-js="cart-page-error"]'); + const countNodes = document.querySelectorAll('[data-js="cart-count"]'); + + if (!items || !empty || !footer || !subtotal || !status || !error) return; + + const fallbackUrl = '/cart'; + let isUpdating = false; + + const setStatus = (message: string): void => { + status.textContent = message; + }; + + const setError = (message: string): void => { + error.textContent = message; + }; + + const setBusy = (busy: boolean): void => { + root.setAttribute('aria-busy', String(busy)); + root.querySelectorAll('button').forEach((button) => { + if (button.type !== 'submit') { + button.disabled = busy; + } + }); + }; + + const render = (cart: CartResponse): void => { + const currency = cart.currency || root.dataset.currency || 'USD'; + const hasItems = cart.item_count > 0; + + countNodes.forEach((node) => { + node.textContent = String(cart.item_count); + node.hidden = cart.item_count < 1; + }); + + empty.hidden = hasItems; + items.hidden = !hasItems; + footer.hidden = !hasItems; + subtotal.textContent = formatMoney(cart.total_price, currency); + + if (!hasItems) { + items.innerHTML = ''; + setStatus('Your cart is empty.'); + return; + } + + items.innerHTML = cart.items + .map((item, index) => { + const imageUrl = getImageUrl(item); + const title = escapeHtml(getItemTitle(item)); + const variantTitle = item.variant_title ? escapeHtml(item.variant_title) : ''; + + return ` +
  • + + ${imageUrl ? `${title}` : ''} + +
    + ${title} + ${variantTitle ? `

    ${variantTitle}

    ` : ''} +
    + + ${item.quantity} + + +
    +

    ${formatMoney(lineTotal(item), currency)}

    +
    +
  • + `; + }) + .join(''); + }; + + const refresh = async (): Promise => { + try { + const cart = await getCart(); + render(cart); + } catch { + setError('Could not load cart data. Reloading...'); + window.location.assign(fallbackUrl); + } + }; + + const updateLine = async (line: number, quantity: number): Promise => { + if (isUpdating) return; + + isUpdating = true; + setBusy(true); + setError(''); + setStatus('Updating cart...'); + + try { + const cart = await changeCartLine(line, quantity); + render(cart); + setStatus(quantity === 0 ? 'Item removed.' : 'Cart updated.'); + } catch { + setError('Could not update cart. Please try again.'); + setStatus('Cart update failed.'); + } finally { + isUpdating = false; + setBusy(false); + } + }; + + root.addEventListener('click', (event) => { + const target = event.target as HTMLElement; + const actionButton = target.closest( + '[data-js="cart-page-dec"], [data-js="cart-page-inc"], [data-js="cart-page-remove"]', + ); + + if (!actionButton) return; + + const line = Number(actionButton.dataset.line); + if (!line) return; + + const row = items.querySelector(`[data-line="${line}"]`); + const quantityNode = row?.querySelector('[data-js="cart-page-qty"]'); + const currentQty = Number(quantityNode?.textContent ?? '1'); + + if (actionButton.dataset.js === 'cart-page-remove') { + void updateLine(line, 0); + return; + } + + if (actionButton.dataset.js === 'cart-page-inc') { + void updateLine(line, currentQty + 1); + return; + } + + if (actionButton.dataset.js === 'cart-page-dec') { + void updateLine(line, Math.max(0, currentQty - 1)); + } + }); + + void refresh(); +} diff --git a/frontend/entrypoints/ts/collection.ts b/frontend/entrypoints/ts/collection.ts index e69de29bb..509725a68 100644 --- a/frontend/entrypoints/ts/collection.ts +++ b/frontend/entrypoints/ts/collection.ts @@ -0,0 +1,83 @@ +function setLoadStatus(message: string): void { + const status = document.querySelector('[data-js="collection-load-status"]'); + if (!status) return; + status.textContent = message; +} + +function resolveNextButton(): HTMLButtonElement | null { + return document.querySelector('[data-js="collection-load-more"]'); +} + +async function onLoadMoreClick(button: HTMLButtonElement): Promise { + const nextUrl = button.dataset.nextUrl; + if (!nextUrl || button.disabled) return; + + button.disabled = true; + setLoadStatus('Loading more products...'); + + try { + const response = await fetch(nextUrl, { + headers: { + 'X-Requested-With': 'XMLHttpRequest', + }, + }); + + if (!response.ok) { + throw new Error('Failed to load next collection page'); + } + + const html = await response.text(); + const parsed = new DOMParser().parseFromString(html, 'text/html'); + const incomingProducts = parsed.querySelector('[data-js="collection-products"]'); + const currentProducts = document.querySelector('[data-js="collection-products"]'); + + if (!incomingProducts || !currentProducts) { + throw new Error('Could not parse collection products'); + } + + incomingProducts.querySelectorAll('[data-js="collection-product-card"]').forEach((card) => { + currentProducts.appendChild(card); + }); + + const incomingButton = parsed.querySelector('[data-js="collection-load-more"]'); + if (incomingButton?.dataset.nextUrl) { + button.dataset.nextUrl = incomingButton.dataset.nextUrl; + button.disabled = false; + setLoadStatus('More products loaded.'); + return; + } + + button.remove(); + setLoadStatus('All products loaded.'); + } catch { + button.disabled = false; + setLoadStatus('Could not load more products. Please try again.'); + } +} + +function bindSortControl(): void { + const controls = document.querySelector('[data-js="collection-controls"]'); + const sort = document.querySelector('[data-js="collection-sort"]'); + if (!controls || !sort) return; + + sort.addEventListener('change', () => { + controls.requestSubmit(); + }); +} + +function bindLoadMoreControl(): void { + const button = resolveNextButton(); + if (!button) return; + + button.addEventListener('click', () => { + void onLoadMoreClick(button); + }); +} + +document.addEventListener('DOMContentLoaded', () => { + const root = document.querySelector('[data-js="collection-root"]'); + if (!root) return; + + bindSortControl(); + bindLoadMoreControl(); +}); diff --git a/frontend/entrypoints/ts/product.ts b/frontend/entrypoints/ts/product.ts index e69de29bb..6fef39fa8 100644 --- a/frontend/entrypoints/ts/product.ts +++ b/frontend/entrypoints/ts/product.ts @@ -0,0 +1,56 @@ +import { state } from './product/state'; +import { syncDOM } from './product/sync'; +import { onOptionClick, onAddToCart, onThumbnailClick } from './product/handlers'; +import { loadRecommendations } from './product/recommendations'; +import type { ProductData } from './utils/variant-picker'; + +function resolveVariantFromURL(productData: ProductData): ProductData['variants'][number] { + const params = new URLSearchParams(window.location.search); + const variantIdParam = Number(params.get('variant')); + + return ( + productData.variants.find((v) => v.id === variantIdParam) ?? + productData.variants.find((v) => v.available) ?? + productData.variants[0] + ); +} + +function applyVariantSelection(variant: ProductData['variants'][number]): void { + state.currentVariant = variant; + state.selectedOptions = [...variant.options]; + state.currentMediaId = variant.featured_media?.id ?? null; +} + +/** + * Bootstraps the product page: parses JSON islands, resolves the initial variant + * from URL or availability, and attaches event listeners. + */ +function init(): void { + const dataEl = document.querySelector('[data-js="product-data"]'); + if (!dataEl?.textContent) return; + + state.productData = JSON.parse(dataEl.textContent) as ProductData; + + const pricesEl = document.querySelector('[data-js="variant-prices"]'); + state.variantPrices = pricesEl?.textContent + ? (JSON.parse(pricesEl.textContent) as Record) + : {}; + + applyVariantSelection(resolveVariantFromURL(state.productData)); + + const form = document.querySelector('[data-js="product-form"]'); + form?.addEventListener('click', onOptionClick); + form?.addEventListener('submit', (e) => void onAddToCart(e)); + + document.addEventListener('click', onThumbnailClick); + window.addEventListener('popstate', () => { + applyVariantSelection(resolveVariantFromURL(state.productData)); + state.cartState = 'idle'; + syncDOM(); + }); + + syncDOM(); + loadRecommendations(); +} + +document.addEventListener('DOMContentLoaded', init); diff --git a/frontend/entrypoints/ts/product/handlers.ts b/frontend/entrypoints/ts/product/handlers.ts new file mode 100644 index 000000000..76d03e498 --- /dev/null +++ b/frontend/entrypoints/ts/product/handlers.ts @@ -0,0 +1,81 @@ +import { findVariantByOptions } from '../utils/variant-picker'; +import { addToCart } from '../utils/cart'; +import { state } from './state'; +import { syncDOM } from './sync'; + +/** + * Handles delegated click events on option buttons. + * Updates `selectedOptions`, resolves the matching variant, and syncs the DOM. + */ +export function onOptionClick(e: Event): void { + const btn = (e.target as HTMLElement).closest('[data-js="option-value"]'); + if (!btn) return; + if (btn.disabled || btn.getAttribute('aria-disabled') === 'true') return; + + const position = Number(btn.dataset.optionPosition) - 1; + const value = btn.dataset.optionValue ?? ''; + if (position < 0 || !value) return; + + state.selectedOptions[position] = value; + + const variant = findVariantByOptions(state.productData.variants, state.selectedOptions); + if (!variant) { + state.selectedOptions[position] = state.currentVariant.options[position] ?? state.selectedOptions[position]; + return; + } + + state.currentVariant = variant; + state.currentMediaId = variant.featured_media?.id ?? null; + + const url = new URL(window.location.href); + url.searchParams.set('variant', String(variant.id)); + history.replaceState({}, '', `${url.pathname}${url.search}${url.hash}`); + syncDOM(); +} + +/** + * Handles delegated click events on thumbnail buttons. + * Updates `currentMediaId` and syncs the DOM. + */ +export function onThumbnailClick(e: Event): void { + const btn = (e.target as HTMLElement).closest('[data-js="thumbnail"]'); + if (!btn) return; + const mediaId = Number(btn.dataset.thumbnail); + if (!mediaId) return; + state.currentMediaId = mediaId; + syncDOM(); +} + +/** + * Handles form submit: delegates the cart request to `addToCart`, + * manages `cartState`, and syncs the button label. + */ +export async function onAddToCart(e: Event): Promise { + e.preventDefault(); + if (state.cartState === 'loading') return; + + const form = e.target as HTMLFormElement; + const quantityInput = form.querySelector('input[name="quantity"]'); + const quantity = quantityInput ? Math.max(1, Number(quantityInput.value) || 1) : 1; + + state.cartState = 'loading'; + syncDOM(); + + try { + await addToCart(state.currentVariant.id, quantity); + + state.cartState = 'success'; + syncDOM(); + setTimeout(() => { + state.cartState = 'idle'; + syncDOM(); + }, 2000); + } catch { + state.cartState = 'error'; + syncDOM(); + setTimeout(() => { + state.cartState = 'idle'; + syncDOM(); + }, 3000); + } +} diff --git a/frontend/entrypoints/ts/product/recommendations.ts b/frontend/entrypoints/ts/product/recommendations.ts new file mode 100644 index 000000000..380def1e9 --- /dev/null +++ b/frontend/entrypoints/ts/product/recommendations.ts @@ -0,0 +1,28 @@ +/** + * Load product recommendations via the Section Rendering API. + * Fetches the product-recommendations section with intent=related + * and injects the HTML into the [data-js="recommendations"] container. + */ +export function loadRecommendations(): void { + const container = document.querySelector('[data-js="recommendations"]'); + if (!container) return; + + const productId = container.dataset.productId; + const sectionId = container.dataset.sectionId; + if (!productId || !sectionId) return; + + const url = `${window.Shopify.routes.root}recommendations/products?section_id=${sectionId}&product_id=${productId}&intent=related`; + + fetch(url) + .then((res) => res.text()) + .then((html) => { + const parsed = new DOMParser().parseFromString(html, 'text/html'); + const inner = parsed.querySelector('[data-js="product-recommendations-root"]'); + if (inner) { + container.innerHTML = inner.innerHTML; + } + }) + .catch(() => { + // Silently fail — recommendations are non-critical + }); +} diff --git a/frontend/entrypoints/ts/product/state.ts b/frontend/entrypoints/ts/product/state.ts new file mode 100644 index 000000000..cee31b153 --- /dev/null +++ b/frontend/entrypoints/ts/product/state.ts @@ -0,0 +1,12 @@ +import type { ProductVariant, ProductData } from '../utils/variant-picker'; + +export type CartState = 'idle' | 'loading' | 'success' | 'error'; + +export const state = { + productData: null as unknown as ProductData, + currentVariant: null as unknown as ProductVariant, + selectedOptions: [] as string[], + variantPrices: {} as Record, + cartState: 'idle' as CartState, + currentMediaId: null as number | null, +}; diff --git a/frontend/entrypoints/ts/product/sync.ts b/frontend/entrypoints/ts/product/sync.ts new file mode 100644 index 000000000..a1dfb1574 --- /dev/null +++ b/frontend/entrypoints/ts/product/sync.ts @@ -0,0 +1,136 @@ +import { getAvailableValues } from '../utils/variant-picker'; +import { state } from './state'; + +/** Calls all sync helpers in sequence after any state change. */ +export function syncDOM(): void { + syncPrice(); + syncAvailability(); + syncMedia(); + syncVariantInput(); + syncButton(); + syncCartStatus(); + syncOptionButtons(); +} + +/** Updates the price element from the pre-formatted `variantPrices` map. */ +function syncPrice(): void { + const priceEl = document.querySelector('[data-js="product-price"]'); + if (!priceEl) return; + const formatted = state.variantPrices[String(state.currentVariant.id)]; + if (formatted) priceEl.textContent = formatted; +} + +/** Shows or hides the "Sold out" availability message. */ +function syncAvailability(): void { + const el = document.querySelector('[data-js="product-availability"]'); + if (!el) return; + el.textContent = state.currentVariant.available ? '' : 'Sold out'; +} + +/** Shows the media item matching `currentMediaId` (or variant featured media), hides all others. */ +function syncMedia(): void { + const items = document.querySelectorAll('[data-js="media-item"]'); + const currentVariantId = String(state.currentVariant.id); + + const targetId = + state.currentMediaId !== null + ? String(state.currentMediaId) + : state.currentVariant.featured_media !== null + ? String(state.currentVariant.featured_media.id) + : null; + + items.forEach((item) => { + const ownerVariantId = item.dataset.variantMedia; + const isShared = !ownerVariantId; + const isCurrentVariantMedia = ownerVariantId === currentVariantId; + + let isVisible: boolean; + if (targetId !== null) { + isVisible = item.dataset.mediaId === targetId; + } else { + // No targeted media: show shared + current variant media + // If no variant images exist at all (all isShared), this shows everything — unchanged behaviour + isVisible = isShared || isCurrentVariantMedia; + } + + if (isVisible) { + item.removeAttribute('hidden'); + } else { + item.setAttribute('hidden', ''); + item.querySelector('video')?.pause(); + } + }); + + // Thumbnail visibility: shared always visible; variant thumbnails only when active + document.querySelectorAll('[data-js="thumbnail"]').forEach((btn) => { + const ownerVariantId = btn.dataset.variantMedia; + const isShared = !ownerVariantId; + const isCurrentVariantMedia = ownerVariantId === currentVariantId; + + if (isShared || isCurrentVariantMedia) { + btn.removeAttribute('hidden'); + } else { + btn.setAttribute('hidden', ''); + } + + btn.setAttribute('aria-pressed', String(btn.dataset.thumbnail === targetId)); + }); +} + +/** Updates the add-to-cart button label and disabled state based on availability and cart state. */ +function syncButton(): void { + const btn = document.querySelector('[data-js="add-to-cart"]'); + if (!btn) return; + + const labels = { + idle: state.currentVariant.available ? 'Add to cart' : 'Sold out', + loading: 'Adding...', + success: 'Added!', + error: 'Try again', + }; + + btn.disabled = !state.currentVariant.available || state.cartState === 'loading'; + btn.setAttribute('aria-busy', String(state.cartState === 'loading')); + btn.textContent = labels[state.cartState]; +} + +function syncVariantInput(): void { + const input = document.querySelector('[data-js="variant-id"]'); + if (!input) return; + input.value = String(state.currentVariant.id); +} + +function syncCartStatus(): void { + const status = document.querySelector('[data-js="cart-status"]'); + if (!status) return; + + const messages = { + idle: state.currentVariant.available ? '' : 'This variant is sold out.', + loading: 'Adding item to cart...', + success: 'Added to cart.', + error: 'Could not add to cart. Please try again.', + }; + + status.textContent = messages[state.cartState]; +} + +/** Updates `aria-pressed` and `aria-disabled` on all option buttons, and refreshes the selected-value label text. */ +function syncOptionButtons(): void { + document.querySelectorAll('[data-js="option-value"]').forEach((btn) => { + const position = Number(btn.dataset.optionPosition) - 1; + const value = btn.dataset.optionValue ?? ''; + const isSelected = state.selectedOptions[position] === value; + + btn.setAttribute('aria-pressed', String(isSelected)); + const available = getAvailableValues(state.productData.variants, state.selectedOptions, position); + const isAvailable = available.has(value); + + btn.setAttribute('aria-disabled', String(!isAvailable)); + btn.disabled = !isAvailable; + }); + + document.querySelectorAll('[data-js="option-label"]').forEach((label) => { + const position = Number(label.dataset.optionLabel) - 1; + label.textContent = state.selectedOptions[position] ?? ''; + }); +} diff --git a/frontend/entrypoints/ts/search.ts b/frontend/entrypoints/ts/search.ts index e69de29bb..d0303b1a3 100644 --- a/frontend/entrypoints/ts/search.ts +++ b/frontend/entrypoints/ts/search.ts @@ -0,0 +1,175 @@ +declare global { + interface Window { + Shopify: { routes: { root: string } }; + } +} + +export {}; + +interface PredictiveItem { + title: string; + url: string; + price?: string; + image?: { url: string; alt?: string | null } | null; +} + +interface PredictivePayload { + resources?: { + results?: { + products?: PredictiveItem[]; + articles?: PredictiveItem[]; + pages?: PredictiveItem[]; + }; + }; +} + +function escapeHtml(value: string): string { + return value + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll("'", '''); +} + +function debounce void>(fn: T, delay = 250): (...args: Parameters) => void { + let timeoutId: number | null = null; + + return (...args: Parameters) => { + if (timeoutId !== null) { + window.clearTimeout(timeoutId); + } + + timeoutId = window.setTimeout(() => { + fn(...args); + }, delay); + }; +} + +document.addEventListener('DOMContentLoaded', () => { + const root = document.querySelector('[data-js="search-root"]'); + if (!root) return; + + const input = root.querySelector('[data-js="search-input"]'); + const panel = root.querySelector('[data-js="predictive-results"]'); + const list = root.querySelector('[data-js="predictive-list"]'); + const predictiveStatus = root.querySelector('[data-js="predictive-status"]'); + const searchStatus = root.querySelector('[data-js="search-status"]'); + if (!input || !panel || !list || !predictiveStatus || !searchStatus) return; + + let currentController: AbortController | null = null; + + const setSearchStatus = (message: string): void => { + searchStatus.textContent = message; + }; + + const setPredictiveStatus = (message: string): void => { + predictiveStatus.textContent = message; + }; + + const closePanel = (): void => { + panel.hidden = true; + input.setAttribute('aria-expanded', 'false'); + }; + + const openPanel = (): void => { + panel.hidden = false; + input.setAttribute('aria-expanded', 'true'); + }; + + const renderResults = (results: PredictiveItem[]): void => { + if (results.length === 0) { + list.innerHTML = ''; + setPredictiveStatus('No quick matches. Press Enter for full results.'); + openPanel(); + return; + } + + list.innerHTML = results + .map((item) => { + const title = escapeHtml(item.title); + const image = item.image?.url + ? `${escapeHtml(item.image.alt ?? item.title)}` + : ''; + + return ` +
  • + + ${image} + ${title} + +
  • + `; + }) + .join(''); + + setPredictiveStatus(`${results.length} quick matches`); + openPanel(); + }; + + const fetchPredictiveResults = async (term: string): Promise => { + if (term.length < 2) { + closePanel(); + setPredictiveStatus(''); + return; + } + + currentController?.abort(); + currentController = new AbortController(); + + setPredictiveStatus('Searching...'); + setSearchStatus('Predictive search active'); + openPanel(); + + const url = `${window.Shopify.routes.root}search/suggest.json?q=${encodeURIComponent(term)}&resources[type]=product,page,article&resources[limit]=6&resources[options][unavailable_products]=last`; + + try { + const response = await fetch(url, { + signal: currentController.signal, + headers: { + Accept: 'application/json', + 'X-Requested-With': 'XMLHttpRequest', + }, + }); + + if (!response.ok) { + throw new Error('Predictive endpoint unavailable'); + } + + const payload = (await response.json()) as PredictivePayload; + const products = payload.resources?.results?.products ?? []; + const pages = payload.resources?.results?.pages ?? []; + const articles = payload.resources?.results?.articles ?? []; + + renderResults([...products, ...pages, ...articles].slice(0, 6)); + } catch (error) { + if (error instanceof DOMException && error.name === 'AbortError') return; + + closePanel(); + setPredictiveStatus(''); + setSearchStatus('Predictive search unavailable. Full search still works.'); + } + }; + + const onInput = debounce((value: string) => { + void fetchPredictiveResults(value.trim()); + }); + + input.addEventListener('input', () => { + onInput(input.value); + }); + + input.addEventListener('focus', () => { + if (list.children.length > 0) { + openPanel(); + } + }); + + root.addEventListener('focusout', () => { + window.setTimeout(() => { + const focused = document.activeElement; + if (focused && root.contains(focused)) return; + closePanel(); + }, 100); + }); +}); diff --git a/frontend/entrypoints/ts/theme.ts b/frontend/entrypoints/ts/theme.ts index 2458eb9b7..9ff551d8a 100644 --- a/frontend/entrypoints/ts/theme.ts +++ b/frontend/entrypoints/ts/theme.ts @@ -1 +1,11 @@ import 'vite/modulepreload-polyfill'; + +document.addEventListener('DOMContentLoaded', () => { + const hasCartDrawer = document.querySelector('[data-js="cart-drawer"]'); + const hasCartTrigger = document.querySelector('[data-js="cart-open"]'); + if (!hasCartDrawer && !hasCartTrigger) return; + + void import('./cart/drawer').then(({ initCartDrawer }) => { + initCartDrawer(); + }); +}); diff --git a/frontend/entrypoints/ts/utils/cart.ts b/frontend/entrypoints/ts/utils/cart.ts new file mode 100644 index 000000000..3aaf11ccd --- /dev/null +++ b/frontend/entrypoints/ts/utils/cart.ts @@ -0,0 +1,90 @@ +declare global { + interface Window { + Shopify: { routes: { root: string } }; + } +} + +/** Structured error returned by the Shopify Cart API. */ +export interface CartError { + description: string; +} + +export interface CartItem { + key: string; + quantity: number; + url: string; + image?: string | null; + featured_image?: { url: string } | null; + product_title?: string; + title?: string; + variant_title?: string | null; + final_line_price?: number; + line_price?: number; +} + +export interface CartResponse { + item_count: number; + total_price: number; + currency: string; + items: CartItem[]; +} + +async function parseCartError(res: Response): Promise { + const err = (await res.json().catch(() => ({}))) as Partial; + return new Error(err.description ?? 'Cart request failed'); +} + +export async function getCart(): Promise { + const res = await fetch(`${window.Shopify.routes.root}cart.js`, { + headers: { + Accept: 'application/json', + 'X-Requested-With': 'XMLHttpRequest', + }, + }); + + if (!res.ok) { + throw await parseCartError(res); + } + + return (await res.json()) as CartResponse; +} + +export async function changeCartLine(line: number, quantity: number): Promise { + const res = await fetch(`${window.Shopify.routes.root}cart/change.js`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + 'X-Requested-With': 'XMLHttpRequest', + }, + body: JSON.stringify({ line, quantity }), + }); + + if (!res.ok) { + throw await parseCartError(res); + } + + return (await res.json()) as CartResponse; +} + +/** + * Adds a variant to the cart via the Shopify AJAX Cart API. + * + * @param variantId - The variant ID to add. + * @param quantity - The quantity to add (must be ≥ 1). + * @throws {Error} If the request fails or the API returns an error response. + */ +export async function addToCart(variantId: number, quantity: number): Promise { + const res = await fetch(`${window.Shopify.routes.root}cart/add.js`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Requested-With': 'XMLHttpRequest', + }, + body: JSON.stringify({ id: variantId, quantity }), + }); + + if (!res.ok) { + throw await parseCartError(res); + } +} diff --git a/frontend/entrypoints/ts/utils/variant-picker.ts b/frontend/entrypoints/ts/utils/variant-picker.ts new file mode 100644 index 000000000..5c74d1977 --- /dev/null +++ b/frontend/entrypoints/ts/utils/variant-picker.ts @@ -0,0 +1,51 @@ +/** Minimal representation of a Shopify product variant as serialised by `{{ product | json }}`. */ +export interface ProductVariant { + id: number; + title: string; + price: number; + available: boolean; + options: string[]; + featured_media: { id: number } | null; +} + +/** Minimal representation of a Shopify product as serialised by `{{ product | json }}`. */ +export interface ProductData { + id: number; + variants: ProductVariant[]; +} + +/** + * Returns the first variant whose options exactly match `selectedOptions`. + * + * @param variants - Full list of product variants. + * @param selectedOptions - Currently selected value for each option position (0-indexed). + * @returns The matching variant, or `undefined` if none found. + */ +export function findVariantByOptions( + variants: ProductVariant[], + selectedOptions: string[], +): ProductVariant | undefined { + return variants.find((v) => v.options.every((opt, i) => opt === selectedOptions[i])); +} + +/** + * Returns the set of option values that are available for `targetPosition`, + * given the currently selected values for all other positions. + * + * @param variants - Full list of product variants. + * @param selectedOptions - Currently selected value for each option position (0-indexed). + * @param targetPosition - The option index (0-indexed) to evaluate. + * @returns A `Set` of available string values for that position. + */ +export function getAvailableValues( + variants: ProductVariant[], + selectedOptions: string[], + targetPosition: number, +): Set { + return new Set( + variants + .filter((v) => v.options.every((opt, i) => i === targetPosition || opt === selectedOptions[i])) + .filter((v) => v.available) + .map((v) => v.options[targetPosition]), + ); +} diff --git a/layout/theme.liquid b/layout/theme.liquid index 67d174e2c..8504ce0d1 100644 --- a/layout/theme.liquid +++ b/layout/theme.liquid @@ -39,6 +39,8 @@ {{ content_for_layout }} + {% render 'cart-drawer' %} + {% sections 'footer-group' %} diff --git a/package.json b/package.json index 0571eccba..17c227090 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "shopify:push": "shopify theme push", "typecheck": "tsc --noEmit", "vite:dev": "vite", - "vite:build": "vite build" + "vite:build": "vite build", + "smoke": "./smoke-checklist.sh" } } diff --git a/sections/cart.liquid b/sections/cart.liquid index 6d7e49841..807b5d2d0 100644 --- a/sections/cart.liquid +++ b/sections/cart.liquid @@ -7,27 +7,50 @@

    {{ 'cart.title' | t }}

    -
    - +
    +

    + + +
    0 %}hidden{% endif %}> +

    Your cart is empty.

    + Continue shopping +
    + +
      {% for item in cart.items %} -
    - - - - +
  • + {% if item.image %} + + {% render 'image', image: item.image, width: 160, class: 'h-full w-full object-cover' %} + + {% endif %} + +
    + {{ item.product.title }} + {% if item.variant.title != 'Default Title' %} +

    {{ item.variant.title }}

    + {% endif %} + +
    + + {{ item.quantity }} + + +
    + +

    {{ item.final_line_price | money }}

    +
    +
  • {% endfor %} -
    - {% render 'image', image: item.image, url: item.url %} - -

    {{ item.product.title }}

    - {{ 'cart.remove' | t | link_to: item.url_to_remove }} -
    - - -
    + - -
    +
    +

    Subtotal: {{ cart.total_price | money }}

    +
    + +
    +
    +
    {% schema %} { diff --git a/sections/collection.liquid b/sections/collection.liquid index e5a786805..cc479f776 100644 --- a/sections/collection.liquid +++ b/sections/collection.liquid @@ -7,35 +7,154 @@

    {{ collection.title }}

    -
    - {% paginate collection.products by 20 %} - {% for product in collection.products %} -
    - {% if product.featured_image %} - {% render 'image', - class: 'collection-product__image', - image: product.featured_image, - url: product.url, - width: 400, - height: 400, - crop: 'center' - %} - {% endif %} -
    -

    {{ product.title | escape | link_to: product.url }}

    -

    {{ product.price | money }}

    +{% paginate collection.products by 20 %} +
    +
    +
    +
    + +
    + + + Clear all
    + + {% if collection.filters.size > 0 %} +
    + {% for filter in collection.filters %} +
    + {{ filter.label }} + + {% case filter.type %} + {% when 'list', 'boolean' %} + {% for filter_value in filter.values %} + + {% endfor %} + {% when 'price_range' %} +
    + + +
    + {% endcase %} +
    + {% endfor %} +
    + {% endif %} +
    + + {% assign has_active_filters = false %} + {% for filter in collection.filters %} + {% if filter.active_values.size > 0 %} + {% assign has_active_filters = true %} + {% endif %} + {% if filter.type == 'price_range' %} + {% if filter.min_value.value or filter.max_value.value %} + {% assign has_active_filters = true %} + {% endif %} + {% endif %} {% endfor %} + {% if has_active_filters %} +
    + {% for filter in collection.filters %} + {% for filter_value in filter.active_values %} + + {{ filter_value.label }} + + {% endfor %} + + {% if filter.type == 'price_range' %} + {% if filter.min_value.value or filter.max_value.value %} + + {{ filter.min_value.value | default: 0 | money_without_currency }} - {{ filter.max_value.value | default: filter.range_max | money_without_currency }} + + + {% endif %} + {% endif %} + {% endfor %} +
    + {% endif %} + +
    + {% for product in collection.products %} +
    + {% if product.featured_image %} + {% render 'image', + class: 'collection-product__image', + image: product.featured_image, + url: product.url, + width: 400, + height: 400, + crop: 'center' + %} + {% endif %} +
    +

    {{ product.title | escape | link_to: product.url }}

    +

    {{ product.price | money }}

    +
    +
    + {% else %} +

    No products found for current filters.

    + {% endfor %} +
    + + {% if paginate.next %} +
    + +

    +
    + {% endif %} + {{ paginate | default_pagination }} - {% endpaginate %} -
    +
    +{% endpaginate %} {% stylesheet %} .collection-products { display: grid; - grid-template-columns: repeat(auto-fill, minmax(500px, 1fr)); + gap: 1rem; + grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); } {% endstylesheet %} diff --git a/sections/header.liquid b/sections/header.liquid index 1a17b6257..613f1992b 100644 --- a/sections/header.liquid +++ b/sections/header.liquid @@ -16,10 +16,8 @@ {% endif %} - - {% if cart.item_count > 0 %} - {{ cart.item_count }} - {% endif %} + + {{ cart.item_count }} {{ 'cart.title' | t }} diff --git a/sections/product-recommendations.liquid b/sections/product-recommendations.liquid new file mode 100644 index 000000000..852703348 --- /dev/null +++ b/sections/product-recommendations.liquid @@ -0,0 +1,43 @@ +
    + {% if recommendations.performed and recommendations.products_count > 0 %} +
    +

    You may also like

    + +
    + {% endif %} +
    + +{% schema %} +{ + "name": "Product recommendations", + "settings": [ + { + "type": "range", + "id": "products_limit", + "label": "Number of products", + "min": 2, + "max": 8, + "step": 1, + "default": 4 + } + ], + "presets": [ + { + "name": "Product recommendations" + } + ] +} +{% endschema %} diff --git a/sections/product.liquid b/sections/product.liquid index 9664dfda5..67549820c 100644 --- a/sections/product.liquid +++ b/sections/product.liquid @@ -1,52 +1,132 @@ {% comment %} - This section is used in the product template to render product page with - media, content, and add-to-cart form. + This section is used in the product template to render product page with + media, content, and add-to-cart form. - https://shopify.dev/docs/storefronts/themes/architecture/templates/product + https://shopify.dev/docs/storefronts/themes/architecture/templates/product {% endcomment %} -
    - {% for image in product.images %} - {% render 'image', class: 'product-image', image: image %} - {% endfor %} -
    +{%- assign current_variant = product.selected_or_first_available_variant -%} -
    -

    {{ product.title }}

    -

    {{ product.price | money }}

    -

    {{ product.description }}

    -
    +
    + + +
    +
    +

    {{ product.title }}

    +

    {{ current_variant.price | money }}

    +

    + {%- unless current_variant.available -%}Sold out{%- endunless -%} +

    +

    {{ product.description }}

    +
    + +
    + {% form 'product', product, id: 'product-form', data-js: 'product-form' %} + {% render 'variant-picker', product: product, current_variant: current_variant %} -
    - {% form 'product', product %} - {% assign current_variant = product.selected_or_first_available_variant %} - - - - - - - {{ form | payment_button }} - {% endform %} + + + + +

    + + {{ form | payment_button }} + {% endform %} +
    +
    +
    + + + + + {% schema %} { - "name": "t:general.product", - "settings": [], - "disabled_on": { - "groups": ["header", "footer"] - } + "name": "t:general.product", + "settings": [], + "disabled_on": { + "groups": ["header", "footer"] + } } {% endschema %} diff --git a/sections/search.liquid b/sections/search.liquid index 7177c1818..57c855b17 100644 --- a/sections/search.liquid +++ b/sections/search.liquid @@ -7,48 +7,69 @@

    {{ 'search.title' | t }}

    -
    - - -
    +
    + + +

    -
    - {% paginate search.results by 20 %} - {% # Search result items may be an article, a page, or a product. %} - {% for result in search.results %} -
    - {% assign featured_image = result.featured_image | default: result.image %} - {% if featured_image %} - {% render 'image', class: 'search-result__image', image: featured_image, url: result.url, width: 400 %} - {% endif %} -
    -

    - {{ result.title | link_to: result.url }} - {% if result.price %} - {{ result.price | money_with_currency }} - {% endif %} -

    + {% if search.performed %} + {% if search.results_count == 0 %} +

    {{ 'search.no_results_html' | t: terms: search.terms }}

    + {% else %} +

    {{ 'search.results_for_html' | t: terms: search.terms, count: search.results_count }}

    + +
    + {% paginate search.results by 20 %} + {% # Search result items may be an article, a page, or a product. %} + {% for result in search.results %} +
    + {% assign featured_image = result.featured_image | default: result.image %} + {% if featured_image %} + {% render 'image', class: 'search-result__image', image: featured_image, url: result.url, width: 400 %} + {% endif %} +
    +

    + {{ result.title | link_to: result.url }} + {% if result.price %} + {{ result.price | money_with_currency }} + {% endif %} +

    +
    -
    - {% endfor %} + {% endfor %} - {{ paginate | default_pagination }} - {% endpaginate %} -
    + {{ paginate | default_pagination }} + {% endpaginate %} +
    + {% endif %} {% endif %} -{% endif %} +
    {% stylesheet %} .search-results { diff --git a/smoke-checklist.sh b/smoke-checklist.sh new file mode 100755 index 000000000..a390ae367 --- /dev/null +++ b/smoke-checklist.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash + +set -euo pipefail + +echo "Running smoke checklist..." + +echo "1) Typecheck" +bun run typecheck + +echo "2) Build" +bun run build + +echo "3) Theme check (optional local)" +if command -v theme-check >/dev/null 2>&1; then + theme-check +elif command -v shopify >/dev/null 2>&1; then + shopify theme check +else + echo "theme-check not available locally; rely on CI Theme Check job" +fi + +echo "4) Generated asset consistency" +git diff --exit-code -- assets snippets/vite-tag.liquid + +echo "Smoke checklist passed." diff --git a/snippets/cart-drawer.liquid b/snippets/cart-drawer.liquid new file mode 100644 index 000000000..7832058d6 --- /dev/null +++ b/snippets/cart-drawer.liquid @@ -0,0 +1,45 @@ + diff --git a/snippets/meta-tags.liquid b/snippets/meta-tags.liquid index 67299c04f..45e9169cd 100644 --- a/snippets/meta-tags.liquid +++ b/snippets/meta-tags.liquid @@ -1,3 +1,12 @@ +{% doc %} + Renders all essential HTML meta tags: charset, viewport, Open Graph, Twitter Card, + JSON-LD structured data, canonical URL, and page title. + + Reads Shopify global objects directly (no parameters required): + - `page_title`, `page_description`, `canonical_url`, `page_image` + - `shop`, `product`, `cart`, `request`, `current_tags`, `current_page` +{% enddoc %} + diff --git a/snippets/product-media-image.liquid b/snippets/product-media-image.liquid new file mode 100644 index 000000000..43ee7ce04 --- /dev/null +++ b/snippets/product-media-image.liquid @@ -0,0 +1,11 @@ +{% comment %} + Renders a product image media item using media_tag. + Accepts: + - media: {Object} Shopify media object (media_type: 'image') + - class: {String} CSS classes applied to the generated img element +{% endcomment %} +{% if media != blank %} + {{ media | media_tag: class: class, widths: '600,900,1200', sizes: '(min-width: 1024px) 50vw, 100vw' }} +{% else %} + {{ 'product-1' | placeholder_svg_tag: class }} +{% endif %} diff --git a/snippets/product-media-video.liquid b/snippets/product-media-video.liquid new file mode 100644 index 000000000..0aed54932 --- /dev/null +++ b/snippets/product-media-video.liquid @@ -0,0 +1,13 @@ +{% comment %} + Renders a product video media item using media_tag. + Accepts: + - media: {Object} Shopify media object (media_type: 'video' or 'external_video') + - class: {String} CSS classes applied to the wrapper div +{% endcomment %} +{% if media != blank %} +
    + {{ media | media_tag: loop: false, muted: false }} +
    +{% else %} +
    {{ 'product-1' | placeholder_svg_tag }}
    +{% endif %} diff --git a/snippets/variant-picker.liquid b/snippets/variant-picker.liquid new file mode 100644 index 000000000..7f920dde3 --- /dev/null +++ b/snippets/variant-picker.liquid @@ -0,0 +1,31 @@ +{% doc %} + Renders the variant picker for a product form. + Outputs a hidden input for the selected variant ID and one button group + per product option. Must be rendered inside a {% form 'product' %} block. + + @param {product} product - The product object + @param {variant} current_variant - The currently selected variant (used for server-side aria-pressed) +{% enddoc %} + + + +{% for option in product.options_with_values %} +
    +

    + {{ option.name }}: + {{- option.selected_value -}} +

    + {% for value in option.values %} + + {% endfor %} +
    +{% endfor %} diff --git a/snippets/vite-tag.liquid b/snippets/vite-tag.liquid index c143d3843..6d1b82fc3 100644 --- a/snippets/vite-tag.liquid +++ b/snippets/vite-tag.liquid @@ -7,7 +7,7 @@ assign path = entry | replace: '@ts/', 'ts/' | replace: '@css/', 'css/' | replace: '~/', '../' | replace: '@/', '../' %} {% if path == "/frontend/entrypoints/css/main.css" or path == "css/main.css" %} - {{ 'main-C4sk1vw8.css' | asset_url | split: '?' | first | stylesheet_tag: preload: preload_stylesheet }} + {{ 'main-CZyZ8laC.css' | asset_url | split: '?' | first | stylesheet_tag: preload: preload_stylesheet }} {% elsif path == "/frontend/entrypoints/ts/404.ts" or path == "ts/404.ts" %} {% elsif path == "/frontend/entrypoints/ts/article.ts" or path == "ts/article.ts" %} @@ -15,9 +15,17 @@ {% elsif path == "/frontend/entrypoints/ts/blog.ts" or path == "ts/blog.ts" %} {% elsif path == "/frontend/entrypoints/ts/cart.ts" or path == "ts/cart.ts" %} - + + + +{% elsif path == "/frontend/entrypoints/ts/cart/drawer.ts" or path == "ts/cart/drawer.ts" %} + + +{% elsif path == "/frontend/entrypoints/ts/cart/page.ts" or path == "ts/cart/page.ts" %} + + {% elsif path == "/frontend/entrypoints/ts/collection.ts" or path == "ts/collection.ts" %} - + {% elsif path == "/frontend/entrypoints/ts/gift-card.ts" or path == "ts/gift-card.ts" %} {% elsif path == "/frontend/entrypoints/ts/index.ts" or path == "ts/index.ts" %} @@ -29,9 +37,33 @@ {% elsif path == "/frontend/entrypoints/ts/password.ts" or path == "ts/password.ts" %} {% elsif path == "/frontend/entrypoints/ts/product.ts" or path == "ts/product.ts" %} - + + + + + + + +{% elsif path == "/frontend/entrypoints/ts/product/handlers.ts" or path == "ts/product/handlers.ts" %} + + + + + +{% elsif path == "/frontend/entrypoints/ts/product/recommendations.ts" or path == "ts/product/recommendations.ts" %} + +{% elsif path == "/frontend/entrypoints/ts/product/state.ts" or path == "ts/product/state.ts" %} + +{% elsif path == "/frontend/entrypoints/ts/product/sync.ts" or path == "ts/product/sync.ts" %} + + + {% elsif path == "/frontend/entrypoints/ts/search.ts" or path == "ts/search.ts" %} - + {% elsif path == "/frontend/entrypoints/ts/theme.ts" or path == "ts/theme.ts" %} - + +{% elsif path == "/frontend/entrypoints/ts/utils/cart.ts" or path == "ts/utils/cart.ts" %} + +{% elsif path == "/frontend/entrypoints/ts/utils/variant-picker.ts" or path == "ts/utils/variant-picker.ts" %} + {% endif %} diff --git a/templates/product.json b/templates/product.json index 8329ddc68..b1568732e 100644 --- a/templates/product.json +++ b/templates/product.json @@ -12,9 +12,14 @@ "main": { "type": "product", "settings": {} + }, + "product-recommendations": { + "type": "product-recommendations", + "settings": {} } }, "order": [ - "main" + "main", + "product-recommendations" ] } diff --git a/todo.md b/todo.md index d844b18c1..35246a7b1 100644 --- a/todo.md +++ b/todo.md @@ -58,80 +58,80 @@ Acceptance: ## 4) Block 3 — Core Features Rollout ### Phase 1 — PDP (Product) -- [ ] Audit current PDP markup and data attributes -- [ ] Implement variant selection state logic in `ts/product.ts` -- [ ] Sync selected variant with URL and form inputs -- [ ] Update price/media state on variant change (if media mapping exists) -- [ ] Add add-to-cart UX states (loading, success, error) -- [ ] Handle unavailable / sold-out variants correctly +- [x] Audit current PDP markup and data attributes +- [x] Implement variant selection state logic in `ts/product.ts` +- [x] Sync selected variant with URL and form inputs +- [x] Update price/media state on variant change (if media mapping exists) +- [x] Add add-to-cart UX states (loading, success, error) +- [x] Handle unavailable / sold-out variants correctly Acceptance: -- [ ] Variant change updates UI correctly -- [ ] Add-to-cart works and shows clear status -- [ ] No JS leakage outside PDP +- [x] Variant change updates UI correctly +- [x] Add-to-cart works and shows clear status +- [x] No JS leakage outside PDP -### Phase 2 — Collection (PLP) -- [ ] Implement sort behavior -- [ ] Implement filters (base behavior first) -- [ ] Add optional progressive loading only if needed +### Phase 2 — Cart / Drawer +- [x] Implement drawer open/close and focus handling +- [x] Quantity update/remove flows with loading states +- [x] Empty cart state UX +- [x] Error handling and resilience Acceptance: -- [ ] Filters/sort stable and reversible -- [ ] PLP JS isolated to collection templates +- [x] Keyboard accessible drawer behavior +- [x] Cart updates reliable with clear feedback -### Phase 3 — Cart / Drawer -- [ ] Implement drawer open/close and focus handling -- [ ] Quantity update/remove flows with loading states -- [ ] Empty cart state UX -- [ ] Error handling and resilience +### Phase 3 — Collection (PLP) +- [x] Implement sort behavior +- [x] Implement filters (base behavior first) +- [x] Add optional progressive loading only if needed Acceptance: -- [ ] Keyboard accessible drawer behavior -- [ ] Cart updates reliable with clear feedback +- [x] Filters/sort stable and reversible +- [x] PLP JS isolated to collection templates ### Phase 4 — Search -- [ ] Implement predictive search UI state -- [ ] Implement results state handling -- [ ] Graceful fallback when predictive endpoint unavailable +- [x] Implement predictive search UI state +- [x] Implement results state handling +- [x] Graceful fallback when predictive endpoint unavailable Acceptance: -- [ ] Search interactions stable -- [ ] No cross-template JS side effects +- [x] Search interactions stable +- [x] No cross-template JS side effects --- ## 5) Performance & Loading Validation -- [ ] Capture baseline metrics (Home, PDP, PLP): LCP, CLS, JS payload -- [ ] Verify each template loads only `ts/theme.ts` + its own entrypoint -- [ ] Add dynamic imports for heavy optional logic where useful +- [x] Capture baseline metrics (Home, PDP, PLP): LCP, CLS, JS payload +- [x] Verify each template loads only `ts/theme.ts` + its own entrypoint +- [x] Add dynamic imports for heavy optional logic where useful Acceptance: -- [ ] No regressions vs baseline -- [ ] Template-specific payload discipline maintained +- [x] No regressions vs baseline +- [x] Template-specific payload discipline maintained --- ## 6) Documentation Finalization -- [ ] Keep README aligned with actual scripts and branch-linked workflow -- [ ] Keep CLAUDE.md aligned with architecture and conventions -- [ ] Add short “how to start new feature branch” section (optional) +- [x] Keep README aligned with actual scripts and branch-linked workflow +- [x] Keep CLAUDE.md aligned with architecture and conventions +- [x] Add short “how to start new feature branch” section (optional) Acceptance: -- [ ] New team member can run project in <10 minutes +- [x] New team member can run project in <10 minutes --- ## 7) Git/Release Workflow -- [ ] Enforce branch model: `feat/* -> staging -> main` -- [ ] Before merging to `staging`/`main`, always run `bun run build` -- [ ] Commit generated artifacts for branch-linked Shopify themes +- [x] Enforce branch model: `feat/* -> staging -> main` +- [x] Before merging to `staging`/`main`, always run `bun run build` +- [x] Commit generated artifacts for branch-linked Shopify themes Acceptance: -- [ ] Shopify branch previews always reflect latest built assets +- [x] Shopify branch previews always reflect latest built assets --- ## 8) Nice-to-have (after core) -- [ ] Add import alias usage examples (`@ts`, `@css`) in code/docs -- [ ] Add lightweight linting strategy for TS/Liquid (if needed) -- [ ] Add automated smoke checklist script (optional) +- [x] Add import alias usage examples (`@ts`, `@css`) in code/docs +- [x] Add lightweight linting strategy for TS/Liquid (if needed) +- [x] Add automated smoke checklist script (optional) From be78d5b02860a0cf1e8cf7ba9babfa9f46dc4730 Mon Sep 17 00:00:00 2001 From: Luca Argentieri Date: Mon, 23 Mar 2026 14:30:55 +0100 Subject: [PATCH 10/14] align starter with docs-first cart/search patterns and AI contributor workflow --- AGENTS.md | 71 ++++ CLAUDE.md | 11 +- PERFORMANCE_BASELINE.md | 39 -- README.md | 37 +- assets/.vite/manifest.json | 47 ++- assets/cart-CmG_L9o1.js | 1 - assets/cart-DAS8aBaw.js | 1 - assets/cart-DExqgdDq.js | 1 + assets/cart-Kw4e-iq4.js | 1 + assets/collection-BkAueZBQ.js | 1 - assets/collection-KDkQRAB3.js | 1 + assets/drawer-BOuNB4a_.js | 1 + assets/drawer-DzmJHKNX.js | 1 + assets/drawer-HPI4d66D.js | 18 - assets/handlers-D9xx4MJz.js | 1 - assets/handlers-DnmO5tfH.js | 1 + assets/main-BF2QWjU5.css | 1 + assets/main-CZyZ8laC.css | 1 - assets/page-Bwhrow8I.js | 1 + assets/page-CWMiLRqk.js | 18 - assets/product-BxBL01GO.js | 1 + assets/product-DPYfuql-.js | 1 - assets/section-rendering-vVqNxfcB.js | 1 + assets/state-Bj51Sk4P.js | 1 - assets/state-Bwp_aMHz.js | 1 + assets/sync-DkV0cu7G.js | 1 - assets/sync-YStD_kYI.js | 1 + assets/theme-BHxQweo7.js | 2 - assets/theme-C1WNKfRD.js | 2 + frontend/entrypoints/ts/cart/drawer.ts | 213 ++++++----- frontend/entrypoints/ts/cart/page.ts | 200 +++++------ frontend/entrypoints/ts/collection.ts | 295 ++++++++++++--- frontend/entrypoints/ts/product.ts | 50 ++- frontend/entrypoints/ts/product/handlers.ts | 18 +- frontend/entrypoints/ts/product/state.ts | 1 + frontend/entrypoints/ts/product/sync.ts | 59 ++- frontend/entrypoints/ts/search/drawer.ts | 340 ++++++++++++++++++ frontend/entrypoints/ts/theme.ts | 17 +- frontend/entrypoints/ts/utils/cart.ts | 34 +- .../entrypoints/ts/utils/section-rendering.ts | 73 ++++ layout/theme.liquid | 3 +- package.json | 3 +- sections/cart-drawer.liquid | 81 +++++ sections/cart.liquid | 11 +- sections/collection.liquid | 34 +- sections/header.liquid | 27 +- sections/product.liquid | 22 +- sections/search-drawer.liquid | 58 +++ smoke-checklist.sh | 25 -- snippets/cart-drawer.liquid | 45 --- snippets/vite-tag.liquid | 53 +-- todo.md | 259 ++++++++----- 52 files changed, 1593 insertions(+), 593 deletions(-) create mode 100644 AGENTS.md delete mode 100644 PERFORMANCE_BASELINE.md delete mode 100644 assets/cart-CmG_L9o1.js delete mode 100644 assets/cart-DAS8aBaw.js create mode 100644 assets/cart-DExqgdDq.js create mode 100644 assets/cart-Kw4e-iq4.js delete mode 100644 assets/collection-BkAueZBQ.js create mode 100644 assets/collection-KDkQRAB3.js create mode 100644 assets/drawer-BOuNB4a_.js create mode 100644 assets/drawer-DzmJHKNX.js delete mode 100644 assets/drawer-HPI4d66D.js delete mode 100644 assets/handlers-D9xx4MJz.js create mode 100644 assets/handlers-DnmO5tfH.js create mode 100644 assets/main-BF2QWjU5.css delete mode 100644 assets/main-CZyZ8laC.css create mode 100644 assets/page-Bwhrow8I.js delete mode 100644 assets/page-CWMiLRqk.js create mode 100644 assets/product-BxBL01GO.js delete mode 100644 assets/product-DPYfuql-.js create mode 100644 assets/section-rendering-vVqNxfcB.js delete mode 100644 assets/state-Bj51Sk4P.js create mode 100644 assets/state-Bwp_aMHz.js delete mode 100644 assets/sync-DkV0cu7G.js create mode 100644 assets/sync-YStD_kYI.js delete mode 100644 assets/theme-BHxQweo7.js create mode 100644 assets/theme-C1WNKfRD.js create mode 100644 frontend/entrypoints/ts/search/drawer.ts create mode 100644 frontend/entrypoints/ts/utils/section-rendering.ts create mode 100644 sections/cart-drawer.liquid create mode 100644 sections/search-drawer.liquid delete mode 100755 smoke-checklist.sh delete mode 100644 snippets/cart-drawer.liquid diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..a482c9123 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,71 @@ +# AGENTS.md - AI Contributor Guide + +This file is for any AI coding assistant (Claude, Codex, Cursor, etc.) working in this repository. + +## Goal + +- Keep the starter aligned with Shopify docs-first patterns. +- Keep TypeScript for behavior and Liquid for markup. +- Keep customization safe via stable `data-js` contracts. + +## Non-Negotiable Rules + +- Do not generate cart/minicart/search HTML strings in TS. +- Do not add `{% render 'vite-tag' %}` in sections/snippets. +- Keep entrypoint routing centralized in `layout/theme.liquid`. +- Keep API calls user-driven where required by this starter architecture. + +## Where to Implement Changes + +- Global bootstrap: `frontend/entrypoints/ts/theme.ts` +- Cart drawer logic: `frontend/entrypoints/ts/cart/drawer.ts` +- Cart page logic: `frontend/entrypoints/ts/cart/page.ts` +- Product logic: `frontend/entrypoints/ts/product.ts` + `frontend/entrypoints/ts/product/*` +- Collection logic: `frontend/entrypoints/ts/collection.ts` +- Search drawer logic: `frontend/entrypoints/ts/search/drawer.ts` +- Liquid markup: `sections/**`, `snippets/**` + +## Shopify Patterns Required + +- Cart mutations use bundled section rendering (`sections`, `sections_url`). +- Single section rendering (`?section_id=`) is used only where explicitly intended (e.g. drawer hydration). +- Locale-aware routes use `window.Shopify.routes.root`. + +## Stable Data Contracts + +- Cart drawer: `cart-drawer`, `cart-open`, `cart-close`, `cart-items`, `cart-empty`, `cart-subtotal` +- Cart page: `cart-page`, `cart-page-items`, `cart-page-empty`, `cart-page-footer`, `cart-page-subtotal` +- Product: `product-form`, `option-value`, `thumbnail`, `add-to-cart`, `cart-status` +- Collection: `collection-root`, `collection-controls`, `collection-products`, `collection-load-more` +- Search drawer: `search-drawer`, `search-open`, `search-close`, `search-drawer-input`, `search-drawer-groups` + +## Local Validation + +Run these before considering work complete: + +```bash +bun run typecheck +bun run build +``` + +If available in the environment: + +```bash +theme-check +``` + +## Branch-Linked Shopify Reminder + +For branches connected directly to a Shopify theme, generated assets must be committed: + +- `assets/*` +- `assets/.vite/manifest.json` +- `snippets/vite-tag.liquid` + +## Suggested Work Loop for AI + +1. Read `CLAUDE.md`, `README.md`, and this file. +2. Edit TS behavior and Liquid markup in their correct layers. +3. Preserve `data-js` hooks unless intentionally migrating both TS + Liquid. +4. Run typecheck/build. +5. Update docs/todo when architecture contracts change. diff --git a/CLAUDE.md b/CLAUDE.md index 22d900cb7..4a02d9613 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -59,6 +59,15 @@ loading is handled exclusively by the layout. `ts/gift-card.ts` is the only exception: loaded directly in `templates/gift_card.liquid` because that template uses `{% layout none %}`. +## TS/Liquid contract + +**TS logic only, Liquid markup only.** + +- Keep behavior and state orchestration in TypeScript modules under `frontend/entrypoints/ts/**` +- Keep editable markup in Liquid sections/snippets under `sections/**` and `snippets/**` +- Treat `data-js` attributes as the integration contract between TS and Liquid +- Do not move markup generation into TS string templates for cart/minicart/search flows + ## When to add JS - **Global bootstrap only**: `frontend/entrypoints/ts/theme.ts` @@ -167,7 +176,7 @@ Before merging to `staging` or `main`, ensure: - `bun run build` passes - `theme-check` passes - Smoke test the templates touched by the PR -- `bun run smoke` passes locally when available +- Verify manual QA on touched templates (PDP, PLP, cart page, minicart as relevant) CI enforces branch flow: - PR to `staging` must come from `feat/*` diff --git a/PERFORMANCE_BASELINE.md b/PERFORMANCE_BASELINE.md deleted file mode 100644 index bdfd8d67a..000000000 --- a/PERFORMANCE_BASELINE.md +++ /dev/null @@ -1,39 +0,0 @@ -# Performance Baseline - -Date: 2026-03-19 - -## Method - -- Build command: `bun run build` -- Asset source: `assets/.vite/manifest.json` + Vite build output -- Template loading rule validation: `layout/theme.liquid` router inspection - -## JS Payload Baseline (Built Assets) - -- `ts/theme.ts`: `assets/theme-BHxQweo7.js` (2.39 kB, gzip 1.18 kB) -- `ts/product.ts`: `assets/product-DPYfuql-.js` (1.13 kB, gzip 0.57 kB) -- `ts/collection.ts`: `assets/collection-BkAueZBQ.js` (1.43 kB, gzip 0.61 kB) -- `ts/cart.ts`: `assets/cart-DAS8aBaw.js` (0.12 kB, gzip 0.13 kB) -- `ts/search.ts`: `assets/search-Dt-ufTnA.js` (2.43 kB, gzip 1.19 kB) -- cart drawer split chunk: `assets/drawer-HPI4d66D.js` (4.83 kB, gzip 1.95 kB) - -## CSS Baseline - -- `css/main.css`: `assets/main-CZyZ8laC.css` (25.02 kB, gzip 5.41 kB) - -## Template-Specific Loading Validation - -From `layout/theme.liquid`: - -- Always loaded: `css/main.css`, `ts/theme.ts` -- Product: `ts/product.ts` -- Collection: `ts/collection.ts` -- Cart: `ts/cart.ts` -- Search: `ts/search.ts` - -This keeps template payloads scoped to `theme + template entrypoint`. - -## LCP/CLS Baseline - -- Requires runtime capture against live/preview storefront pages (Home, PDP, PLP). -- Use Lighthouse/PageSpeed in browser context to collect and persist numeric baseline values. diff --git a/README.md b/README.md index 50ddec59d..876e65c63 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,10 @@ - Built assets are committed to branch for Shopify Git-connected themes - CI validates quality gates (`typecheck`, `vite:build`, `theme-check`) +- AI-ready contributor workflow + - Generic agent guide in `AGENTS.md` + - Architecture + conventions in `CLAUDE.md` + --- ## Scripts @@ -47,7 +51,6 @@ - `vite:dev`: Start Vite dev server only - `vite:build`: Build Vite assets only - `shopify:dev`: Start Shopify theme dev server (`development` environment) -- `smoke`: Run local smoke checklist (`typecheck`, `build`, optional theme-check, generated assets diff) --- @@ -133,6 +136,29 @@ --- +## Customization Contract + +- Keep TS modules focused on behavior/state only. + - Put DOM events, async flows, and state sync in `frontend/entrypoints/ts/**`. +- Keep Liquid focused on markup/content structure only. + - Put editable HTML structure in `sections/**` and `snippets/**`. +- Treat `data-js="..."` attributes as a stable public contract between TS and Liquid. + - You can restyle or rearrange markup as long as required `data-js` hooks remain intact. + +- Search drawer safe customization points: + - Layout container and spacing in `sections/search-drawer.liquid` + - Result card markup inside `frontend/entrypoints/ts/search/drawer.ts` (`createResultItem`) + - Group ordering/labels inside `frontend/entrypoints/ts/search/drawer.ts` (`renderGroups`) + +- Stable `data-js` contracts by module: + - Cart drawer: `cart-drawer`, `cart-open`, `cart-close`, `cart-items`, `cart-empty`, `cart-subtotal` + - Cart page: `cart-page`, `cart-page-items`, `cart-page-empty`, `cart-page-footer`, `cart-page-subtotal` + - Product: `product-form`, `option-value`, `thumbnail`, `add-to-cart`, `cart-status` + - Collection: `collection-root`, `collection-controls`, `collection-products`, `collection-load-more` + - Search drawer: `search-drawer`, `search-open`, `search-close`, `search-drawer-input`, `search-drawer-groups` + +--- + ## Environment Configuration - `.env` (local, gitignored) @@ -185,10 +211,13 @@ git pull git checkout -b feat/ ``` -Before opening a PR: +Before opening a PR, run local quality gates: ```bash -bun run smoke +bun run typecheck +bun run build +# Optional when installed locally +theme-check ``` If the branch is Shopify Git-connected, commit generated build artifacts: @@ -213,7 +242,7 @@ import '@css/main.css'; - TypeScript quality gate: `bun run typecheck` - Liquid quality gate: Theme Check in CI (`Theme Check` job) -- Local smoke: `bun run smoke` (runs typecheck, build, optional local theme-check, generated assets diff) +- Local checks: `bun run typecheck` and `bun run build` (plus optional `theme-check`) --- diff --git a/assets/.vite/manifest.json b/assets/.vite/manifest.json index 22de2e21e..0aa5b8af9 100644 --- a/assets/.vite/manifest.json +++ b/assets/.vite/manifest.json @@ -1,6 +1,6 @@ { "frontend/entrypoints/css/main.css": { - "file": "main-CZyZ8laC.css", + "file": "main-BF2QWjU5.css", "src": "frontend/entrypoints/css/main.css", "isEntry": true, "name": "main", @@ -27,36 +27,39 @@ "isEntry": true }, "frontend/entrypoints/ts/cart.ts": { - "file": "cart-DAS8aBaw.js", + "file": "cart-DExqgdDq.js", "name": "cart", "src": "frontend/entrypoints/ts/cart.ts", "isEntry": true, "imports": [ "frontend/entrypoints/ts/cart/page.ts", - "frontend/entrypoints/ts/utils/cart.ts" + "frontend/entrypoints/ts/utils/cart.ts", + "frontend/entrypoints/ts/utils/section-rendering.ts" ] }, "frontend/entrypoints/ts/cart/drawer.ts": { - "file": "drawer-HPI4d66D.js", + "file": "drawer-DzmJHKNX.js", "name": "drawer", "src": "frontend/entrypoints/ts/cart/drawer.ts", "isEntry": true, "isDynamicEntry": true, "imports": [ - "frontend/entrypoints/ts/utils/cart.ts" + "frontend/entrypoints/ts/utils/cart.ts", + "frontend/entrypoints/ts/utils/section-rendering.ts" ] }, "frontend/entrypoints/ts/cart/page.ts": { - "file": "page-CWMiLRqk.js", + "file": "page-Bwhrow8I.js", "name": "page", "src": "frontend/entrypoints/ts/cart/page.ts", "isEntry": true, "imports": [ - "frontend/entrypoints/ts/utils/cart.ts" + "frontend/entrypoints/ts/utils/cart.ts", + "frontend/entrypoints/ts/utils/section-rendering.ts" ] }, "frontend/entrypoints/ts/collection.ts": { - "file": "collection-BkAueZBQ.js", + "file": "collection-KDkQRAB3.js", "name": "collection", "src": "frontend/entrypoints/ts/collection.ts", "isEntry": true @@ -92,7 +95,7 @@ "isEntry": true }, "frontend/entrypoints/ts/product.ts": { - "file": "product-DPYfuql-.js", + "file": "product-BxBL01GO.js", "name": "product", "src": "frontend/entrypoints/ts/product.ts", "isEntry": true, @@ -106,7 +109,7 @@ ] }, "frontend/entrypoints/ts/product/handlers.ts": { - "file": "handlers-D9xx4MJz.js", + "file": "handlers-DnmO5tfH.js", "name": "handlers", "src": "frontend/entrypoints/ts/product/handlers.ts", "isEntry": true, @@ -124,13 +127,13 @@ "isEntry": true }, "frontend/entrypoints/ts/product/state.ts": { - "file": "state-Bj51Sk4P.js", + "file": "state-Bwp_aMHz.js", "name": "state", "src": "frontend/entrypoints/ts/product/state.ts", "isEntry": true }, "frontend/entrypoints/ts/product/sync.ts": { - "file": "sync-DkV0cu7G.js", + "file": "sync-YStD_kYI.js", "name": "sync", "src": "frontend/entrypoints/ts/product/sync.ts", "isEntry": true, @@ -145,21 +148,35 @@ "src": "frontend/entrypoints/ts/search.ts", "isEntry": true }, + "frontend/entrypoints/ts/search/drawer.ts": { + "file": "drawer-BOuNB4a_.js", + "name": "drawer", + "src": "frontend/entrypoints/ts/search/drawer.ts", + "isEntry": true, + "isDynamicEntry": true + }, "frontend/entrypoints/ts/theme.ts": { - "file": "theme-BHxQweo7.js", + "file": "theme-C1WNKfRD.js", "name": "theme", "src": "frontend/entrypoints/ts/theme.ts", "isEntry": true, "dynamicImports": [ - "frontend/entrypoints/ts/cart/drawer.ts" + "frontend/entrypoints/ts/cart/drawer.ts", + "frontend/entrypoints/ts/search/drawer.ts" ] }, "frontend/entrypoints/ts/utils/cart.ts": { - "file": "cart-CmG_L9o1.js", + "file": "cart-Kw4e-iq4.js", "name": "cart", "src": "frontend/entrypoints/ts/utils/cart.ts", "isEntry": true }, + "frontend/entrypoints/ts/utils/section-rendering.ts": { + "file": "section-rendering-vVqNxfcB.js", + "name": "section-rendering", + "src": "frontend/entrypoints/ts/utils/section-rendering.ts", + "isEntry": true + }, "frontend/entrypoints/ts/utils/variant-picker.ts": { "file": "variant-picker-DRrQ71gT.js", "name": "variant-picker", diff --git a/assets/cart-CmG_L9o1.js b/assets/cart-CmG_L9o1.js deleted file mode 100644 index 651392df4..000000000 --- a/assets/cart-CmG_L9o1.js +++ /dev/null @@ -1 +0,0 @@ -async function o(t){const a=await t.json().catch(()=>({}));return new Error(a.description??"Cart request failed")}async function n(){const t=await fetch(`${window.Shopify.routes.root}cart.js`,{headers:{Accept:"application/json","X-Requested-With":"XMLHttpRequest"}});if(!t.ok)throw await o(t);return await t.json()}async function r(t,a){const e=await fetch(`${window.Shopify.routes.root}cart/change.js`,{method:"POST",headers:{"Content-Type":"application/json",Accept:"application/json","X-Requested-With":"XMLHttpRequest"},body:JSON.stringify({line:t,quantity:a})});if(!e.ok)throw await o(e);return await e.json()}async function i(t,a){const e=await fetch(`${window.Shopify.routes.root}cart/add.js`,{method:"POST",headers:{"Content-Type":"application/json","X-Requested-With":"XMLHttpRequest"},body:JSON.stringify({id:t,quantity:a})});if(!e.ok)throw await o(e)}export{i as a,r as c,n as g}; diff --git a/assets/cart-DAS8aBaw.js b/assets/cart-DAS8aBaw.js deleted file mode 100644 index d1234fea1..000000000 --- a/assets/cart-DAS8aBaw.js +++ /dev/null @@ -1 +0,0 @@ -import{i as t}from"./page-CWMiLRqk.js";import"./cart-CmG_L9o1.js";document.addEventListener("DOMContentLoaded",()=>{t()}); diff --git a/assets/cart-DExqgdDq.js b/assets/cart-DExqgdDq.js new file mode 100644 index 000000000..26f89ee40 --- /dev/null +++ b/assets/cart-DExqgdDq.js @@ -0,0 +1 @@ +import{i as t}from"./page-Bwhrow8I.js";import"./cart-Kw4e-iq4.js";import"./section-rendering-vVqNxfcB.js";document.addEventListener("DOMContentLoaded",()=>{t()}); diff --git a/assets/cart-Kw4e-iq4.js b/assets/cart-Kw4e-iq4.js new file mode 100644 index 000000000..2ccaa632b --- /dev/null +++ b/assets/cart-Kw4e-iq4.js @@ -0,0 +1 @@ +async function s(t){const n=await t.json().catch(()=>({}));return new Error(n.description??"Cart request failed")}function i(t){return t?t.startsWith("/")?t:`/${t}`:"/"}async function r(t,n,e={}){const o={line:t,quantity:n};e.sections&&(o.sections=e.sections),e.sectionsUrl&&(o.sections_url=i(e.sectionsUrl));const a=await fetch(`${window.Shopify.routes.root}cart/change.js`,{method:"POST",headers:{"Content-Type":"application/json",Accept:"application/json","X-Requested-With":"XMLHttpRequest"},body:JSON.stringify(o)});if(!a.ok)throw await s(a);return await a.json()}async function c(t,n){const e=await fetch(`${window.Shopify.routes.root}cart/add.js`,{method:"POST",headers:{"Content-Type":"application/json","X-Requested-With":"XMLHttpRequest"},body:JSON.stringify({id:t,quantity:n})});if(!e.ok)throw await s(e)}export{c as a,r as c}; diff --git a/assets/collection-BkAueZBQ.js b/assets/collection-BkAueZBQ.js deleted file mode 100644 index ea2c53e99..000000000 --- a/assets/collection-BkAueZBQ.js +++ /dev/null @@ -1 +0,0 @@ -function o(t){const e=document.querySelector('[data-js="collection-load-status"]');e&&(e.textContent=t)}function i(){return document.querySelector('[data-js="collection-load-more"]')}async function u(t){const e=t.dataset.nextUrl;if(!(!e||t.disabled)){t.disabled=!0,o("Loading more products...");try{const r=await fetch(e,{headers:{"X-Requested-With":"XMLHttpRequest"}});if(!r.ok)throw new Error("Failed to load next collection page");const l=await r.text(),n=new DOMParser().parseFromString(l,"text/html"),c=n.querySelector('[data-js="collection-products"]'),d=document.querySelector('[data-js="collection-products"]');if(!c||!d)throw new Error("Could not parse collection products");c.querySelectorAll('[data-js="collection-product-card"]').forEach(s=>{d.appendChild(s)});const a=n.querySelector('[data-js="collection-load-more"]');if(a?.dataset.nextUrl){t.dataset.nextUrl=a.dataset.nextUrl,t.disabled=!1,o("More products loaded.");return}t.remove(),o("All products loaded.")}catch{t.disabled=!1,o("Could not load more products. Please try again.")}}}function m(){const t=document.querySelector('[data-js="collection-controls"]'),e=document.querySelector('[data-js="collection-sort"]');!t||!e||e.addEventListener("change",()=>{t.requestSubmit()})}function f(){const t=i();t&&t.addEventListener("click",()=>{u(t)})}document.addEventListener("DOMContentLoaded",()=>{document.querySelector('[data-js="collection-root"]')&&(m(),f())}); diff --git a/assets/collection-KDkQRAB3.js b/assets/collection-KDkQRAB3.js new file mode 100644 index 000000000..84fbd6ca7 --- /dev/null +++ b/assets/collection-KDkQRAB3.js @@ -0,0 +1 @@ +const C='[data-js="collection-root"]';function q(e){return{root:e,sectionId:e.dataset.sectionId??"",controls:e.querySelector('[data-js="collection-controls"]'),products:e.querySelector('[data-js="collection-products"]'),paginationWrap:e.querySelector('[data-js="collection-pagination-wrap"]'),loadStatus:e.querySelector('[data-js="collection-load-status"]'),status:e.querySelector('[data-js="collection-status"]'),error:e.querySelector('[data-js="collection-error"]')}}function g(e,n){e.status&&(e.status.textContent=n)}function w(e,n){e.loadStatus&&(e.loadStatus.textContent=n)}function m(e,n){e.error&&(e.error.textContent=n)}function y(e,n){e.root.setAttribute("aria-busy",String(n))}function l(e){return new URL(e,window.location.origin)}function x(e,n){const t=new URL(e.toString());return t.searchParams.set("section_id",n),`${t.pathname}${t.search}`}function j(e){const n=l(e.action||window.location.href),t=new URLSearchParams;return new FormData(e).forEach((u,S)=>{const d=String(u).trim();d.length>0&&t.append(S,d)}),n.search=t.toString(),n}function v(e){return new DOMParser().parseFromString(e,"text/html").querySelector(C)}document.addEventListener("DOMContentLoaded",()=>{const e=document.querySelector(C);if(!e)return;let n=e,t=q(n);if(!t.sectionId)return;let s=0,u=null;const S=o=>{n=o,t=q(o)},d=async(o,a)=>{u?.abort(),u=new AbortController;const r=await fetch(x(o,t.sectionId),{signal:u.signal,headers:{"X-Requested-With":"XMLHttpRequest"}});if(!r.ok)throw new Error("Collection section request failed");const i=await r.text();if(a!==s)throw new DOMException("Stale collection request","AbortError");const c=v(i);if(!c)throw new Error("Collection section parse failed");return c},p=async(o,a)=>{const r=++s;y(t,!0),m(t,""),g(t,"Loading products...");try{const i=await d(o,r);n.replaceWith(i),S(i),a&&window.history.pushState({source:"collection-replace"},"",`${o.pathname}${o.search}`),g(t,"Products updated."),w(t,"")}catch(i){if(i instanceof DOMException&&i.name==="AbortError")return;m(t,"Could not update collection right now. Please try again."),g(t,"Collection update failed.")}finally{r===s&&y(t,!1)}},E=async o=>{const a=l(o),r=++s;y(t,!0),m(t,""),w(t,"Loading more products...");try{const i=await d(a,r),c=q(i);if(!t.products||!c.products||!t.paginationWrap||!c.paginationWrap)throw new Error("Collection append targets missing");const f=document.createDocumentFragment();c.products.querySelectorAll('[data-js="collection-product-card"]').forEach(h=>{f.appendChild(h)}),t.products.appendChild(f),t.paginationWrap.replaceWith(c.paginationWrap),t.paginationWrap=c.paginationWrap,window.history.pushState({source:"collection-append"},"",`${a.pathname}${a.search}`),w(t,"More products loaded."),g(t,"Collection updated.")}catch(i){if(i instanceof DOMException&&i.name==="AbortError")return;m(t,"Could not load more products. Please try again."),w(t,"Load more failed.")}finally{r===s&&y(t,!1)}},R=o=>{const a=o.target,r=a.closest('[data-js="collection-load-more"]');if(r){o.preventDefault();const h=r.dataset.nextUrl;if(!h||r.disabled)return;r.disabled=!0,r.setAttribute("aria-busy","true"),E(h).finally(()=>{r.disabled=!1,r.setAttribute("aria-busy","false")});return}const i=a.closest('[data-js="collection-clear"]');if(i){o.preventDefault(),p(l(i.href),!0);return}const c=a.closest('[data-js="collection-filter-remove"]');if(c){o.preventDefault(),p(l(c.href),!0);return}const f=a.closest('[data-js="collection-default-pagination"] a');f&&(o.preventDefault(),p(l(f.href),!0))},b=o=>{!o.target.closest("input, select")||!t.controls||t.controls.requestSubmit()},L=o=>{const a=o.target;a.matches('[data-js="collection-controls"]')&&(o.preventDefault(),p(j(a),!0))};document.addEventListener("click",o=>{n.contains(o.target)&&R(o)}),document.addEventListener("change",o=>{n.contains(o.target)&&b(o)}),document.addEventListener("submit",o=>{n.contains(o.target)&&L(o)}),window.addEventListener("popstate",()=>{p(l(window.location.href),!1)})}); diff --git a/assets/drawer-BOuNB4a_.js b/assets/drawer-BOuNB4a_.js new file mode 100644 index 000000000..9957d1efa --- /dev/null +++ b/assets/drawer-BOuNB4a_.js @@ -0,0 +1 @@ +const M='a[href], button:not([disabled]), textarea, input, select, [tabindex]:not([tabindex="-1"])';function $(t,n=250){let r=null;return(...a)=>{r!==null&&window.clearTimeout(r),r=window.setTimeout(()=>{t(...a)},n)}}function H(t){const n=document.createElement("li"),r=document.createElement("a");if(r.href=t.url,r.className="flex items-center gap-3 rounded border px-2 py-2 hover:bg-gray-50",t.image?.url){const o=document.createElement("img");o.src=t.image.url,o.alt=t.image.alt??t.title,o.className="h-10 w-10 object-cover",r.appendChild(o)}const a=document.createElement("span");return a.className="text-sm",a.textContent=t.title,r.appendChild(a),n.appendChild(r),n}function U(t){const n=document.createElement("section"),r=document.createElement("h3");r.className="mb-2 text-xs font-semibold uppercase tracking-wide opacity-70",r.textContent=t.title;const a=document.createElement("ul");return a.className="space-y-1",t.items.forEach(o=>{a.appendChild(H(o))}),n.appendChild(r),n.appendChild(a),n}function X(){const t=document.querySelector('[data-js="search-drawer"]');if(!t)return;const n=t.querySelector('[data-js="search-drawer-overlay"]'),r=t.querySelector('[data-js="search-drawer-panel"]'),a=t.querySelector('[data-js="search-close"]'),o=t.querySelector('[data-js="search-drawer-form"]'),A=t.querySelector('[data-js="search-drawer-input"]'),j=t.querySelector('[data-js="search-drawer-status"]'),k=t.querySelector('[data-js="search-drawer-error"]'),f=t.querySelector('[data-js="search-drawer-loader"]'),h=t.querySelector('[data-js="search-drawer-empty"]'),m=t.querySelector('[data-js="search-drawer-groups"]');if(!n||!r||!a||!o||!A||!j||!k||!f||!h||!m)return;const y=t,w=A,S=document.querySelectorAll('[data-js="search-open"]');let d=!1,q=null,i=null,b=0;const p=e=>{j.textContent=e},x=e=>{k.textContent=e},E=e=>{r.setAttribute("aria-busy",String(e)),e?(f.classList.remove("hidden"),f.classList.add("flex")):(f.classList.add("hidden"),f.classList.remove("flex"))},D=e=>{w.setAttribute("aria-expanded",String(e))},C=e=>{m.replaceChildren(),h.textContent=e,h.classList.remove("hidden")},F=()=>Array.from(r.querySelectorAll(M)).filter(e=>!e.hasAttribute("disabled")&&!e.getAttribute("aria-hidden")),N=e=>{if(!d)return;if(e.key==="Escape"){e.preventDefault(),g();return}if(e.key!=="Tab")return;const s=F();if(s.length===0)return;const c=s[0],l=s[s.length-1];if(e.shiftKey&&document.activeElement===c){e.preventDefault(),l.focus();return}!e.shiftKey&&document.activeElement===l&&(e.preventDefault(),c.focus())};function I(e){d||(d=!0,q=e??(document.activeElement instanceof HTMLElement?document.activeElement:null),y.hidden=!1,y.setAttribute("aria-hidden","false"),S.forEach(s=>s.setAttribute("aria-expanded","true")),document.body.style.overflow="hidden",D(!0),document.addEventListener("keydown",N),w.focus())}function g(){d&&(d=!1,i?.abort(),i=null,y.hidden=!0,y.setAttribute("aria-hidden","true"),S.forEach(e=>e.setAttribute("aria-expanded","false")),document.body.style.overflow="",D(!1),E(!1),document.removeEventListener("keydown",N),q&&q.focus())}const O=e=>{if(m.replaceChildren(),e.length===0){C("No quick matches. Press Enter for full results."),p("No quick matches.");return}const s=document.createDocumentFragment();e.forEach(l=>{s.appendChild(U(l))}),m.appendChild(s),h.classList.add("hidden");const c=e.reduce((l,u)=>l+u.items.length,0);p(`${c} quick matches`)},B=async e=>{if(!d)return;if(e.length<2){i?.abort(),i=null,x(""),p("Type at least 2 characters."),E(!1),C("Start typing to see quick results.");return}i?.abort(),i=new AbortController;const s=++b;E(!0),x(""),p("Searching...");const c=new URLSearchParams;c.set("q",e),c.set("resources[type]","product,page,article"),c.set("resources[limit]","6"),c.set("resources[options][unavailable_products]","last");const l=`${window.Shopify.routes.root}search/suggest.json?${c.toString()}`;try{const u=await fetch(l,{signal:i.signal,headers:{Accept:"application/json","X-Requested-With":"XMLHttpRequest"}});if(!u.ok)throw new Error("Predictive endpoint unavailable");const L=await u.json();if(s!==b)return;const v=[],P=L.resources?.results?.products??[],R=L.resources?.results?.pages??[],T=L.resources?.results?.articles??[];P.length>0&&v.push({title:"Products",items:P.slice(0,6)}),R.length>0&&v.push({title:"Pages",items:R.slice(0,6)}),T.length>0&&v.push({title:"Articles",items:T.slice(0,6)}),O(v)}catch(u){if(u instanceof DOMException&&u.name==="AbortError"||s!==b)return;C("Quick results unavailable. Press Enter for full results."),x("Predictive search is unavailable. Full search still works."),p("Predictive search unavailable.")}finally{s===b&&E(!1)}},K=$(e=>{B(e.trim())});S.forEach(e=>{e.setAttribute("aria-expanded","false"),e.addEventListener("click",s=>{s.preventDefault(),I(e)})}),a.addEventListener("click",g),n.addEventListener("click",g),w.addEventListener("input",()=>{K(w.value)}),o.addEventListener("submit",()=>{g()})}export{X as initSearchDrawer}; diff --git a/assets/drawer-DzmJHKNX.js b/assets/drawer-DzmJHKNX.js new file mode 100644 index 000000000..d8d4f8768 --- /dev/null +++ b/assets/drawer-DzmJHKNX.js @@ -0,0 +1 @@ +import{c as V}from"./cart-Kw4e-iq4.js";import{n as W,f as X,a as Y}from"./section-rendering-vVqNxfcB.js";const Z='a[href], button:not([disabled]), textarea, input, select, [tabindex]:not([tabindex="-1"])';function rt(){const n=document.querySelector('[data-js="cart-drawer"]');if(!n)return;const S=n.querySelector('[data-js="cart-drawer-overlay"]'),q=n.querySelector('[data-js="cart-drawer-panel"]'),b=n.querySelector('[data-js="cart-close"]'),v=n.querySelector('[data-js="cart-items"]'),j=n.querySelector('[data-js="cart-empty"]'),E=n.querySelector('[data-js="cart-subtotal"]'),x=n.querySelector('[data-js="cart-drawer-status"]'),C=n.querySelector('[data-js="cart-drawer-error"]'),O=document.querySelectorAll('[data-js="cart-count"]');if(!S||!q||!b||!v||!j||!E||!x||!C)return;const o=n,H=S,L=q,g=b,K=x,T=C,A=W(window.location.pathname),u=document.querySelectorAll('[data-js="cart-open"]');let s=v,k=j,U=E,c=!1,f=!1,y=0,i=null,m=null;const p=t=>{K.textContent=t},l=t=>{T.textContent=t},D=t=>{L.setAttribute("aria-busy",String(t)),o.querySelectorAll("button").forEach(e=>{e.dataset.js==="cart-close"||(e.disabled=t)})},R=t=>{O.forEach(e=>{e.textContent=String(t),e.hidden=t<1})},P=()=>{const t=s.querySelectorAll('[data-js="cart-qty-value"]');return Array.from(t).reduce((e,a)=>{const r=Number(a.textContent??"0");return e+(Number.isFinite(r)?r:0)},0)},B=(t,e)=>{const a=s.querySelector(`[data-line="${t}"]`);if(!a)return;a.classList.toggle("animate-pulse",e);const r=a.querySelector('[data-js="cart-drawer-line-overlay"]');r&&(e?(r.classList.remove("hidden"),r.classList.add("flex")):(r.classList.add("hidden"),r.classList.remove("flex")))},N=t=>{const e=Y(t,'[data-js="cart-drawer"]',[{key:"items",current:s,selector:'[data-js="cart-items"]'},{key:"empty",current:k,selector:'[data-js="cart-empty"]'},{key:"subtotal",current:U,selector:'[data-js="cart-subtotal"]'}]);if(!e.ok)return!1;const a=e.nodes.items,r=e.nodes.empty,d=e.nodes.subtotal;return!a||!r||!d?!1:(s=a,k=r,U=d,R(P()),!0)},Q=t=>N(t),_=async()=>{l("");try{const t=await X("cart-drawer",A);if(!N(t))throw new Error("Drawer section rendering failed")}catch{l("Could not load your cart right now. Please try again."),p("Cart load failed.")}},$=async(t,e)=>{const a=++y;f=!0,l(""),D(!0),B(t,!0);try{const r=await V(t,e,{sections:["cart-drawer"],sectionsUrl:A});if(!Q(r.sections?.["cart-drawer"])||a!==y)throw new Error("Drawer section rendering failed");R(r.item_count),p(e===0?"Item removed.":"Cart updated.")}catch{l("Could not refresh cart UI. Please try again."),p("Cart update failed.")}finally{a===y&&(f=!1,D(!1),B(t,!1),i&&I())}},I=async()=>{if(!f)for(;i;){const t=i;i=null,await $(t.line,t.quantity)}},w=(t,e)=>{i={line:t,quantity:e},I()},z=()=>Array.from(L.querySelectorAll(Z)).filter(t=>!t.hasAttribute("disabled")&&!t.getAttribute("aria-hidden")),F=t=>{if(!c)return;if(t.key==="Escape"){t.preventDefault(),h();return}if(t.key!=="Tab")return;const e=z();if(e.length===0)return;const a=e[0],r=e[e.length-1];if(t.shiftKey&&document.activeElement===a){t.preventDefault(),r.focus();return}!t.shiftKey&&document.activeElement===r&&(t.preventDefault(),a.focus())};function G(t){c||(c=!0,m=t??(document.activeElement instanceof HTMLElement?document.activeElement:null),o.hidden=!1,o.setAttribute("aria-hidden","false"),u.forEach(e=>e.setAttribute("aria-expanded","true")),document.body.style.overflow="hidden",document.addEventListener("keydown",F),g.focus(),_())}function h(){c&&(c=!1,o.hidden=!0,o.setAttribute("aria-hidden","true"),u.forEach(t=>t.setAttribute("aria-expanded","false")),document.body.style.overflow="",document.removeEventListener("keydown",F),m&&m.focus())}u.forEach(t=>{t.addEventListener("click",e=>{e.preventDefault(),G(t)}),t.setAttribute("aria-expanded","false")}),g.addEventListener("click",h),H.addEventListener("click",h),o.addEventListener("click",t=>{const a=t.target.closest('[data-js="cart-qty-dec"], [data-js="cart-qty-inc"], [data-js="cart-remove"]');if(!a)return;const r=Number(a.dataset.line);if(!r)return;const J=s.querySelector(`[data-line="${r}"]`)?.querySelector('[data-js="cart-qty-value"]'),M=Number(J?.textContent??"1");if(a.dataset.js==="cart-remove"){w(r,0);return}if(a.dataset.js==="cart-qty-inc"){w(r,M+1);return}a.dataset.js==="cart-qty-dec"&&w(r,Math.max(0,M-1))})}export{rt as initCartDrawer}; diff --git a/assets/drawer-HPI4d66D.js b/assets/drawer-HPI4d66D.js deleted file mode 100644 index cd18f094d..000000000 --- a/assets/drawer-HPI4d66D.js +++ /dev/null @@ -1,18 +0,0 @@ -import{c as O,g as P}from"./cart-CmG_L9o1.js";const Q='a[href], button:not([disabled]), textarea, input, select, [tabindex]:not([tabindex="-1"])';function D(e){return e.replaceAll("&","&").replaceAll("<","<").replaceAll(">",">").replaceAll('"',""").replaceAll("'","'")}function T(e,c){try{return new Intl.NumberFormat(void 0,{style:"currency",currency:c}).format(e/100)}catch{return`${(e/100).toFixed(2)} ${c}`}}function Y(e){return e.image??e.featured_image?.url??null}function z(e){return e.product_title??e.title??"Product"}function G(e){return e.final_line_price??e.line_price??0}function V(){const e=document.querySelector('[data-js="cart-drawer"]');if(!e)return;const c=e.querySelector('[data-js="cart-drawer-overlay"]'),h=e.querySelector('[data-js="cart-drawer-panel"]'),w=e.querySelector('[data-js="cart-close"]'),g=e.querySelector('[data-js="cart-items"]'),q=e.querySelector('[data-js="cart-empty"]'),j=e.querySelector('[data-js="cart-subtotal"]'),S=e.querySelector('[data-js="cart-drawer-status"]'),E=e.querySelector('[data-js="cart-drawer-error"]'),U=document.querySelectorAll('[data-js="cart-count"]');if(!c||!h||!w||!g||!q||!j||!S||!E)return;const o=e,B=c,C=h,x=w,y=g,$=q,I=j,M=S,N=E,F=o.dataset.cartUrl??"/cart";let s=!1,m=!1,p=null;const u=t=>{M.textContent=t},d=t=>{N.textContent=t},A=t=>{C.setAttribute("aria-busy",String(t)),o.querySelectorAll("button").forEach(r=>{r.dataset.js==="cart-close"||(r.disabled=t)})},L=t=>{const r=t.currency||o.dataset.currency||"USD",n=t.item_count===0;if(U.forEach(a=>{a.textContent=String(t.item_count),a.hidden=t.item_count<1}),I.textContent=T(t.total_price,r),n){y.innerHTML="",$.hidden=!1,u("Your cart is empty.");return}$.hidden=!0,y.innerHTML=t.items.map((a,l)=>{const f=Y(a),i=D(z(a)),k=a.variant_title?D(a.variant_title):"";return` -
  • - - ${f?`${i}`:""} - -
    - ${i} - ${k?`

    ${k}

    `:""} -
    - - ${a.quantity} - - -
    -

    ${T(G(a),r)}

    -
    -
  • - `}).join("")},_=async()=>{d("");try{const t=await P();L(t)}catch{d("Could not load your cart. Redirecting to cart page..."),window.location.assign(F)}},b=async(t,r)=>{if(!m){m=!0,d(""),u("Updating cart..."),A(!0);try{const n=await O(t,r);L(n),u(r===0?"Item removed.":"Cart updated.")}catch{d("Could not update cart. Please try again."),u("Cart update failed.")}finally{m=!1,A(!1)}}},H=()=>Array.from(C.querySelectorAll(Q)).filter(t=>!t.hasAttribute("disabled")&&!t.getAttribute("aria-hidden")),R=t=>{if(!s)return;if(t.key==="Escape"){t.preventDefault(),v();return}if(t.key!=="Tab")return;const r=H();if(r.length===0)return;const n=r[0],a=r[r.length-1];if(t.shiftKey&&document.activeElement===n){t.preventDefault(),a.focus();return}!t.shiftKey&&document.activeElement===a&&(t.preventDefault(),n.focus())};function K(t){s||(s=!0,p=t??(document.activeElement instanceof HTMLElement?document.activeElement:null),o.hidden=!1,o.setAttribute("aria-hidden","false"),document.body.style.overflow="hidden",document.addEventListener("keydown",R),x.focus(),_())}function v(){s&&(s=!1,o.hidden=!0,o.setAttribute("aria-hidden","true"),document.body.style.overflow="",document.removeEventListener("keydown",R),p&&p.focus())}document.querySelectorAll('[data-js="cart-open"]').forEach(t=>{t.addEventListener("click",r=>{r.preventDefault(),K(t)})}),x.addEventListener("click",v),B.addEventListener("click",v),o.addEventListener("click",t=>{const n=t.target.closest('[data-js="cart-qty-dec"], [data-js="cart-qty-inc"], [data-js="cart-remove"]');if(!n)return;const a=Number(n.dataset.line);if(!a)return;const f=y.querySelector(`[data-line="${a}"]`)?.querySelector('[data-js="cart-qty-value"]'),i=Number(f?.textContent??"1");if(n.dataset.js==="cart-remove"){b(a,0);return}if(n.dataset.js==="cart-qty-inc"){b(a,i+1);return}n.dataset.js==="cart-qty-dec"&&b(a,Math.max(0,i-1))}),_()}export{V as initCartDrawer}; diff --git a/assets/handlers-D9xx4MJz.js b/assets/handlers-D9xx4MJz.js deleted file mode 100644 index 41e413461..000000000 --- a/assets/handlers-D9xx4MJz.js +++ /dev/null @@ -1 +0,0 @@ -import{f as c}from"./variant-picker-DRrQ71gT.js";import{a as u}from"./cart-CmG_L9o1.js";import{s as t}from"./state-Bj51Sk4P.js";import{s as r}from"./sync-DkV0cu7G.js";function f(n){const e=n.target.closest('[data-js="option-value"]');if(!e||e.disabled||e.getAttribute("aria-disabled")==="true")return;const a=Number(e.dataset.optionPosition)-1,i=e.dataset.optionValue??"";if(a<0||!i)return;t.selectedOptions[a]=i;const o=c(t.productData.variants,t.selectedOptions);if(!o){t.selectedOptions[a]=t.currentVariant.options[a]??t.selectedOptions[a];return}t.currentVariant=o,t.currentMediaId=o.featured_media?.id??null;const s=new URL(window.location.href);s.searchParams.set("variant",String(o.id)),history.replaceState({},"",`${s.pathname}${s.search}${s.hash}`),r()}function b(n){const e=n.target.closest('[data-js="thumbnail"]');if(!e)return;const a=Number(e.dataset.thumbnail);a&&(t.currentMediaId=a,r())}async function h(n){if(n.preventDefault(),t.cartState==="loading")return;const a=n.target.querySelector('input[name="quantity"]'),i=a?Math.max(1,Number(a.value)||1):1;t.cartState="loading",r();try{await u(t.currentVariant.id,i),t.cartState="success",r(),setTimeout(()=>{t.cartState="idle",r()},2e3)}catch{t.cartState="error",r(),setTimeout(()=>{t.cartState="idle",r()},3e3)}}export{h as a,b,f as o}; diff --git a/assets/handlers-DnmO5tfH.js b/assets/handlers-DnmO5tfH.js new file mode 100644 index 000000000..d16344180 --- /dev/null +++ b/assets/handlers-DnmO5tfH.js @@ -0,0 +1 @@ +import{f as u}from"./variant-picker-DRrQ71gT.js";import{a as m}from"./cart-Kw4e-iq4.js";import{s as t}from"./state-Bwp_aMHz.js";import{s as i}from"./sync-YStD_kYI.js";function I(n){const e=n.target.closest('[data-js="option-value"]');if(!e||e.disabled||e.getAttribute("aria-disabled")==="true")return;const a=Number(e.dataset.optionPosition)-1,r=e.dataset.optionValue??"";if(a<0||!r)return;t.selectedOptions[a]=r;const o=u(t.productData.variants,t.selectedOptions);if(!o){t.selectedOptions[a]=t.currentVariant.options[a]??t.selectedOptions[a];return}const d=t.currentMediaId,c=t.mediaContextVariantId;t.currentVariant=o,o.featured_media?(t.currentMediaId=o.featured_media.id,t.mediaContextVariantId=o.id):(t.currentMediaId=d,t.mediaContextVariantId=c);const s=new URL(window.location.href);s.searchParams.set("variant",String(o.id)),history.replaceState({},"",`${s.pathname}${s.search}${s.hash}`),i()}function h(n){const e=n.target.closest('[data-js="thumbnail"]');if(!e)return;const a=Number(e.dataset.thumbnail);if(!a)return;t.currentMediaId=a;const r=Number((e.dataset.variantMedia??"").split(",")[0]);r&&(t.mediaContextVariantId=r),i()}async function V(n){if(n.preventDefault(),t.cartState==="loading")return;const a=n.target.querySelector('input[name="quantity"]'),r=a?Math.max(1,Number(a.value)||1):1;t.cartState="loading",i();try{await m(t.currentVariant.id,r),t.cartState="success",i(),setTimeout(()=>{t.cartState="idle",i()},2e3)}catch{t.cartState="error",i(),setTimeout(()=>{t.cartState="idle",i()},3e3)}}export{V as a,h as b,I as o}; diff --git a/assets/main-BF2QWjU5.css b/assets/main-BF2QWjU5.css new file mode 100644 index 000000000..94e3a5bf6 --- /dev/null +++ b/assets/main-BF2QWjU5.css @@ -0,0 +1 @@ +@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-border-style:solid;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-duration:initial;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-500:oklch(63.7% .237 25.331);--color-red-700:oklch(50.5% .213 27.518);--color-yellow-500:oklch(79.5% .184 86.047);--color-green-500:oklch(72.3% .219 149.579);--color-blue-50:oklch(97% .014 254.604);--color-blue-400:oklch(70.7% .165 254.624);--color-blue-500:oklch(62.3% .214 259.815);--color-blue-600:oklch(54.6% .245 262.881);--color-blue-700:oklch(48.8% .243 264.376);--color-gray-50:oklch(98.5% .002 247.839);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-300:oklch(87.2% .01 258.338);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-600:oklch(44.6% .03 256.802);--color-gray-700:oklch(37.3% .034 259.733);--color-gray-800:oklch(27.8% .033 256.848);--color-gray-900:oklch(21% .034 264.665);--color-neutral-100:oklch(97% 0 0);--color-neutral-900:oklch(20.5% 0 0);--color-black:#000;--color-white:#fff;--spacing:.25rem;--breakpoint-sm:40rem;--breakpoint-md:48rem;--breakpoint-lg:64rem;--breakpoint-xl:80rem;--container-md:28rem;--container-lg:32rem;--container-xl:36rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height: 1.5 ;--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-3xl:1.875rem;--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5 / 2.25);--font-weight-thin:100;--font-weight-extralight:200;--font-weight-light:300;--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--font-weight-extrabold:800;--font-weight-black:900;--tracking-wide:.025em;--radius-sm:.25rem;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--shadow-sm:0 1px 3px 0 #0000001a, 0 1px 2px -1px #0000001a;--shadow-md:0 4px 6px -1px #0000001a, 0 2px 4px -2px #0000001a;--animate-spin:spin 1s linear infinite;--animate-pulse:pulse 2s cubic-bezier(.4, 0, .6, 1) infinite;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.pointer-events-none{pointer-events:none}.invisible{visibility:hidden}.visible{visibility:visible}.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.inset-0{inset:calc(var(--spacing) * 0)}.end{inset-inline-end:var(--spacing)}.top-0{top:calc(var(--spacing) * 0)}.top-\[117px\]{top:117px}.right-0{right:calc(var(--spacing) * 0)}.bottom-0{bottom:calc(var(--spacing) * 0)}.left-0{left:calc(var(--spacing) * 0)}.z-10{z-index:10}.z-20{z-index:20}.z-50{z-index:50}.container{width:100%}@media(min-width:40rem){.container{max-width:40rem}}@media(min-width:48rem){.container{max-width:48rem}}@media(min-width:64rem){.container{max-width:64rem}}@media(min-width:80rem){.container{max-width:80rem}}@media(min-width:96rem){.container{max-width:96rem}}.mx-2{margin-inline:calc(var(--spacing) * 2)}.mx-5{margin-inline:calc(var(--spacing) * 5)}.mt-1{margin-top:calc(var(--spacing) * 1)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-8{margin-bottom:calc(var(--spacing) * 8)}.ml-auto{margin-left:auto}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.aspect-square{aspect-ratio:1}.h-4{height:calc(var(--spacing) * 4)}.h-5{height:calc(var(--spacing) * 5)}.h-8{height:calc(var(--spacing) * 8)}.h-9{height:calc(var(--spacing) * 9)}.h-10{height:calc(var(--spacing) * 10)}.h-16{height:calc(var(--spacing) * 16)}.h-20{height:calc(var(--spacing) * 20)}.h-full{height:100%}.max-h-\[calc\(100vh-240px\)\]{max-height:calc(100vh - 240px)}.min-h-5{min-height:calc(var(--spacing) * 5)}.min-h-\[240px\]{min-height:240px}.min-h-svh{min-height:100svh}.w-4{width:calc(var(--spacing) * 4)}.w-5{width:calc(var(--spacing) * 5)}.w-8{width:calc(var(--spacing) * 8)}.w-9{width:calc(var(--spacing) * 9)}.w-10{width:calc(var(--spacing) * 10)}.w-16{width:calc(var(--spacing) * 16)}.w-20{width:calc(var(--spacing) * 20)}.w-\[calc\(100\%-2rem\)\]{width:calc(100% - 2rem)}.w-full{width:100%}.max-w-lg{max-width:var(--container-lg)}.max-w-md{max-width:var(--container-md)}.max-w-xl{max-width:var(--container-xl)}.min-w-6{min-width:calc(var(--spacing) * 6)}.flex-1{flex:1}.shrink-0{flex-shrink:0}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.animate-pulse{animation:var(--animate-pulse)}.animate-spin{animation:var(--animate-spin)}.cursor-pointer{cursor:pointer}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-\[64px_1fr\]{grid-template-columns:64px 1fr}.grid-cols-\[80px_1fr\]{grid-template-columns:80px 1fr}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-end{align-items:flex-end}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-1{gap:calc(var(--spacing) * 1)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 5) * calc(1 - var(--tw-space-y-reverse)))}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-xl{border-radius:var(--radius-xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-black\/20{border-color:#0003}@supports (color:color-mix(in lab,red,red)){.border-black\/20{border-color:color-mix(in oklab,var(--color-black) 20%,transparent)}}.border-gray-300{border-color:var(--color-gray-300)}.border-t-black{border-top-color:var(--color-black)}.bg-\[\#FF5A00\]{background-color:#ff5a00}.bg-black\/40{background-color:#0006}@supports (color:color-mix(in lab,red,red)){.bg-black\/40{background-color:color-mix(in oklab,var(--color-black) 40%,transparent)}}.bg-blue-400{background-color:var(--color-blue-400)}.bg-blue-500{background-color:var(--color-blue-500)}.bg-blue-600{background-color:var(--color-blue-600)}.bg-gray-800{background-color:var(--color-gray-800)}.bg-white{background-color:var(--color-white)}.bg-white\/60{background-color:#fff9}@supports (color:color-mix(in lab,red,red)){.bg-white\/60{background-color:color-mix(in oklab,var(--color-white) 60%,transparent)}}.\[mask-type\:luminance\]{mask-type:luminance}.fill-\(--brand-color\){fill:var(--brand-color)}.object-cover{object-fit:cover}.p-2{padding:calc(var(--spacing) * 2)}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.p-6{padding:calc(var(--spacing) * 6)}.p-8{padding:calc(var(--spacing) * 8)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.py-1{padding-block:calc(var(--spacing) * 1)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-8{padding-block:calc(var(--spacing) * 8)}.pb-3{padding-bottom:calc(var(--spacing) * 3)}.pb-4{padding-bottom:calc(var(--spacing) * 4)}.text-center{text-align:center}.text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.text-\(color\:--brand-text\){color:var(--brand-text)}.text-blue-500{color:var(--color-blue-500)}.text-gray-300{color:var(--color-gray-300)}.text-gray-500{color:var(--color-gray-500)}.text-gray-600{color:var(--color-gray-600)}.text-gray-700{color:var(--color-gray-700)}.text-gray-800{color:var(--color-gray-800)}.text-gray-900{color:var(--color-gray-900)}.text-green-500{color:var(--color-green-500)}.text-red-500{color:var(--color-red-500)}.text-red-700{color:var(--color-red-700)}.text-white{color:var(--color-white)}.text-yellow-500{color:var(--color-yellow-500)}.capitalize{text-transform:capitalize}.uppercase{text-transform:uppercase}.underline{text-decoration-line:underline}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.opacity-100{opacity:1}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-2xl{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a), 0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a), 0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-300{--tw-duration:.3s;transition-duration:.3s}.forced-color-adjust-auto{forced-color-adjust:auto}.forced-color-adjust-none{forced-color-adjust:none}@media(hover:hover){.group-hover\:text-blue-600:is(:where(.group):hover *){color:var(--color-blue-600)}.group-hover\:text-gray-700:is(:where(.group):hover *){color:var(--color-gray-700)}.group-hover\:underline:is(:where(.group):hover *){text-decoration-line:underline}}.peer-checked\:block:is(:where(.peer):checked~*){display:block}.first\:pt-0:first-child{padding-top:calc(var(--spacing) * 0)}.last\:pb-0:last-child{padding-bottom:calc(var(--spacing) * 0)}.odd\:bg-gray-50:nth-child(odd){background-color:var(--color-gray-50)}.even\:bg-white:nth-child(2n){background-color:var(--color-white)}.focus-within\:ring-2:focus-within{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}@media(hover:hover){.hover\:-translate-y-1:hover{--tw-translate-y:calc(var(--spacing) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.hover\:bg-blue-50:hover{background-color:var(--color-blue-50)}.hover\:bg-blue-600:hover{background-color:var(--color-blue-600)}.hover\:bg-blue-700:hover{background-color:var(--color-blue-700)}.hover\:bg-gray-50:hover{background-color:var(--color-gray-50)}.hover\:shadow-lg:hover{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}}.focus\:not-sr-only:focus{clip-path:none;white-space:normal;width:auto;height:auto;margin:0;padding:0;position:static;overflow:visible}.focus\:absolute:focus{position:absolute}.focus\:top-4:focus{top:calc(var(--spacing) * 4)}.focus\:left-4:focus{left:calc(var(--spacing) * 4)}.focus\:z-50:focus{z-index:50}.focus\:rounded:focus{border-radius:.25rem}.focus\:border-blue-500:focus{border-color:var(--color-blue-500)}.focus\:bg-white:focus{background-color:var(--color-white)}.focus\:px-4:focus{padding-inline:calc(var(--spacing) * 4)}.focus\:py-2:focus{padding-block:calc(var(--spacing) * 2)}.focus\:ring-1:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-blue-500:focus{--tw-ring-color:var(--color-blue-500)}.focus\:ring-offset-2:focus{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.focus-visible\:ring-2:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus-visible\:ring-blue-500:focus-visible{--tw-ring-color:var(--color-blue-500)}.focus-visible\:ring-offset-2:focus-visible{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.active\:bg-blue-700:active{background-color:var(--color-blue-700)}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}@media(prefers-reduced-motion:reduce){.motion-reduce\:transform-none{transform:none}.motion-reduce\:transition-none{transition-property:none}}@media(min-width:40rem){.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(min-width:48rem){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:p-8{padding:calc(var(--spacing) * 8)}.md\:px-8{padding-inline:calc(var(--spacing) * 8)}@media(hover:hover){.md\:hover\:px-10:hover{padding-inline:calc(var(--spacing) * 10)}}}@media(min-width:64rem){.lg\:w-16{width:calc(var(--spacing) * 16)}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:flex-col{flex-direction:column}.lg\:flex-row-reverse{flex-direction:row-reverse}.lg\:p-12{padding:calc(var(--spacing) * 12)}}@media(min-width:80rem){.xl\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}}@media(prefers-color-scheme:dark){.dark\:bg-blue-400{background-color:var(--color-blue-400)}.dark\:bg-blue-500{background-color:var(--color-blue-500)}.dark\:bg-gray-900{background-color:var(--color-gray-900)}.dark\:text-gray-100{color:var(--color-gray-100)}.dark\:opacity-90{opacity:.9}.dark\:shadow-none{--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark\:ring-1{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark\:ring-white\/10{--tw-ring-color:#ffffff1a}@supports (color:color-mix(in lab,red,red)){.dark\:ring-white\/10{--tw-ring-color:color-mix(in oklab, var(--color-white) 10%, transparent)}}@media(hover:hover){.dark\:hover\:bg-blue-400:hover{background-color:var(--color-blue-400)}.dark\:hover\:bg-blue-500:hover{background-color:var(--color-blue-500)}}}}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@keyframes spin{to{transform:rotate(360deg)}}@keyframes pulse{50%{opacity:.5}} diff --git a/assets/main-CZyZ8laC.css b/assets/main-CZyZ8laC.css deleted file mode 100644 index 0a29125fc..000000000 --- a/assets/main-CZyZ8laC.css +++ /dev/null @@ -1 +0,0 @@ -@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-border-style:solid;--tw-font-weight:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-duration:initial;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-500:oklch(63.7% .237 25.331);--color-red-700:oklch(50.5% .213 27.518);--color-yellow-500:oklch(79.5% .184 86.047);--color-green-500:oklch(72.3% .219 149.579);--color-blue-50:oklch(97% .014 254.604);--color-blue-400:oklch(70.7% .165 254.624);--color-blue-500:oklch(62.3% .214 259.815);--color-blue-600:oklch(54.6% .245 262.881);--color-blue-700:oklch(48.8% .243 264.376);--color-gray-50:oklch(98.5% .002 247.839);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-300:oklch(87.2% .01 258.338);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-600:oklch(44.6% .03 256.802);--color-gray-700:oklch(37.3% .034 259.733);--color-gray-800:oklch(27.8% .033 256.848);--color-gray-900:oklch(21% .034 264.665);--color-neutral-100:oklch(97% 0 0);--color-neutral-900:oklch(20.5% 0 0);--color-black:#000;--color-white:#fff;--spacing:.25rem;--breakpoint-sm:40rem;--breakpoint-md:48rem;--breakpoint-lg:64rem;--breakpoint-xl:80rem;--container-md:28rem;--container-xl:36rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height: 1.5 ;--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-3xl:1.875rem;--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5 / 2.25);--font-weight-thin:100;--font-weight-extralight:200;--font-weight-light:300;--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--font-weight-extrabold:800;--font-weight-black:900;--radius-sm:.25rem;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--shadow-sm:0 1px 3px 0 #0000001a, 0 1px 2px -1px #0000001a;--shadow-md:0 4px 6px -1px #0000001a, 0 2px 4px -2px #0000001a;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.invisible{visibility:hidden}.visible{visibility:visible}.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.inset-0{inset:calc(var(--spacing) * 0)}.start{inset-inline-start:var(--spacing)}.end{inset-inline-end:var(--spacing)}.top-0{top:calc(var(--spacing) * 0)}.top-\[117px\]{top:117px}.right-0{right:calc(var(--spacing) * 0)}.bottom-0{bottom:calc(var(--spacing) * 0)}.left-0{left:calc(var(--spacing) * 0)}.z-20{z-index:20}.z-50{z-index:50}.container{width:100%}@media(min-width:40rem){.container{max-width:40rem}}@media(min-width:48rem){.container{max-width:48rem}}@media(min-width:64rem){.container{max-width:64rem}}@media(min-width:80rem){.container{max-width:80rem}}@media(min-width:96rem){.container{max-width:96rem}}.mx-2{margin-inline:calc(var(--spacing) * 2)}.mt-1{margin-top:calc(var(--spacing) * 1)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-8{margin-bottom:calc(var(--spacing) * 8)}.ml-auto{margin-left:auto}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.aspect-square{aspect-ratio:1}.h-8{height:calc(var(--spacing) * 8)}.h-9{height:calc(var(--spacing) * 9)}.h-10{height:calc(var(--spacing) * 10)}.h-16{height:calc(var(--spacing) * 16)}.h-20{height:calc(var(--spacing) * 20)}.h-full{height:100%}.max-h-\[calc\(100vh-240px\)\]{max-height:calc(100vh - 240px)}.min-h-svh{min-height:100svh}.w-8{width:calc(var(--spacing) * 8)}.w-9{width:calc(var(--spacing) * 9)}.w-10{width:calc(var(--spacing) * 10)}.w-16{width:calc(var(--spacing) * 16)}.w-20{width:calc(var(--spacing) * 20)}.w-\[calc\(100\%-2rem\)\]{width:calc(100% - 2rem)}.w-full{width:100%}.max-w-md{max-width:var(--container-md)}.max-w-xl{max-width:var(--container-xl)}.min-w-6{min-width:calc(var(--spacing) * 6)}.flex-1{flex:1}.shrink-0{flex-shrink:0}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.cursor-pointer{cursor:pointer}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-\[64px_1fr\]{grid-template-columns:64px 1fr}.grid-cols-\[80px_1fr\]{grid-template-columns:80px 1fr}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-end{align-items:flex-end}.justify-between{justify-content:space-between}.gap-1{gap:calc(var(--spacing) * 1)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 5) * calc(1 - var(--tw-space-y-reverse)))}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-xl{border-radius:var(--radius-xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-gray-300{border-color:var(--color-gray-300)}.bg-\[\#FF5A00\]{background-color:#ff5a00}.bg-black\/40{background-color:#0006}@supports (color:color-mix(in lab,red,red)){.bg-black\/40{background-color:color-mix(in oklab,var(--color-black) 40%,transparent)}}.bg-blue-400{background-color:var(--color-blue-400)}.bg-blue-500{background-color:var(--color-blue-500)}.bg-blue-600{background-color:var(--color-blue-600)}.bg-gray-800{background-color:var(--color-gray-800)}.bg-white{background-color:var(--color-white)}.\[mask-type\:luminance\]{mask-type:luminance}.fill-\(--brand-color\){fill:var(--brand-color)}.object-cover{object-fit:cover}.p-2{padding:calc(var(--spacing) * 2)}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.p-6{padding:calc(var(--spacing) * 6)}.p-8{padding:calc(var(--spacing) * 8)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.py-1{padding-block:calc(var(--spacing) * 1)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-8{padding-block:calc(var(--spacing) * 8)}.pb-3{padding-bottom:calc(var(--spacing) * 3)}.pb-4{padding-bottom:calc(var(--spacing) * 4)}.text-center{text-align:center}.text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.text-\(color\:--brand-text\){color:var(--brand-text)}.text-blue-500{color:var(--color-blue-500)}.text-gray-300{color:var(--color-gray-300)}.text-gray-500{color:var(--color-gray-500)}.text-gray-600{color:var(--color-gray-600)}.text-gray-700{color:var(--color-gray-700)}.text-gray-800{color:var(--color-gray-800)}.text-gray-900{color:var(--color-gray-900)}.text-green-500{color:var(--color-green-500)}.text-red-500{color:var(--color-red-500)}.text-red-700{color:var(--color-red-700)}.text-white{color:var(--color-white)}.text-yellow-500{color:var(--color-yellow-500)}.capitalize{text-transform:capitalize}.underline{text-decoration-line:underline}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.opacity-100{opacity:1}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-2xl{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a), 0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a), 0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-300{--tw-duration:.3s;transition-duration:.3s}.forced-color-adjust-auto{forced-color-adjust:auto}.forced-color-adjust-none{forced-color-adjust:none}@media(hover:hover){.group-hover\:text-blue-600:is(:where(.group):hover *){color:var(--color-blue-600)}.group-hover\:text-gray-700:is(:where(.group):hover *){color:var(--color-gray-700)}.group-hover\:underline:is(:where(.group):hover *){text-decoration-line:underline}}.peer-checked\:block:is(:where(.peer):checked~*){display:block}.first\:pt-0:first-child{padding-top:calc(var(--spacing) * 0)}.last\:pb-0:last-child{padding-bottom:calc(var(--spacing) * 0)}.odd\:bg-gray-50:nth-child(odd){background-color:var(--color-gray-50)}.even\:bg-white:nth-child(2n){background-color:var(--color-white)}.focus-within\:ring-2:focus-within{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}@media(hover:hover){.hover\:-translate-y-1:hover{--tw-translate-y:calc(var(--spacing) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.hover\:bg-blue-50:hover{background-color:var(--color-blue-50)}.hover\:bg-blue-600:hover{background-color:var(--color-blue-600)}.hover\:bg-blue-700:hover{background-color:var(--color-blue-700)}.hover\:bg-gray-50:hover{background-color:var(--color-gray-50)}.hover\:shadow-lg:hover{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}}.focus\:not-sr-only:focus{clip-path:none;white-space:normal;width:auto;height:auto;margin:0;padding:0;position:static;overflow:visible}.focus\:absolute:focus{position:absolute}.focus\:top-4:focus{top:calc(var(--spacing) * 4)}.focus\:left-4:focus{left:calc(var(--spacing) * 4)}.focus\:z-50:focus{z-index:50}.focus\:rounded:focus{border-radius:.25rem}.focus\:border-blue-500:focus{border-color:var(--color-blue-500)}.focus\:bg-white:focus{background-color:var(--color-white)}.focus\:px-4:focus{padding-inline:calc(var(--spacing) * 4)}.focus\:py-2:focus{padding-block:calc(var(--spacing) * 2)}.focus\:ring-1:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-blue-500:focus{--tw-ring-color:var(--color-blue-500)}.focus\:ring-offset-2:focus{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.focus-visible\:ring-2:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus-visible\:ring-blue-500:focus-visible{--tw-ring-color:var(--color-blue-500)}.focus-visible\:ring-offset-2:focus-visible{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.active\:bg-blue-700:active{background-color:var(--color-blue-700)}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}@media(prefers-reduced-motion:reduce){.motion-reduce\:transform-none{transform:none}.motion-reduce\:transition-none{transition-property:none}}@media(min-width:40rem){.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(min-width:48rem){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:p-8{padding:calc(var(--spacing) * 8)}.md\:px-8{padding-inline:calc(var(--spacing) * 8)}@media(hover:hover){.md\:hover\:px-10:hover{padding-inline:calc(var(--spacing) * 10)}}}@media(min-width:64rem){.lg\:w-16{width:calc(var(--spacing) * 16)}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:flex-col{flex-direction:column}.lg\:flex-row-reverse{flex-direction:row-reverse}.lg\:p-12{padding:calc(var(--spacing) * 12)}}@media(min-width:80rem){.xl\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}}@media(prefers-color-scheme:dark){.dark\:bg-blue-400{background-color:var(--color-blue-400)}.dark\:bg-blue-500{background-color:var(--color-blue-500)}.dark\:bg-gray-900{background-color:var(--color-gray-900)}.dark\:text-gray-100{color:var(--color-gray-100)}.dark\:opacity-90{opacity:.9}.dark\:shadow-none{--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark\:ring-1{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark\:ring-white\/10{--tw-ring-color:#ffffff1a}@supports (color:color-mix(in lab,red,red)){.dark\:ring-white\/10{--tw-ring-color:color-mix(in oklab, var(--color-white) 10%, transparent)}}@media(hover:hover){.dark\:hover\:bg-blue-400:hover{background-color:var(--color-blue-400)}.dark\:hover\:bg-blue-500:hover{background-color:var(--color-blue-500)}}}}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0} diff --git a/assets/page-Bwhrow8I.js b/assets/page-Bwhrow8I.js new file mode 100644 index 000000000..e65331cef --- /dev/null +++ b/assets/page-Bwhrow8I.js @@ -0,0 +1 @@ +import{c as B}from"./cart-Kw4e-iq4.js";import{n as M,a as F}from"./section-rendering-vVqNxfcB.js";function R(){const o=document.querySelector('[data-js="cart-page"]');if(!o)return;const i=o.dataset.sectionId,v=M(window.location.pathname),f=o.querySelector('[data-js="cart-page-items"]'),y=o.querySelector('[data-js="cart-page-empty"]'),m=o.querySelector('[data-js="cart-page-footer"]'),w=o.querySelector('[data-js="cart-page-subtotal"]'),g=o.querySelector('[data-js="cart-page-status"]'),j=o.querySelector('[data-js="cart-page-error"]'),N=document.querySelectorAll('[data-js="cart-count"]');if(!f||!y||!m||!w||!g||!j||!i)return;let n=f,S=y,d=m,l=!1,u=0,s=null;const q=t=>{g.textContent=t},h=t=>{j.textContent=t},x=t=>{o.setAttribute("aria-busy",String(t)),o.querySelectorAll("button").forEach(e=>{e.type!=="submit"&&(e.disabled=t)})},b=t=>{N.forEach(e=>{e.textContent=String(t),e.hidden=t<1})},C=(t,e)=>{const r=n.querySelector(`[data-line="${t}"]`);if(!r)return;r.classList.toggle("animate-pulse",e);const a=r.querySelector('[data-js="cart-page-line-overlay"]');a&&(e?(a.classList.remove("hidden"),a.classList.add("flex")):(a.classList.add("hidden"),a.classList.remove("flex")))},I=t=>{const e=F(t,'[data-js="cart-page"]',[{key:"items",current:n,selector:'[data-js="cart-page-items"]'},{key:"empty",current:S,selector:'[data-js="cart-page-empty"]'},{key:"footer",current:d,selector:'[data-js="cart-page-footer"]'}]);if(!e.ok)return!1;const r=e.nodes.items,a=e.nodes.empty,c=e.nodes.footer;return!r||!a||!c?!1:(n=r,S=a,d=c,!!d.querySelector('[data-js="cart-page-subtotal"]'))},k=t=>I(t),E=async(t,e)=>{const r=++u;l=!0,x(!0),C(t,!0),h("");try{const a=await B(t,e,{sections:[i],sectionsUrl:v});if(!k(a.sections?.[i])||r!==u)throw new Error("Cart section rendering failed");b(a.item_count),q(e===0?"Item removed.":"Cart updated.")}catch{h("Could not refresh cart UI. Please try again."),q("Cart update failed.")}finally{r===u&&(l=!1,x(!1),C(t,!1),s&&L())}},L=async()=>{if(!l)for(;s;){const t=s;s=null,await E(t.line,t.quantity)}},p=(t,e)=>{s={line:t,quantity:e},L()};o.addEventListener("click",t=>{const r=t.target.closest('[data-js="cart-page-dec"], [data-js="cart-page-inc"], [data-js="cart-page-remove"]');if(!r)return;const a=Number(r.dataset.line);if(!a)return;const A=n.querySelector(`[data-line="${a}"]`)?.querySelector('[data-js="cart-page-qty"]'),U=Number(A?.textContent??"1");if(r.dataset.js==="cart-page-remove"){p(a,0);return}if(r.dataset.js==="cart-page-inc"){p(a,U+1);return}r.dataset.js==="cart-page-dec"&&p(a,Math.max(0,U-1))})}export{R as i}; diff --git a/assets/page-CWMiLRqk.js b/assets/page-CWMiLRqk.js deleted file mode 100644 index 9ede39648..000000000 --- a/assets/page-CWMiLRqk.js +++ /dev/null @@ -1,18 +0,0 @@ -import{c as _,g as x}from"./cart-CmG_L9o1.js";function q(t){return t.replaceAll("&","&").replaceAll("<","<").replaceAll(">",">").replaceAll('"',""").replaceAll("'","'")}function $(t,c){try{return new Intl.NumberFormat(void 0,{style:"currency",currency:c}).format(t/100)}catch{return`${(t/100).toFixed(2)} ${c}`}}function A(t){return t.image??t.featured_image?.url??null}function I(t){return t.product_title??t.title??"Product"}function U(t){return t.final_line_price??t.line_price??0}function N(){const t=document.querySelector('[data-js="cart-page"]');if(!t)return;const c=t.querySelector('[data-js="cart-page-items"]'),g=t.querySelector('[data-js="cart-page-empty"]'),f=t.querySelector('[data-js="cart-page-footer"]'),y=t.querySelector('[data-js="cart-page-subtotal"]'),m=t.querySelector('[data-js="cart-page-status"]'),b=t.querySelector('[data-js="cart-page-error"]'),S=document.querySelectorAll('[data-js="cart-count"]');if(!c||!g||!f||!y||!m||!b)return;const w="/cart";let u=!1;const l=e=>{m.textContent=e},d=e=>{b.textContent=e},j=e=>{t.setAttribute("aria-busy",String(e)),t.querySelectorAll("button").forEach(n=>{n.type!=="submit"&&(n.disabled=e)})},h=e=>{const n=e.currency||t.dataset.currency||"USD",r=e.item_count>0;if(S.forEach(a=>{a.textContent=String(e.item_count),a.hidden=e.item_count<1}),g.hidden=r,c.hidden=!r,f.hidden=!r,y.textContent=$(e.total_price,n),!r){c.innerHTML="",l("Your cart is empty.");return}c.innerHTML=e.items.map((a,o)=>{const i=A(a),s=q(I(a)),v=a.variant_title?q(a.variant_title):"";return` -
  • - - ${i?`${s}`:""} - -
    - ${s} - ${v?`

    ${v}

    `:""} -
    - - ${a.quantity} - - -
    -

    ${$(U(a),n)}

    -
    -
  • - `}).join("")},C=async()=>{try{const e=await x();h(e)}catch{d("Could not load cart data. Reloading..."),window.location.assign(w)}},p=async(e,n)=>{if(!u){u=!0,j(!0),d(""),l("Updating cart...");try{const r=await _(e,n);h(r),l(n===0?"Item removed.":"Cart updated.")}catch{d("Could not update cart. Please try again."),l("Cart update failed.")}finally{u=!1,j(!1)}}};t.addEventListener("click",e=>{const r=e.target.closest('[data-js="cart-page-dec"], [data-js="cart-page-inc"], [data-js="cart-page-remove"]');if(!r)return;const a=Number(r.dataset.line);if(!a)return;const i=c.querySelector(`[data-line="${a}"]`)?.querySelector('[data-js="cart-page-qty"]'),s=Number(i?.textContent??"1");if(r.dataset.js==="cart-page-remove"){p(a,0);return}if(r.dataset.js==="cart-page-inc"){p(a,s+1);return}r.dataset.js==="cart-page-dec"&&p(a,Math.max(0,s-1))}),C()}export{N as i}; diff --git a/assets/product-BxBL01GO.js b/assets/product-BxBL01GO.js new file mode 100644 index 000000000..c9f948105 --- /dev/null +++ b/assets/product-BxBL01GO.js @@ -0,0 +1 @@ +import{s as e}from"./state-Bwp_aMHz.js";import{s as d}from"./sync-YStD_kYI.js";import{o as m,a as u,b as l}from"./handlers-DnmO5tfH.js";import{l as f}from"./recommendations-H0stnrna.js";import"./variant-picker-DRrQ71gT.js";import"./cart-Kw4e-iq4.js";function s(t){const n=new URLSearchParams(window.location.search),r=Number(n.get("variant"));return t.variants.find(a=>a.id===r)??t.variants.find(a=>a.available)??t.variants[0]}function c(t){if(e.currentVariant=t,e.selectedOptions=[...t.options],t.featured_media){e.currentMediaId=t.featured_media.id,e.mediaContextVariantId=t.id;return}if(e.currentMediaId===null){const n=document.querySelector('[data-js="media-item"]'),r=Number(n?.dataset.mediaId??""),a=Number((n?.dataset.variantMedia??"").split(",")[0]);e.currentMediaId=r||null,e.mediaContextVariantId=a||e.mediaContextVariantId}}function p(t){return!!t.target.closest('[data-js="option-value"]')}function C(t){return!!t.target.closest('[data-js="thumbnail"]')}function v(){const t=document.querySelector('[data-js="product-data"]');if(!t?.textContent)return;e.productData=JSON.parse(t.textContent);const n=document.querySelector('[data-js="variant-prices"]');e.variantPrices=n?.textContent?JSON.parse(n.textContent):{},c(s(e.productData));let r=!1;const a=()=>{r||(r=!0,f())},o=document.querySelector('[data-js="product-form"]');o?.addEventListener("click",i=>{p(i)&&a(),m(i)}),o?.addEventListener("submit",i=>{a(),u(i)}),document.addEventListener("click",i=>{C(i)&&a(),l(i)}),window.addEventListener("popstate",()=>{c(s(e.productData)),e.cartState="idle",d()}),d()}document.addEventListener("DOMContentLoaded",v); diff --git a/assets/product-DPYfuql-.js b/assets/product-DPYfuql-.js deleted file mode 100644 index ab3309a34..000000000 --- a/assets/product-DPYfuql-.js +++ /dev/null @@ -1 +0,0 @@ -import{s as e}from"./state-Bj51Sk4P.js";import{s as r}from"./sync-DkV0cu7G.js";import{o as s,a as c,b as m}from"./handlers-D9xx4MJz.js";import{l as u}from"./recommendations-H0stnrna.js";import"./variant-picker-DRrQ71gT.js";import"./cart-CmG_L9o1.js";function i(t){const n=new URLSearchParams(window.location.search),o=Number(n.get("variant"));return t.variants.find(a=>a.id===o)??t.variants.find(a=>a.available)??t.variants[0]}function d(t){e.currentVariant=t,e.selectedOptions=[...t.options],e.currentMediaId=t.featured_media?.id??null}function l(){const t=document.querySelector('[data-js="product-data"]');if(!t?.textContent)return;e.productData=JSON.parse(t.textContent);const n=document.querySelector('[data-js="variant-prices"]');e.variantPrices=n?.textContent?JSON.parse(n.textContent):{},d(i(e.productData));const o=document.querySelector('[data-js="product-form"]');o?.addEventListener("click",s),o?.addEventListener("submit",a=>{c(a)}),document.addEventListener("click",m),window.addEventListener("popstate",()=>{d(i(e.productData)),e.cartState="idle",r()}),r(),u()}document.addEventListener("DOMContentLoaded",l); diff --git a/assets/section-rendering-vVqNxfcB.js b/assets/section-rendering-vVqNxfcB.js new file mode 100644 index 000000000..7a47c73b8 --- /dev/null +++ b/assets/section-rendering-vVqNxfcB.js @@ -0,0 +1 @@ +function f(e,a,o){if(!e)return{ok:!1,nodes:{}};const r=new DOMParser().parseFromString(e,"text/html").querySelector(a);if(!r)return{ok:!1,nodes:{}};const i={};for(const t of o){const n=r.querySelector(t.selector);if(!n&&t.required!==!1)return{ok:!1,nodes:{}};n&&(i[t.key]=n)}for(const t of o){const n=i[t.key];n&&t.current.replaceWith(n)}return{ok:!0,nodes:i}}function c(e){return e?e.startsWith("/")?e:`/${e}`:"/"}async function d(e,a){const o=c(a),s=new URL(o,window.location.origin);s.searchParams.set("section_id",e);const r=await fetch(s.pathname+s.search,{headers:{"X-Requested-With":"XMLHttpRequest"}});if(!r.ok)throw new Error("Section rendering request failed");return r.text()}export{f as a,d as f,c as n}; diff --git a/assets/state-Bj51Sk4P.js b/assets/state-Bj51Sk4P.js deleted file mode 100644 index 51036a0fa..000000000 --- a/assets/state-Bj51Sk4P.js +++ /dev/null @@ -1 +0,0 @@ -const t={productData:null,currentVariant:null,selectedOptions:[],variantPrices:{},cartState:"idle",currentMediaId:null};export{t as s}; diff --git a/assets/state-Bwp_aMHz.js b/assets/state-Bwp_aMHz.js new file mode 100644 index 000000000..c202ed658 --- /dev/null +++ b/assets/state-Bwp_aMHz.js @@ -0,0 +1 @@ +const t={productData:null,currentVariant:null,selectedOptions:[],variantPrices:{},cartState:"idle",currentMediaId:null,mediaContextVariantId:null};export{t as s}; diff --git a/assets/sync-DkV0cu7G.js b/assets/sync-DkV0cu7G.js deleted file mode 100644 index 04498afdc..000000000 --- a/assets/sync-DkV0cu7G.js +++ /dev/null @@ -1 +0,0 @@ -import{g as c}from"./variant-picker-DRrQ71gT.js";import{s as e}from"./state-Bj51Sk4P.js";function g(){u(),l(),S(),m(),f(),p(),y()}function u(){const t=document.querySelector('[data-js="product-price"]');if(!t)return;const a=e.variantPrices[String(e.currentVariant.id)];a&&(t.textContent=a)}function l(){const t=document.querySelector('[data-js="product-availability"]');t&&(t.textContent=e.currentVariant.available?"":"Sold out")}function S(){const t=document.querySelectorAll('[data-js="media-item"]'),a=String(e.currentVariant.id),n=e.currentMediaId!==null?String(e.currentMediaId):e.currentVariant.featured_media!==null?String(e.currentVariant.featured_media.id):null;t.forEach(r=>{const i=r.dataset.variantMedia,s=!i,d=i===a;let o;n!==null?o=r.dataset.mediaId===n:o=s||d,o?r.removeAttribute("hidden"):(r.setAttribute("hidden",""),r.querySelector("video")?.pause())}),document.querySelectorAll('[data-js="thumbnail"]').forEach(r=>{const i=r.dataset.variantMedia;!i||i===a?r.removeAttribute("hidden"):r.setAttribute("hidden",""),r.setAttribute("aria-pressed",String(r.dataset.thumbnail===n))})}function f(){const t=document.querySelector('[data-js="add-to-cart"]');if(!t)return;const a={idle:e.currentVariant.available?"Add to cart":"Sold out",loading:"Adding...",success:"Added!",error:"Try again"};t.disabled=!e.currentVariant.available||e.cartState==="loading",t.setAttribute("aria-busy",String(e.cartState==="loading")),t.textContent=a[e.cartState]}function m(){const t=document.querySelector('[data-js="variant-id"]');t&&(t.value=String(e.currentVariant.id))}function p(){const t=document.querySelector('[data-js="cart-status"]');if(!t)return;const a={idle:e.currentVariant.available?"":"This variant is sold out.",loading:"Adding item to cart...",success:"Added to cart.",error:"Could not add to cart. Please try again."};t.textContent=a[e.cartState]}function y(){document.querySelectorAll('[data-js="option-value"]').forEach(t=>{const a=Number(t.dataset.optionPosition)-1,n=t.dataset.optionValue??"",r=e.selectedOptions[a]===n;t.setAttribute("aria-pressed",String(r));const s=c(e.productData.variants,e.selectedOptions,a).has(n);t.setAttribute("aria-disabled",String(!s)),t.disabled=!s}),document.querySelectorAll('[data-js="option-label"]').forEach(t=>{const a=Number(t.dataset.optionLabel)-1;t.textContent=e.selectedOptions[a]??""})}export{g as s}; diff --git a/assets/sync-YStD_kYI.js b/assets/sync-YStD_kYI.js new file mode 100644 index 000000000..47450335e --- /dev/null +++ b/assets/sync-YStD_kYI.js @@ -0,0 +1 @@ +import{g as v}from"./variant-picker-DRrQ71gT.js";import{s as e}from"./state-Bwp_aMHz.js";function m(t,i){return t?t.split(",").some(n=>n.trim()===i):!1}function C(){b(),p(),g(),A(),y(),h(),V()}function b(){const t=document.querySelector('[data-js="product-price"]');if(!t)return;const i=e.variantPrices[String(e.currentVariant.id)];i&&(t.textContent=i)}function p(){const t=document.querySelector('[data-js="product-availability"]');t&&(t.textContent=e.currentVariant.available?"":"Sold out")}function g(){const t=document.querySelectorAll('[data-js="media-item"]');if(t.length===0)return;const i=e.mediaContextVariantId!==null?String(e.mediaContextVariantId):String(e.currentVariant.id),n=e.currentMediaId!==null?String(e.currentMediaId):e.currentVariant.featured_media!==null?String(e.currentVariant.featured_media.id):null,o=n!==null&&Array.from(t).some(a=>a.dataset.mediaId===n)?n:null,r=t[0]?.dataset.mediaId??null;let d=!1;t.forEach(a=>{const s=a.dataset.variantMedia,c=!s,l=m(s,i);let u;o!==null?u=a.dataset.mediaId===o:u=c||l,u?(a.removeAttribute("hidden"),d=!0):(a.setAttribute("hidden",""),a.querySelector("video")?.pause())}),!d&&r&&t.forEach(a=>{a.dataset.mediaId===r?a.removeAttribute("hidden"):(a.setAttribute("hidden",""),a.querySelector("video")?.pause())});const S=d?o:r;document.querySelectorAll('[data-js="thumbnail"]').forEach(a=>{const s=a.dataset.variantMedia,c=!s,l=m(s,i);c||l?a.removeAttribute("hidden"):a.setAttribute("hidden",""),a.setAttribute("aria-pressed",String(a.dataset.thumbnail===S))})}function y(){const t=document.querySelector('[data-js="add-to-cart"]');if(!t)return;const i={idle:e.currentVariant.available?"Add to cart":"Sold out",loading:"Adding...",success:"Added!",error:"Try again"};t.disabled=!e.currentVariant.available||e.cartState==="loading",t.setAttribute("aria-busy",String(e.cartState==="loading")),t.textContent=i[e.cartState]}function A(){const t=document.querySelector('[data-js="variant-id"]');t&&(t.value=String(e.currentVariant.id))}function h(){const t=document.querySelector('[data-js="cart-status"]');if(!t)return;const i={idle:e.currentVariant.available?"":"This variant is sold out.",loading:"",success:"Added to cart.",error:"Could not add to cart. Please try again."};t.textContent=i[e.cartState]}function V(){document.querySelectorAll('[data-js="option-value"]').forEach(t=>{const i=Number(t.dataset.optionPosition)-1,n=t.dataset.optionValue??"",f=e.selectedOptions[i]===n;t.setAttribute("aria-pressed",String(f));const r=v(e.productData.variants,e.selectedOptions,i).has(n);t.setAttribute("aria-disabled",String(!r)),t.disabled=!r}),document.querySelectorAll('[data-js="option-label"]').forEach(t=>{const i=Number(t.dataset.optionLabel)-1;t.textContent=e.selectedOptions[i]??""})}export{C as s}; diff --git a/assets/theme-BHxQweo7.js b/assets/theme-BHxQweo7.js deleted file mode 100644 index c70124fe9..000000000 --- a/assets/theme-BHxQweo7.js +++ /dev/null @@ -1,2 +0,0 @@ -const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["./drawer-HPI4d66D.js","./cart-CmG_L9o1.js"])))=>i.map(i=>d[i]); -const g="modulepreload",v=function(a,o){return new URL(a,o).href},p={},E=function(o,s,u){let e=Promise.resolve();if(s&&s.length>0){let y=function(n){return Promise.all(n.map(l=>Promise.resolve(l).then(d=>({status:"fulfilled",value:d}),d=>({status:"rejected",reason:d}))))};const r=document.getElementsByTagName("link"),c=document.querySelector("meta[property=csp-nonce]"),h=c?.nonce||c?.getAttribute("nonce");e=y(s.map(n=>{if(n=v(n,u),n in p)return;p[n]=!0;const l=n.endsWith(".css"),d=l?'[rel="stylesheet"]':"";if(u)for(let f=r.length-1;f>=0;f--){const m=r[f];if(m.href===n&&(!l||m.rel==="stylesheet"))return}else if(document.querySelector(`link[href="${n}"]${d}`))return;const i=document.createElement("link");if(i.rel=l?"stylesheet":g,l||(i.as="script"),i.crossOrigin="",i.href=n,h&&i.setAttribute("nonce",h),document.head.appendChild(i),l)return new Promise((f,m)=>{i.addEventListener("load",f),i.addEventListener("error",()=>m(new Error(`Unable to preload CSS for ${n}`)))})}))}function t(r){const c=new Event("vite:preloadError",{cancelable:!0});if(c.payload=r,window.dispatchEvent(c),!c.defaultPrevented)throw r}return e.then(r=>{for(const c of r||[])c.status==="rejected"&&t(c.reason);return o().catch(t)})};(function(){const o=document.createElement("link").relList;if(o&&o.supports&&o.supports("modulepreload"))return;for(const e of document.querySelectorAll('link[rel="modulepreload"]'))u(e);new MutationObserver(e=>{for(const t of e)if(t.type==="childList")for(const r of t.addedNodes)r.tagName==="LINK"&&r.rel==="modulepreload"&&u(r)}).observe(document,{childList:!0,subtree:!0});function s(e){const t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),e.crossOrigin==="use-credentials"?t.credentials="include":e.crossOrigin==="anonymous"?t.credentials="omit":t.credentials="same-origin",t}function u(e){if(e.ep)return;e.ep=!0;const t=s(e);fetch(e.href,t)}})();document.addEventListener("DOMContentLoaded",()=>{const a=document.querySelector('[data-js="cart-drawer"]'),o=document.querySelector('[data-js="cart-open"]');!a&&!o||E(async()=>{const{initCartDrawer:s}=await import("./drawer-HPI4d66D.js");return{initCartDrawer:s}},__vite__mapDeps([0,1]),import.meta.url).then(({initCartDrawer:s})=>{s()})}); diff --git a/assets/theme-C1WNKfRD.js b/assets/theme-C1WNKfRD.js new file mode 100644 index 000000000..41d90d021 --- /dev/null +++ b/assets/theme-C1WNKfRD.js @@ -0,0 +1,2 @@ +const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["./drawer-DzmJHKNX.js","./cart-Kw4e-iq4.js","./section-rendering-vVqNxfcB.js"])))=>i.map(i=>d[i]); +const v="modulepreload",w=function(u,o){return new URL(u,o).href},p={},y=function(o,i,a){let e=Promise.resolve();if(i&&i.length>0){let g=function(n){return Promise.all(n.map(l=>Promise.resolve(l).then(d=>({status:"fulfilled",value:d}),d=>({status:"rejected",reason:d}))))};const r=document.getElementsByTagName("link"),s=document.querySelector("meta[property=csp-nonce]"),h=s?.nonce||s?.getAttribute("nonce");e=g(i.map(n=>{if(n=w(n,a),n in p)return;p[n]=!0;const l=n.endsWith(".css"),d=l?'[rel="stylesheet"]':"";if(a)for(let f=r.length-1;f>=0;f--){const m=r[f];if(m.href===n&&(!l||m.rel==="stylesheet"))return}else if(document.querySelector(`link[href="${n}"]${d}`))return;const c=document.createElement("link");if(c.rel=l?"stylesheet":v,l||(c.as="script"),c.crossOrigin="",c.href=n,h&&c.setAttribute("nonce",h),document.head.appendChild(c),l)return new Promise((f,m)=>{c.addEventListener("load",f),c.addEventListener("error",()=>m(new Error(`Unable to preload CSS for ${n}`)))})}))}function t(r){const s=new Event("vite:preloadError",{cancelable:!0});if(s.payload=r,window.dispatchEvent(s),!s.defaultPrevented)throw r}return e.then(r=>{for(const s of r||[])s.status==="rejected"&&t(s.reason);return o().catch(t)})};(function(){const o=document.createElement("link").relList;if(o&&o.supports&&o.supports("modulepreload"))return;for(const e of document.querySelectorAll('link[rel="modulepreload"]'))a(e);new MutationObserver(e=>{for(const t of e)if(t.type==="childList")for(const r of t.addedNodes)r.tagName==="LINK"&&r.rel==="modulepreload"&&a(r)}).observe(document,{childList:!0,subtree:!0});function i(e){const t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),e.crossOrigin==="use-credentials"?t.credentials="include":e.crossOrigin==="anonymous"?t.credentials="omit":t.credentials="same-origin",t}function a(e){if(e.ep)return;e.ep=!0;const t=i(e);fetch(e.href,t)}})();document.addEventListener("DOMContentLoaded",()=>{const u=document.querySelector('[data-js="cart-drawer"]'),o=document.querySelector('[data-js="cart-open"]'),i=document.querySelector('[data-js="search-drawer"]'),a=document.querySelector('[data-js="search-open"]');(u||o)&&y(async()=>{const{initCartDrawer:e}=await import("./drawer-DzmJHKNX.js");return{initCartDrawer:e}},__vite__mapDeps([0,1,2]),import.meta.url).then(({initCartDrawer:e})=>{e()}),(i||a)&&y(async()=>{const{initSearchDrawer:e}=await import("./drawer-BOuNB4a_.js");return{initSearchDrawer:e}},[],import.meta.url).then(({initSearchDrawer:e})=>{e()})}); diff --git a/frontend/entrypoints/ts/cart/drawer.ts b/frontend/entrypoints/ts/cart/drawer.ts index 1f060cda8..c7cf93df6 100644 --- a/frontend/entrypoints/ts/cart/drawer.ts +++ b/frontend/entrypoints/ts/cart/drawer.ts @@ -1,40 +1,13 @@ -import { changeCartLine, getCart, type CartItem, type CartResponse } from '../utils/cart'; +import { changeCartLine } from '../utils/cart'; +import { + applySectionReplace, + fetchSingleSectionHtml, + normalizeSectionsUrl, +} from '../utils/section-rendering'; const FOCUSABLE_SELECTOR = 'a[href], button:not([disabled]), textarea, input, select, [tabindex]:not([tabindex="-1"])'; -function escapeHtml(value: string): string { - return value - .replaceAll('&', '&') - .replaceAll('<', '<') - .replaceAll('>', '>') - .replaceAll('"', '"') - .replaceAll("'", '''); -} - -function formatMoney(cents: number, currency: string): string { - try { - return new Intl.NumberFormat(undefined, { - style: 'currency', - currency, - }).format(cents / 100); - } catch { - return `${(cents / 100).toFixed(2)} ${currency}`; - } -} - -function getImageUrl(item: CartItem): string | null { - return item.image ?? item.featured_image?.url ?? null; -} - -function getItemTitle(item: CartItem): string { - return item.product_title ?? item.title ?? 'Product'; -} - -function lineTotal(item: CartItem): number { - return item.final_line_price ?? item.line_price ?? 0; -} - export function initCartDrawer(): void { const drawerRoot = document.querySelector('[data-js="cart-drawer"]'); if (!drawerRoot) return; @@ -66,16 +39,19 @@ export function initCartDrawer(): void { const overlay = overlayRoot; const panel = panelRoot; const closeButton = closeButtonRoot; - const itemsContainer = itemsContainerRoot; - const emptyState = emptyStateRoot; - const subtotal = subtotalRoot; const status = statusRoot; const error = errorRoot; + const sectionContextUrl = normalizeSectionsUrl(window.location.pathname); + const triggers = document.querySelectorAll('[data-js="cart-open"]'); - const fallbackUrl = drawer.dataset.cartUrl ?? '/cart'; + let itemsContainer: HTMLElement = itemsContainerRoot; + let emptyState: HTMLElement = emptyStateRoot; + let subtotal: HTMLElement = subtotalRoot; let isOpen = false; let isUpdating = false; + let latestMutationId = 0; + let queuedUpdate: { line: number; quantity: number } | null = null; let lastFocused: HTMLElement | null = null; const setStatus = (message: string): void => { @@ -96,89 +72,131 @@ export function initCartDrawer(): void { }); }; - const renderItems = (cart: CartResponse): void => { - const currency = cart.currency || drawer.dataset.currency || 'USD'; - const isEmpty = cart.item_count === 0; - + const updateCount = (count: number): void => { countNodes.forEach((node) => { - node.textContent = String(cart.item_count); - node.hidden = cart.item_count < 1; + node.textContent = String(count); + node.hidden = count < 1; }); + }; - subtotal.textContent = formatMoney(cart.total_price, currency); + const countItemsFromDrawerMarkup = (): number => { + const qtyNodes = itemsContainer.querySelectorAll('[data-js="cart-qty-value"]'); + return Array.from(qtyNodes).reduce((sum, node) => { + const qty = Number(node.textContent ?? '0'); + return sum + (Number.isFinite(qty) ? qty : 0); + }, 0); + }; - if (isEmpty) { - itemsContainer.innerHTML = ''; - emptyState.hidden = false; - setStatus('Your cart is empty.'); - return; + const setLineLoading = (line: number, busy: boolean): void => { + const row = itemsContainer.querySelector(`[data-line="${line}"]`); + if (!row) return; + + row.classList.toggle('animate-pulse', busy); + + const lineOverlay = row.querySelector('[data-js="cart-drawer-line-overlay"]'); + if (!lineOverlay) return; + + if (busy) { + lineOverlay.classList.remove('hidden'); + lineOverlay.classList.add('flex'); + } else { + lineOverlay.classList.add('hidden'); + lineOverlay.classList.remove('flex'); } + }; + + const applyDrawerSectionMarkup = (sectionHtml: string | null | undefined): boolean => { + const result = applySectionReplace(sectionHtml, '[data-js="cart-drawer"]', [ + { key: 'items', current: itemsContainer, selector: '[data-js="cart-items"]' }, + { key: 'empty', current: emptyState, selector: '[data-js="cart-empty"]' }, + { key: 'subtotal', current: subtotal, selector: '[data-js="cart-subtotal"]' }, + ]); - emptyState.hidden = true; - itemsContainer.innerHTML = cart.items - .map((item, index) => { - const imageUrl = getImageUrl(item); - const itemTitle = escapeHtml(getItemTitle(item)); - const variantTitle = item.variant_title ? escapeHtml(item.variant_title) : ''; - - return ` -
  • - - ${ - imageUrl - ? `${itemTitle}` - : '' - } - -
    - ${itemTitle} - ${variantTitle ? `

    ${variantTitle}

    ` : ''} -
    - - ${item.quantity} - - -
    -

    ${formatMoney(lineTotal(item), currency)}

    -
    -
  • - `; - }) - .join(''); + if (!result.ok) return false; + + const nextItems = result.nodes.items; + const nextEmpty = result.nodes.empty; + const nextSubtotal = result.nodes.subtotal; + if (!nextItems || !nextEmpty || !nextSubtotal) return false; + + itemsContainer = nextItems; + emptyState = nextEmpty; + subtotal = nextSubtotal; + updateCount(countItemsFromDrawerMarkup()); + + return true; + }; + + const updateFromSectionResponse = (sectionHtml: string | null | undefined): boolean => { + return applyDrawerSectionMarkup(sectionHtml); }; const refresh = async (): Promise => { setError(''); try { - const cart = await getCart(); - renderItems(cart); + const sectionHtml = await fetchSingleSectionHtml('cart-drawer', sectionContextUrl); + const sectionUpdated = applyDrawerSectionMarkup(sectionHtml); + if (!sectionUpdated) { + throw new Error('Drawer section rendering failed'); + } } catch { - setError('Could not load your cart. Redirecting to cart page...'); - window.location.assign(fallbackUrl); + setError('Could not load your cart right now. Please try again.'); + setStatus('Cart load failed.'); } }; const updateLine = async (line: number, quantity: number): Promise => { - if (isUpdating) return; + const mutationId = ++latestMutationId; isUpdating = true; setError(''); - setStatus('Updating cart...'); setBusyState(true); + setLineLoading(line, true); try { - const cart = await changeCartLine(line, quantity); - renderItems(cart); + const cart = await changeCartLine(line, quantity, { + sections: ['cart-drawer'], + sectionsUrl: sectionContextUrl, + }); + + const sectionUpdated = updateFromSectionResponse(cart.sections?.['cart-drawer']); + + if (!sectionUpdated || mutationId !== latestMutationId) { + throw new Error('Drawer section rendering failed'); + } + + updateCount(cart.item_count); setStatus(quantity === 0 ? 'Item removed.' : 'Cart updated.'); } catch { - setError('Could not update cart. Please try again.'); + setError('Could not refresh cart UI. Please try again.'); setStatus('Cart update failed.'); } finally { - isUpdating = false; - setBusyState(false); + if (mutationId === latestMutationId) { + isUpdating = false; + setBusyState(false); + setLineLoading(line, false); + if (queuedUpdate) { + void flushQueuedUpdates(); + } + } + } + }; + + const flushQueuedUpdates = async (): Promise => { + if (isUpdating) return; + + while (queuedUpdate) { + const nextUpdate = queuedUpdate; + queuedUpdate = null; + await updateLine(nextUpdate.line, nextUpdate.quantity); } }; + const queueLineUpdate = (line: number, quantity: number): void => { + queuedUpdate = { line, quantity }; + void flushQueuedUpdates(); + }; + const focusables = (): HTMLElement[] => Array.from(panel.querySelectorAll(FOCUSABLE_SELECTOR)).filter( (el) => !el.hasAttribute('disabled') && !el.getAttribute('aria-hidden'), @@ -220,6 +238,7 @@ export function initCartDrawer(): void { drawer.hidden = false; drawer.setAttribute('aria-hidden', 'false'); + triggers.forEach((button) => button.setAttribute('aria-expanded', 'true')); document.body.style.overflow = 'hidden'; document.addEventListener('keydown', onKeyDown); @@ -233,6 +252,7 @@ export function initCartDrawer(): void { drawer.hidden = true; drawer.setAttribute('aria-hidden', 'true'); + triggers.forEach((button) => button.setAttribute('aria-expanded', 'false')); document.body.style.overflow = ''; document.removeEventListener('keydown', onKeyDown); @@ -241,11 +261,12 @@ export function initCartDrawer(): void { } } - document.querySelectorAll('[data-js="cart-open"]').forEach((trigger) => { + triggers.forEach((trigger) => { trigger.addEventListener('click', (event) => { event.preventDefault(); openDrawer(trigger); }); + trigger.setAttribute('aria-expanded', 'false'); }); closeButton.addEventListener('click', closeDrawer); @@ -267,19 +288,17 @@ export function initCartDrawer(): void { const currentQty = Number(quantityNode?.textContent ?? '1'); if (actionButton.dataset.js === 'cart-remove') { - void updateLine(line, 0); + queueLineUpdate(line, 0); return; } if (actionButton.dataset.js === 'cart-qty-inc') { - void updateLine(line, currentQty + 1); + queueLineUpdate(line, currentQty + 1); return; } if (actionButton.dataset.js === 'cart-qty-dec') { - void updateLine(line, Math.max(0, currentQty - 1)); + queueLineUpdate(line, Math.max(0, currentQty - 1)); } }); - - void refresh(); } diff --git a/frontend/entrypoints/ts/cart/page.ts b/frontend/entrypoints/ts/cart/page.ts index 62a48a41a..ede726a11 100644 --- a/frontend/entrypoints/ts/cart/page.ts +++ b/frontend/entrypoints/ts/cart/page.ts @@ -1,53 +1,33 @@ -import { changeCartLine, getCart, type CartItem, type CartResponse } from '../utils/cart'; - -function escapeHtml(value: string): string { - return value - .replaceAll('&', '&') - .replaceAll('<', '<') - .replaceAll('>', '>') - .replaceAll('"', '"') - .replaceAll("'", '''); -} - -function formatMoney(cents: number, currency: string): string { - try { - return new Intl.NumberFormat(undefined, { - style: 'currency', - currency, - }).format(cents / 100); - } catch { - return `${(cents / 100).toFixed(2)} ${currency}`; - } -} - -function getImageUrl(item: CartItem): string | null { - return item.image ?? item.featured_image?.url ?? null; -} - -function getItemTitle(item: CartItem): string { - return item.product_title ?? item.title ?? 'Product'; -} - -function lineTotal(item: CartItem): number { - return item.final_line_price ?? item.line_price ?? 0; -} +import { changeCartLine } from '../utils/cart'; +import { + applySectionReplace, + normalizeSectionsUrl, +} from '../utils/section-rendering'; export function initCartPage(): void { const root = document.querySelector('[data-js="cart-page"]'); if (!root) return; - const items = root.querySelector('[data-js="cart-page-items"]'); - const empty = root.querySelector('[data-js="cart-page-empty"]'); - const footer = root.querySelector('[data-js="cart-page-footer"]'); - const subtotal = root.querySelector('[data-js="cart-page-subtotal"]'); + const sectionId = root.dataset.sectionId; + const sectionContextUrl = normalizeSectionsUrl(window.location.pathname); + + const itemsNode = root.querySelector('[data-js="cart-page-items"]'); + const emptyNode = root.querySelector('[data-js="cart-page-empty"]'); + const footerNode = root.querySelector('[data-js="cart-page-footer"]'); + const subtotalNode = root.querySelector('[data-js="cart-page-subtotal"]'); const status = root.querySelector('[data-js="cart-page-status"]'); const error = root.querySelector('[data-js="cart-page-error"]'); const countNodes = document.querySelectorAll('[data-js="cart-count"]'); - if (!items || !empty || !footer || !subtotal || !status || !error) return; + if (!itemsNode || !emptyNode || !footerNode || !subtotalNode || !status || !error || !sectionId) return; + + let items: HTMLElement = itemsNode; + let empty: HTMLElement = emptyNode; + let footer: HTMLElement = footerNode; - const fallbackUrl = '/cart'; let isUpdating = false; + let latestMutationId = 0; + let queuedUpdate: { line: number; quantity: number } | null = null; const setStatus = (message: string): void => { status.textContent = message; @@ -66,85 +46,107 @@ export function initCartPage(): void { }); }; - const render = (cart: CartResponse): void => { - const currency = cart.currency || root.dataset.currency || 'USD'; - const hasItems = cart.item_count > 0; - + const updateCount = (count: number): void => { countNodes.forEach((node) => { - node.textContent = String(cart.item_count); - node.hidden = cart.item_count < 1; + node.textContent = String(count); + node.hidden = count < 1; }); + }; - empty.hidden = hasItems; - items.hidden = !hasItems; - footer.hidden = !hasItems; - subtotal.textContent = formatMoney(cart.total_price, currency); + const setLineLoading = (line: number, busy: boolean): void => { + const row = items.querySelector(`[data-line="${line}"]`); + if (!row) return; - if (!hasItems) { - items.innerHTML = ''; - setStatus('Your cart is empty.'); - return; + row.classList.toggle('animate-pulse', busy); + + const overlay = row.querySelector('[data-js="cart-page-line-overlay"]'); + if (!overlay) return; + + if (busy) { + overlay.classList.remove('hidden'); + overlay.classList.add('flex'); + } else { + overlay.classList.add('hidden'); + overlay.classList.remove('flex'); } + }; + + const applySectionMarkup = (sectionHtml: string | null | undefined): boolean => { + const result = applySectionReplace(sectionHtml, '[data-js="cart-page"]', [ + { key: 'items', current: items, selector: '[data-js="cart-page-items"]' }, + { key: 'empty', current: empty, selector: '[data-js="cart-page-empty"]' }, + { key: 'footer', current: footer, selector: '[data-js="cart-page-footer"]' }, + ]); + + if (!result.ok) return false; - items.innerHTML = cart.items - .map((item, index) => { - const imageUrl = getImageUrl(item); - const title = escapeHtml(getItemTitle(item)); - const variantTitle = item.variant_title ? escapeHtml(item.variant_title) : ''; - - return ` -
  • - - ${imageUrl ? `${title}` : ''} - -
    - ${title} - ${variantTitle ? `

    ${variantTitle}

    ` : ''} -
    - - ${item.quantity} - - -
    -

    ${formatMoney(lineTotal(item), currency)}

    -
    -
  • - `; - }) - .join(''); + const nextItems = result.nodes.items; + const nextEmpty = result.nodes.empty; + const nextFooter = result.nodes.footer; + if (!nextItems || !nextEmpty || !nextFooter) return false; + + items = nextItems; + empty = nextEmpty; + footer = nextFooter; + + return Boolean(footer.querySelector('[data-js="cart-page-subtotal"]')); }; - const refresh = async (): Promise => { - try { - const cart = await getCart(); - render(cart); - } catch { - setError('Could not load cart data. Reloading...'); - window.location.assign(fallbackUrl); - } + const updateFromSectionResponse = (sectionHtml: string | null | undefined): boolean => { + return applySectionMarkup(sectionHtml); }; const updateLine = async (line: number, quantity: number): Promise => { - if (isUpdating) return; - + const mutationId = ++latestMutationId; isUpdating = true; setBusy(true); + setLineLoading(line, true); setError(''); - setStatus('Updating cart...'); try { - const cart = await changeCartLine(line, quantity); - render(cart); + const cart = await changeCartLine(line, quantity, { + sections: [sectionId], + sectionsUrl: sectionContextUrl, + }); + + const sectionUpdated = updateFromSectionResponse(cart.sections?.[sectionId]); + + if (!sectionUpdated || mutationId !== latestMutationId) { + throw new Error('Cart section rendering failed'); + } + + updateCount(cart.item_count); setStatus(quantity === 0 ? 'Item removed.' : 'Cart updated.'); } catch { - setError('Could not update cart. Please try again.'); + setError('Could not refresh cart UI. Please try again.'); setStatus('Cart update failed.'); } finally { - isUpdating = false; - setBusy(false); + if (mutationId === latestMutationId) { + isUpdating = false; + setBusy(false); + setLineLoading(line, false); + if (queuedUpdate) { + void flushQueuedUpdates(); + } + } + } + }; + + const flushQueuedUpdates = async (): Promise => { + if (isUpdating) return; + + while (queuedUpdate) { + const nextUpdate = queuedUpdate; + queuedUpdate = null; + await updateLine(nextUpdate.line, nextUpdate.quantity); } }; + const queueLineUpdate = (line: number, quantity: number): void => { + queuedUpdate = { line, quantity }; + void flushQueuedUpdates(); + }; + root.addEventListener('click', (event) => { const target = event.target as HTMLElement; const actionButton = target.closest( @@ -161,19 +163,17 @@ export function initCartPage(): void { const currentQty = Number(quantityNode?.textContent ?? '1'); if (actionButton.dataset.js === 'cart-page-remove') { - void updateLine(line, 0); + queueLineUpdate(line, 0); return; } if (actionButton.dataset.js === 'cart-page-inc') { - void updateLine(line, currentQty + 1); + queueLineUpdate(line, currentQty + 1); return; } if (actionButton.dataset.js === 'cart-page-dec') { - void updateLine(line, Math.max(0, currentQty - 1)); + queueLineUpdate(line, Math.max(0, currentQty - 1)); } }); - - void refresh(); } diff --git a/frontend/entrypoints/ts/collection.ts b/frontend/entrypoints/ts/collection.ts index 509725a68..5f86d2c79 100644 --- a/frontend/entrypoints/ts/collection.ts +++ b/frontend/entrypoints/ts/collection.ts @@ -1,83 +1,272 @@ -function setLoadStatus(message: string): void { - const status = document.querySelector('[data-js="collection-load-status"]'); - if (!status) return; - status.textContent = message; +const ROOT_SELECTOR = '[data-js="collection-root"]'; + +interface CollectionView { + root: HTMLElement; + sectionId: string; + controls: HTMLFormElement | null; + products: HTMLElement | null; + paginationWrap: HTMLElement | null; + loadStatus: HTMLElement | null; + status: HTMLElement | null; + error: HTMLElement | null; +} + +function queryView(root: HTMLElement): CollectionView { + return { + root, + sectionId: root.dataset.sectionId ?? '', + controls: root.querySelector('[data-js="collection-controls"]'), + products: root.querySelector('[data-js="collection-products"]'), + paginationWrap: root.querySelector('[data-js="collection-pagination-wrap"]'), + loadStatus: root.querySelector('[data-js="collection-load-status"]'), + status: root.querySelector('[data-js="collection-status"]'), + error: root.querySelector('[data-js="collection-error"]'), + }; +} + +function setStatus(view: CollectionView, message: string): void { + if (view.status) { + view.status.textContent = message; + } +} + +function setLoadStatus(view: CollectionView, message: string): void { + if (view.loadStatus) { + view.loadStatus.textContent = message; + } +} + +function setError(view: CollectionView, message: string): void { + if (view.error) { + view.error.textContent = message; + } +} + +function setBusy(view: CollectionView, busy: boolean): void { + view.root.setAttribute('aria-busy', String(busy)); +} + +function toUrl(pathOrUrl: string): URL { + return new URL(pathOrUrl, window.location.origin); +} + +function buildSectionRequestUrl(targetUrl: URL, sectionId: string): string { + const requestUrl = new URL(targetUrl.toString()); + requestUrl.searchParams.set('section_id', sectionId); + return `${requestUrl.pathname}${requestUrl.search}`; } -function resolveNextButton(): HTMLButtonElement | null { - return document.querySelector('[data-js="collection-load-more"]'); +function serializeControls(form: HTMLFormElement): URL { + const actionUrl = toUrl(form.action || window.location.href); + const params = new URLSearchParams(); + const data = new FormData(form); + + data.forEach((value, key) => { + const stringValue = String(value).trim(); + if (stringValue.length > 0) { + params.append(key, stringValue); + } + }); + + actionUrl.search = params.toString(); + return actionUrl; } -async function onLoadMoreClick(button: HTMLButtonElement): Promise { - const nextUrl = button.dataset.nextUrl; - if (!nextUrl || button.disabled) return; +function parseCollectionRoot(html: string): HTMLElement | null { + const parsed = new DOMParser().parseFromString(html, 'text/html'); + return parsed.querySelector(ROOT_SELECTOR); +} + +document.addEventListener('DOMContentLoaded', () => { + const initialRoot = document.querySelector(ROOT_SELECTOR); + if (!initialRoot) return; + + let activeRoot: HTMLElement = initialRoot; - button.disabled = true; - setLoadStatus('Loading more products...'); + let activeView = queryView(activeRoot); + if (!activeView.sectionId) return; - try { - const response = await fetch(nextUrl, { + let activeRequestId = 0; + let activeController: AbortController | null = null; + + const syncActiveView = (nextRoot: HTMLElement): void => { + activeRoot = nextRoot; + activeView = queryView(nextRoot); + }; + + const fetchNextRoot = async (targetUrl: URL, requestId: number): Promise => { + activeController?.abort(); + activeController = new AbortController(); + + const response = await fetch(buildSectionRequestUrl(targetUrl, activeView.sectionId), { + signal: activeController.signal, headers: { 'X-Requested-With': 'XMLHttpRequest', }, }); if (!response.ok) { - throw new Error('Failed to load next collection page'); + throw new Error('Collection section request failed'); } const html = await response.text(); - const parsed = new DOMParser().parseFromString(html, 'text/html'); - const incomingProducts = parsed.querySelector('[data-js="collection-products"]'); - const currentProducts = document.querySelector('[data-js="collection-products"]'); + if (requestId !== activeRequestId) { + throw new DOMException('Stale collection request', 'AbortError'); + } - if (!incomingProducts || !currentProducts) { - throw new Error('Could not parse collection products'); + const nextRoot = parseCollectionRoot(html); + if (!nextRoot) { + throw new Error('Collection section parse failed'); } - incomingProducts.querySelectorAll('[data-js="collection-product-card"]').forEach((card) => { - currentProducts.appendChild(card); - }); + return nextRoot; + }; + + const replaceView = async (targetUrl: URL, pushHistory: boolean): Promise => { + const requestId = ++activeRequestId; + + setBusy(activeView, true); + setError(activeView, ''); + setStatus(activeView, 'Loading products...'); + + try { + const nextRoot = await fetchNextRoot(targetUrl, requestId); + activeRoot.replaceWith(nextRoot); + syncActiveView(nextRoot); - const incomingButton = parsed.querySelector('[data-js="collection-load-more"]'); - if (incomingButton?.dataset.nextUrl) { - button.dataset.nextUrl = incomingButton.dataset.nextUrl; - button.disabled = false; - setLoadStatus('More products loaded.'); + if (pushHistory) { + window.history.pushState({ source: 'collection-replace' }, '', `${targetUrl.pathname}${targetUrl.search}`); + } + + setStatus(activeView, 'Products updated.'); + setLoadStatus(activeView, ''); + } catch (errorValue) { + if (errorValue instanceof DOMException && errorValue.name === 'AbortError') { + return; + } + + setError(activeView, 'Could not update collection right now. Please try again.'); + setStatus(activeView, 'Collection update failed.'); + } finally { + if (requestId === activeRequestId) { + setBusy(activeView, false); + } + } + }; + + const appendProducts = async (nextUrlValue: string): Promise => { + const nextUrl = toUrl(nextUrlValue); + const requestId = ++activeRequestId; + + setBusy(activeView, true); + setError(activeView, ''); + setLoadStatus(activeView, 'Loading more products...'); + + try { + const nextRoot = await fetchNextRoot(nextUrl, requestId); + const nextView = queryView(nextRoot); + + if (!activeView.products || !nextView.products || !activeView.paginationWrap || !nextView.paginationWrap) { + throw new Error('Collection append targets missing'); + } + + const fragment = document.createDocumentFragment(); + nextView.products.querySelectorAll('[data-js="collection-product-card"]').forEach((card) => { + fragment.appendChild(card); + }); + activeView.products.appendChild(fragment); + activeView.paginationWrap.replaceWith(nextView.paginationWrap); + activeView.paginationWrap = nextView.paginationWrap; + + window.history.pushState({ source: 'collection-append' }, '', `${nextUrl.pathname}${nextUrl.search}`); + + setLoadStatus(activeView, 'More products loaded.'); + setStatus(activeView, 'Collection updated.'); + } catch (errorValue) { + if (errorValue instanceof DOMException && errorValue.name === 'AbortError') { + return; + } + + setError(activeView, 'Could not load more products. Please try again.'); + setLoadStatus(activeView, 'Load more failed.'); + } finally { + if (requestId === activeRequestId) { + setBusy(activeView, false); + } + } + }; + + const onRootClick = (event: MouseEvent): void => { + const target = event.target as HTMLElement; + + const loadMoreButton = target.closest('[data-js="collection-load-more"]'); + if (loadMoreButton) { + event.preventDefault(); + const nextUrl = loadMoreButton.dataset.nextUrl; + if (!nextUrl || loadMoreButton.disabled) return; + + loadMoreButton.disabled = true; + loadMoreButton.setAttribute('aria-busy', 'true'); + void appendProducts(nextUrl).finally(() => { + loadMoreButton.disabled = false; + loadMoreButton.setAttribute('aria-busy', 'false'); + }); return; } - button.remove(); - setLoadStatus('All products loaded.'); - } catch { - button.disabled = false; - setLoadStatus('Could not load more products. Please try again.'); - } -} + const clearLink = target.closest('[data-js="collection-clear"]'); + if (clearLink) { + event.preventDefault(); + void replaceView(toUrl(clearLink.href), true); + return; + } + + const filterRemoveLink = target.closest('[data-js="collection-filter-remove"]'); + if (filterRemoveLink) { + event.preventDefault(); + void replaceView(toUrl(filterRemoveLink.href), true); + return; + } -function bindSortControl(): void { - const controls = document.querySelector('[data-js="collection-controls"]'); - const sort = document.querySelector('[data-js="collection-sort"]'); - if (!controls || !sort) return; + const paginationLink = target.closest('[data-js="collection-default-pagination"] a'); + if (paginationLink) { + event.preventDefault(); + void replaceView(toUrl(paginationLink.href), true); + } + }; - sort.addEventListener('change', () => { - controls.requestSubmit(); - }); -} + const onRootChange = (event: Event): void => { + const target = event.target as HTMLElement; + const field = target.closest('input, select'); + if (!field || !activeView.controls) return; -function bindLoadMoreControl(): void { - const button = resolveNextButton(); - if (!button) return; + activeView.controls.requestSubmit(); + }; - button.addEventListener('click', () => { - void onLoadMoreClick(button); + const onRootSubmit = (event: Event): void => { + const form = event.target as HTMLFormElement; + if (!form.matches('[data-js="collection-controls"]')) return; + + event.preventDefault(); + void replaceView(serializeControls(form), true); + }; + + document.addEventListener('click', (event) => { + if (!activeRoot.contains(event.target as Node)) return; + onRootClick(event); }); -} -document.addEventListener('DOMContentLoaded', () => { - const root = document.querySelector('[data-js="collection-root"]'); - if (!root) return; + document.addEventListener('change', (event) => { + if (!activeRoot.contains(event.target as Node)) return; + onRootChange(event); + }); + + document.addEventListener('submit', (event) => { + if (!activeRoot.contains(event.target as Node)) return; + onRootSubmit(event); + }); - bindSortControl(); - bindLoadMoreControl(); + window.addEventListener('popstate', () => { + void replaceView(toUrl(window.location.href), false); + }); }); diff --git a/frontend/entrypoints/ts/product.ts b/frontend/entrypoints/ts/product.ts index 6fef39fa8..2b72828d2 100644 --- a/frontend/entrypoints/ts/product.ts +++ b/frontend/entrypoints/ts/product.ts @@ -18,7 +18,28 @@ function resolveVariantFromURL(productData: ProductData): ProductData['variants' function applyVariantSelection(variant: ProductData['variants'][number]): void { state.currentVariant = variant; state.selectedOptions = [...variant.options]; - state.currentMediaId = variant.featured_media?.id ?? null; + + if (variant.featured_media) { + state.currentMediaId = variant.featured_media.id; + state.mediaContextVariantId = variant.id; + return; + } + + if (state.currentMediaId === null) { + const firstMedia = document.querySelector('[data-js="media-item"]'); + const firstMediaId = Number(firstMedia?.dataset.mediaId ?? ''); + const firstMediaOwnerVariantId = Number((firstMedia?.dataset.variantMedia ?? '').split(',')[0]); + state.currentMediaId = firstMediaId || null; + state.mediaContextVariantId = firstMediaOwnerVariantId || state.mediaContextVariantId; + } +} + +function isOptionClick(event: Event): boolean { + return Boolean((event.target as HTMLElement).closest('[data-js="option-value"]')); +} + +function isThumbnailClick(event: Event): boolean { + return Boolean((event.target as HTMLElement).closest('[data-js="thumbnail"]')); } /** @@ -38,11 +59,31 @@ function init(): void { applyVariantSelection(resolveVariantFromURL(state.productData)); + let hasLoadedRecommendations = false; + const loadRecommendationsOnce = (): void => { + if (hasLoadedRecommendations) return; + hasLoadedRecommendations = true; + loadRecommendations(); + }; + const form = document.querySelector('[data-js="product-form"]'); - form?.addEventListener('click', onOptionClick); - form?.addEventListener('submit', (e) => void onAddToCart(e)); + form?.addEventListener('click', (event) => { + if (isOptionClick(event)) { + loadRecommendationsOnce(); + } + onOptionClick(event); + }); + form?.addEventListener('submit', (e) => { + loadRecommendationsOnce(); + void onAddToCart(e); + }); - document.addEventListener('click', onThumbnailClick); + document.addEventListener('click', (event) => { + if (isThumbnailClick(event)) { + loadRecommendationsOnce(); + } + onThumbnailClick(event); + }); window.addEventListener('popstate', () => { applyVariantSelection(resolveVariantFromURL(state.productData)); state.cartState = 'idle'; @@ -50,7 +91,6 @@ function init(): void { }); syncDOM(); - loadRecommendations(); } document.addEventListener('DOMContentLoaded', init); diff --git a/frontend/entrypoints/ts/product/handlers.ts b/frontend/entrypoints/ts/product/handlers.ts index 76d03e498..32512548a 100644 --- a/frontend/entrypoints/ts/product/handlers.ts +++ b/frontend/entrypoints/ts/product/handlers.ts @@ -24,8 +24,18 @@ export function onOptionClick(e: Event): void { return; } + const previousMediaId = state.currentMediaId; + const previousMediaContextVariantId = state.mediaContextVariantId; + state.currentVariant = variant; - state.currentMediaId = variant.featured_media?.id ?? null; + + if (variant.featured_media) { + state.currentMediaId = variant.featured_media.id; + state.mediaContextVariantId = variant.id; + } else { + state.currentMediaId = previousMediaId; + state.mediaContextVariantId = previousMediaContextVariantId; + } const url = new URL(window.location.href); url.searchParams.set('variant', String(variant.id)); @@ -43,6 +53,12 @@ export function onThumbnailClick(e: Event): void { const mediaId = Number(btn.dataset.thumbnail); if (!mediaId) return; state.currentMediaId = mediaId; + + const mediaOwnerVariantId = Number((btn.dataset.variantMedia ?? '').split(',')[0]); + if (mediaOwnerVariantId) { + state.mediaContextVariantId = mediaOwnerVariantId; + } + syncDOM(); } diff --git a/frontend/entrypoints/ts/product/state.ts b/frontend/entrypoints/ts/product/state.ts index cee31b153..e6069b571 100644 --- a/frontend/entrypoints/ts/product/state.ts +++ b/frontend/entrypoints/ts/product/state.ts @@ -9,4 +9,5 @@ export const state = { variantPrices: {} as Record, cartState: 'idle' as CartState, currentMediaId: null as number | null, + mediaContextVariantId: null as number | null, }; diff --git a/frontend/entrypoints/ts/product/sync.ts b/frontend/entrypoints/ts/product/sync.ts index a1dfb1574..685e7252d 100644 --- a/frontend/entrypoints/ts/product/sync.ts +++ b/frontend/entrypoints/ts/product/sync.ts @@ -1,6 +1,11 @@ import { getAvailableValues } from '../utils/variant-picker'; import { state } from './state'; +function matchesVariantOwner(ownerVariantIds: string | undefined, variantId: string): boolean { + if (!ownerVariantIds) return false; + return ownerVariantIds.split(',').some((id) => id.trim() === variantId); +} + /** Calls all sync helpers in sequence after any state change. */ export function syncDOM(): void { syncPrice(); @@ -30,7 +35,12 @@ function syncAvailability(): void { /** Shows the media item matching `currentMediaId` (or variant featured media), hides all others. */ function syncMedia(): void { const items = document.querySelectorAll('[data-js="media-item"]'); - const currentVariantId = String(state.currentVariant.id); + if (items.length === 0) return; + + const activeMediaContextVariantId = + state.mediaContextVariantId !== null + ? String(state.mediaContextVariantId) + : String(state.currentVariant.id); const targetId = state.currentMediaId !== null @@ -39,41 +49,62 @@ function syncMedia(): void { ? String(state.currentVariant.featured_media.id) : null; + const hasTargetMedia = + targetId !== null && Array.from(items).some((item) => item.dataset.mediaId === targetId); + const effectiveTargetId = hasTargetMedia ? targetId : null; + const fallbackVisibleMediaId = items[0]?.dataset.mediaId ?? null; + + let hasVisibleMedia = false; + items.forEach((item) => { - const ownerVariantId = item.dataset.variantMedia; - const isShared = !ownerVariantId; - const isCurrentVariantMedia = ownerVariantId === currentVariantId; + const ownerVariantIds = item.dataset.variantMedia; + const isShared = !ownerVariantIds; + const isActiveContextMedia = matchesVariantOwner(ownerVariantIds, activeMediaContextVariantId); let isVisible: boolean; - if (targetId !== null) { - isVisible = item.dataset.mediaId === targetId; + if (effectiveTargetId !== null) { + isVisible = item.dataset.mediaId === effectiveTargetId; } else { - // No targeted media: show shared + current variant media + // No targeted media: show shared + active media context // If no variant images exist at all (all isShared), this shows everything — unchanged behaviour - isVisible = isShared || isCurrentVariantMedia; + isVisible = isShared || isActiveContextMedia; } if (isVisible) { item.removeAttribute('hidden'); + hasVisibleMedia = true; } else { item.setAttribute('hidden', ''); item.querySelector('video')?.pause(); } }); + if (!hasVisibleMedia && fallbackVisibleMediaId) { + items.forEach((item) => { + if (item.dataset.mediaId === fallbackVisibleMediaId) { + item.removeAttribute('hidden'); + } else { + item.setAttribute('hidden', ''); + item.querySelector('video')?.pause(); + } + }); + } + + const activeMediaId = hasVisibleMedia ? effectiveTargetId : fallbackVisibleMediaId; + // Thumbnail visibility: shared always visible; variant thumbnails only when active document.querySelectorAll('[data-js="thumbnail"]').forEach((btn) => { - const ownerVariantId = btn.dataset.variantMedia; - const isShared = !ownerVariantId; - const isCurrentVariantMedia = ownerVariantId === currentVariantId; + const ownerVariantIds = btn.dataset.variantMedia; + const isShared = !ownerVariantIds; + const isActiveContextMedia = matchesVariantOwner(ownerVariantIds, activeMediaContextVariantId); - if (isShared || isCurrentVariantMedia) { + if (isShared || isActiveContextMedia) { btn.removeAttribute('hidden'); } else { btn.setAttribute('hidden', ''); } - btn.setAttribute('aria-pressed', String(btn.dataset.thumbnail === targetId)); + btn.setAttribute('aria-pressed', String(btn.dataset.thumbnail === activeMediaId)); }); } @@ -106,7 +137,7 @@ function syncCartStatus(): void { const messages = { idle: state.currentVariant.available ? '' : 'This variant is sold out.', - loading: 'Adding item to cart...', + loading: '', success: 'Added to cart.', error: 'Could not add to cart. Please try again.', }; diff --git a/frontend/entrypoints/ts/search/drawer.ts b/frontend/entrypoints/ts/search/drawer.ts new file mode 100644 index 000000000..568ac46ec --- /dev/null +++ b/frontend/entrypoints/ts/search/drawer.ts @@ -0,0 +1,340 @@ +declare global { + interface Window { + Shopify: { routes: { root: string } }; + } +} + +const FOCUSABLE_SELECTOR = + 'a[href], button:not([disabled]), textarea, input, select, [tabindex]:not([tabindex="-1"])'; + +interface PredictiveItem { + title: string; + url: string; + image?: { url: string; alt?: string | null } | null; +} + +interface PredictivePayload { + resources?: { + results?: { + products?: PredictiveItem[]; + articles?: PredictiveItem[]; + pages?: PredictiveItem[]; + }; + }; +} + +interface SearchGroup { + title: string; + items: PredictiveItem[]; +} + +function debounce void>(fn: T, delay = 250): (...args: Parameters) => void { + let timeoutId: number | null = null; + + return (...args: Parameters) => { + if (timeoutId !== null) { + window.clearTimeout(timeoutId); + } + + timeoutId = window.setTimeout(() => { + fn(...args); + }, delay); + }; +} + +function createResultItem(item: PredictiveItem): HTMLLIElement { + const li = document.createElement('li'); + const anchor = document.createElement('a'); + anchor.href = item.url; + anchor.className = 'flex items-center gap-3 rounded border px-2 py-2 hover:bg-gray-50'; + + if (item.image?.url) { + const img = document.createElement('img'); + img.src = item.image.url; + img.alt = item.image.alt ?? item.title; + img.className = 'h-10 w-10 object-cover'; + anchor.appendChild(img); + } + + const title = document.createElement('span'); + title.className = 'text-sm'; + title.textContent = item.title; + anchor.appendChild(title); + + li.appendChild(anchor); + return li; +} + +function createGroupNode(group: SearchGroup): HTMLElement { + const wrapper = document.createElement('section'); + const heading = document.createElement('h3'); + heading.className = 'mb-2 text-xs font-semibold uppercase tracking-wide opacity-70'; + heading.textContent = group.title; + + const list = document.createElement('ul'); + list.className = 'space-y-1'; + group.items.forEach((item) => { + list.appendChild(createResultItem(item)); + }); + + wrapper.appendChild(heading); + wrapper.appendChild(list); + return wrapper; +} + +export function initSearchDrawer(): void { + const drawerNode = document.querySelector('[data-js="search-drawer"]'); + if (!drawerNode) return; + + const overlay = drawerNode.querySelector('[data-js="search-drawer-overlay"]'); + const panel = drawerNode.querySelector('[data-js="search-drawer-panel"]'); + const closeButton = drawerNode.querySelector('[data-js="search-close"]'); + const form = drawerNode.querySelector('[data-js="search-drawer-form"]'); + const input = drawerNode.querySelector('[data-js="search-drawer-input"]'); + const status = drawerNode.querySelector('[data-js="search-drawer-status"]'); + const error = drawerNode.querySelector('[data-js="search-drawer-error"]'); + const loader = drawerNode.querySelector('[data-js="search-drawer-loader"]'); + const empty = drawerNode.querySelector('[data-js="search-drawer-empty"]'); + const groups = drawerNode.querySelector('[data-js="search-drawer-groups"]'); + + if (!overlay || !panel || !closeButton || !form || !input || !status || !error || !loader || !empty || !groups) { + return; + } + + const drawer = drawerNode; + const inputField = input; + + const triggers = document.querySelectorAll('[data-js="search-open"]'); + + let isOpen = false; + let lastFocused: HTMLElement | null = null; + let activeController: AbortController | null = null; + let latestRequestId = 0; + + const setStatus = (message: string): void => { + status.textContent = message; + }; + + const setError = (message: string): void => { + error.textContent = message; + }; + + const setBusy = (busy: boolean): void => { + panel.setAttribute('aria-busy', String(busy)); + if (busy) { + loader.classList.remove('hidden'); + loader.classList.add('flex'); + } else { + loader.classList.add('hidden'); + loader.classList.remove('flex'); + } + }; + + const setExpanded = (expanded: boolean): void => { + inputField.setAttribute('aria-expanded', String(expanded)); + }; + + const clearResults = (message: string): void => { + groups.replaceChildren(); + empty.textContent = message; + empty.classList.remove('hidden'); + }; + + const focusables = (): HTMLElement[] => + Array.from(panel.querySelectorAll(FOCUSABLE_SELECTOR)).filter( + (el) => !el.hasAttribute('disabled') && !el.getAttribute('aria-hidden'), + ); + + const onKeyDown = (event: KeyboardEvent): void => { + if (!isOpen) return; + + if (event.key === 'Escape') { + event.preventDefault(); + closeDrawer(); + return; + } + + if (event.key !== 'Tab') return; + + const nodes = focusables(); + if (nodes.length === 0) return; + + const first = nodes[0]; + const last = nodes[nodes.length - 1]; + + if (event.shiftKey && document.activeElement === first) { + event.preventDefault(); + last.focus(); + return; + } + + if (!event.shiftKey && document.activeElement === last) { + event.preventDefault(); + first.focus(); + } + }; + + function openDrawer(trigger?: HTMLElement): void { + if (isOpen) return; + isOpen = true; + lastFocused = trigger ?? (document.activeElement instanceof HTMLElement ? document.activeElement : null); + + drawer.hidden = false; + drawer.setAttribute('aria-hidden', 'false'); + triggers.forEach((button) => button.setAttribute('aria-expanded', 'true')); + document.body.style.overflow = 'hidden'; + setExpanded(true); + document.addEventListener('keydown', onKeyDown); + + inputField.focus(); + } + + function closeDrawer(): void { + if (!isOpen) return; + isOpen = false; + + activeController?.abort(); + activeController = null; + + drawer.hidden = true; + drawer.setAttribute('aria-hidden', 'true'); + triggers.forEach((button) => button.setAttribute('aria-expanded', 'false')); + document.body.style.overflow = ''; + setExpanded(false); + setBusy(false); + document.removeEventListener('keydown', onKeyDown); + + if (lastFocused) { + lastFocused.focus(); + } + } + + const renderGroups = (resultGroups: SearchGroup[]): void => { + groups.replaceChildren(); + + if (resultGroups.length === 0) { + clearResults('No quick matches. Press Enter for full results.'); + setStatus('No quick matches.'); + return; + } + + const fragment = document.createDocumentFragment(); + resultGroups.forEach((group) => { + fragment.appendChild(createGroupNode(group)); + }); + groups.appendChild(fragment); + + empty.classList.add('hidden'); + + const total = resultGroups.reduce((sum, group) => sum + group.items.length, 0); + setStatus(`${total} quick matches`); + }; + + const fetchPredictiveResults = async (term: string): Promise => { + if (!isOpen) return; + + if (term.length < 2) { + activeController?.abort(); + activeController = null; + setError(''); + setStatus('Type at least 2 characters.'); + setBusy(false); + clearResults('Start typing to see quick results.'); + return; + } + + activeController?.abort(); + activeController = new AbortController(); + const requestId = ++latestRequestId; + + setBusy(true); + setError(''); + setStatus('Searching...'); + + const params = new URLSearchParams(); + params.set('q', term); + params.set('resources[type]', 'product,page,article'); + params.set('resources[limit]', '6'); + params.set('resources[options][unavailable_products]', 'last'); + + const url = `${window.Shopify.routes.root}search/suggest.json?${params.toString()}`; + + try { + const response = await fetch(url, { + signal: activeController.signal, + headers: { + Accept: 'application/json', + 'X-Requested-With': 'XMLHttpRequest', + }, + }); + + if (!response.ok) { + throw new Error('Predictive endpoint unavailable'); + } + + const payload = (await response.json()) as PredictivePayload; + + if (requestId !== latestRequestId) { + return; + } + + const resultGroups: SearchGroup[] = []; + const products = payload.resources?.results?.products ?? []; + const pages = payload.resources?.results?.pages ?? []; + const articles = payload.resources?.results?.articles ?? []; + + if (products.length > 0) { + resultGroups.push({ title: 'Products', items: products.slice(0, 6) }); + } + + if (pages.length > 0) { + resultGroups.push({ title: 'Pages', items: pages.slice(0, 6) }); + } + + if (articles.length > 0) { + resultGroups.push({ title: 'Articles', items: articles.slice(0, 6) }); + } + + renderGroups(resultGroups); + } catch (errorValue) { + if (errorValue instanceof DOMException && errorValue.name === 'AbortError') { + return; + } + + if (requestId !== latestRequestId) { + return; + } + + clearResults('Quick results unavailable. Press Enter for full results.'); + setError('Predictive search is unavailable. Full search still works.'); + setStatus('Predictive search unavailable.'); + } finally { + if (requestId === latestRequestId) { + setBusy(false); + } + } + }; + + const onInput = debounce((value: string) => { + void fetchPredictiveResults(value.trim()); + }); + + triggers.forEach((trigger) => { + trigger.setAttribute('aria-expanded', 'false'); + trigger.addEventListener('click', (event) => { + event.preventDefault(); + openDrawer(trigger); + }); + }); + + closeButton.addEventListener('click', closeDrawer); + overlay.addEventListener('click', closeDrawer); + + inputField.addEventListener('input', () => { + onInput(inputField.value); + }); + + form.addEventListener('submit', () => { + closeDrawer(); + }); +} diff --git a/frontend/entrypoints/ts/theme.ts b/frontend/entrypoints/ts/theme.ts index 9ff551d8a..ac400d559 100644 --- a/frontend/entrypoints/ts/theme.ts +++ b/frontend/entrypoints/ts/theme.ts @@ -3,9 +3,18 @@ import 'vite/modulepreload-polyfill'; document.addEventListener('DOMContentLoaded', () => { const hasCartDrawer = document.querySelector('[data-js="cart-drawer"]'); const hasCartTrigger = document.querySelector('[data-js="cart-open"]'); - if (!hasCartDrawer && !hasCartTrigger) return; + const hasSearchDrawer = document.querySelector('[data-js="search-drawer"]'); + const hasSearchTrigger = document.querySelector('[data-js="search-open"]'); - void import('./cart/drawer').then(({ initCartDrawer }) => { - initCartDrawer(); - }); + if (hasCartDrawer || hasCartTrigger) { + void import('./cart/drawer').then(({ initCartDrawer }) => { + initCartDrawer(); + }); + } + + if (hasSearchDrawer || hasSearchTrigger) { + void import('./search/drawer').then(({ initSearchDrawer }) => { + initSearchDrawer(); + }); + } }); diff --git a/frontend/entrypoints/ts/utils/cart.ts b/frontend/entrypoints/ts/utils/cart.ts index 3aaf11ccd..957f33b97 100644 --- a/frontend/entrypoints/ts/utils/cart.ts +++ b/frontend/entrypoints/ts/utils/cart.ts @@ -27,6 +27,12 @@ export interface CartResponse { total_price: number; currency: string; items: CartItem[]; + sections?: Record; +} + +export interface CartChangeOptions { + sections?: string[] | string; + sectionsUrl?: string; } async function parseCartError(res: Response): Promise { @@ -34,6 +40,11 @@ async function parseCartError(res: Response): Promise { return new Error(err.description ?? 'Cart request failed'); } +function normalizeSectionsUrl(url: string): string { + if (!url) return '/'; + return url.startsWith('/') ? url : `/${url}`; +} + export async function getCart(): Promise { const res = await fetch(`${window.Shopify.routes.root}cart.js`, { headers: { @@ -49,7 +60,26 @@ export async function getCart(): Promise { return (await res.json()) as CartResponse; } -export async function changeCartLine(line: number, quantity: number): Promise { +export async function changeCartLine( + line: number, + quantity: number, + options: CartChangeOptions = {}, +): Promise { + const payload: { + line: number; + quantity: number; + sections?: string[] | string; + sections_url?: string; + } = { line, quantity }; + + if (options.sections) { + payload.sections = options.sections; + } + + if (options.sectionsUrl) { + payload.sections_url = normalizeSectionsUrl(options.sectionsUrl); + } + const res = await fetch(`${window.Shopify.routes.root}cart/change.js`, { method: 'POST', headers: { @@ -57,7 +87,7 @@ export async function changeCartLine(line: number, quantity: number): Promise; +} + +export function applySectionReplace( + sectionHtml: string | null | undefined, + rootSelector: string, + targets: SectionReplaceTarget[], +): SectionReplaceResult { + if (!sectionHtml) { + return { ok: false, nodes: {} }; + } + + const parsed = new DOMParser().parseFromString(sectionHtml, 'text/html'); + const nextRoot = parsed.querySelector(rootSelector); + + if (!nextRoot) { + return { ok: false, nodes: {} }; + } + + const nextNodes: Record = {}; + + for (const target of targets) { + const nextNode = nextRoot.querySelector(target.selector); + if (!nextNode && target.required !== false) { + return { ok: false, nodes: {} }; + } + + if (nextNode) { + nextNodes[target.key] = nextNode; + } + } + + for (const target of targets) { + const nextNode = nextNodes[target.key]; + if (nextNode) { + target.current.replaceWith(nextNode); + } + } + + return { ok: true, nodes: nextNodes }; +} + +export function normalizeSectionsUrl(url: string): string { + if (!url) return '/'; + return url.startsWith('/') ? url : `/${url}`; +} + +export async function fetchSingleSectionHtml(sectionId: string, sectionsUrl: string): Promise { + const normalizedUrl = normalizeSectionsUrl(sectionsUrl); + const url = new URL(normalizedUrl, window.location.origin); + url.searchParams.set('section_id', sectionId); + + const response = await fetch(url.pathname + url.search, { + headers: { + 'X-Requested-With': 'XMLHttpRequest', + }, + }); + + if (!response.ok) { + throw new Error('Section rendering request failed'); + } + + return response.text(); +} diff --git a/layout/theme.liquid b/layout/theme.liquid index 8504ce0d1..d16698bbe 100644 --- a/layout/theme.liquid +++ b/layout/theme.liquid @@ -39,7 +39,8 @@ {{ content_for_layout }} - {% render 'cart-drawer' %} + {% section 'cart-drawer' %} + {% section 'search-drawer' %} {% sections 'footer-group' %} diff --git a/package.json b/package.json index 17c227090..0571eccba 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,6 @@ "shopify:push": "shopify theme push", "typecheck": "tsc --noEmit", "vite:dev": "vite", - "vite:build": "vite build", - "smoke": "./smoke-checklist.sh" + "vite:build": "vite build" } } diff --git a/sections/cart-drawer.liquid b/sections/cart-drawer.liquid new file mode 100644 index 000000000..09af78934 --- /dev/null +++ b/sections/cart-drawer.liquid @@ -0,0 +1,81 @@ + + +{% schema %} +{ + "name": "Cart drawer", + "settings": [] +} +{% endschema %} diff --git a/sections/cart.liquid b/sections/cart.liquid index 807b5d2d0..1090d05e5 100644 --- a/sections/cart.liquid +++ b/sections/cart.liquid @@ -7,9 +7,9 @@

    {{ 'cart.title' | t }}

    -
    -

    - +
    +

    +
    0 %}hidden{% endif %}>

    Your cart is empty.

    @@ -18,7 +18,10 @@
      {% for item in cart.items %} -
    • +
    • + {% if item.image %} {% render 'image', image: item.image, width: 160, class: 'h-full w-full object-cover' %} diff --git a/sections/collection.liquid b/sections/collection.liquid index cc479f776..492ad7d94 100644 --- a/sections/collection.liquid +++ b/sections/collection.liquid @@ -8,8 +8,11 @@

      {{ collection.title }}

      {% paginate collection.products by 20 %} -
      -
      +
      +

      + + +
      {% if collection.filters.size > 0 %} @@ -97,14 +106,14 @@
      {% for filter in collection.filters %} {% for filter_value in filter.active_values %} - + {{ filter_value.label }} {% endfor %} {% if filter.type == 'price_range' %} {% if filter.min_value.value or filter.max_value.value %} - + {{ filter.min_value.value | default: 0 | money_without_currency }} - {{ filter.max_value.value | default: filter.range_max | money_without_currency }} @@ -137,16 +146,19 @@ {% endfor %}
      - {% if paginate.next %} -
      +
      + {% if paginate.next %} -

      -
      - {% endif %} + {% endif %} - {{ paginate | default_pagination }} +

      + +
      + {{ paginate | default_pagination }} +
      +
      {% endpaginate %} diff --git a/sections/header.liquid b/sections/header.liquid index 613f1992b..8bfc5ba36 100644 --- a/sections/header.liquid +++ b/sections/header.liquid @@ -1,4 +1,4 @@ -
      +

      {{ shop.name | link_to: routes.root_url }}

      @@ -16,8 +16,29 @@ {% endif %} - - {{ cart.item_count }} + + {{ 'search.title' | t }} + + + + {{ 'cart.title' | t }} diff --git a/sections/product.liquid b/sections/product.liquid index 67549820c..0fad29a5c 100644 --- a/sections/product.liquid +++ b/sections/product.liquid @@ -14,17 +14,20 @@ {{- '' }}
      {% for media in product.media %} - {%- assign media_variant_id = nil -%} + {%- assign media_variant_ids = '' -%} {%- for variant in product.variants -%} {%- if variant.featured_media.id == media.id -%} - {%- assign media_variant_id = variant.id -%} - {%- break -%} + {%- if media_variant_ids == '' -%} + {%- assign media_variant_ids = variant.id | append: '' -%} + {%- else -%} + {%- assign media_variant_ids = media_variant_ids | append: ',' | append: variant.id -%} + {%- endif -%} {%- endif -%} {%- endfor -%}
      {% case media.media_type %} @@ -45,18 +48,21 @@ {% if product.media.size > 1 %}
      {% for media in product.media %} - {%- assign media_variant_id = nil -%} + {%- assign media_variant_ids = '' -%} {%- for variant in product.variants -%} {%- if variant.featured_media.id == media.id -%} - {%- assign media_variant_id = variant.id -%} - {%- break -%} + {%- if media_variant_ids == '' -%} + {%- assign media_variant_ids = variant.id | append: '' -%} + {%- else -%} + {%- assign media_variant_ids = media_variant_ids | append: ',' | append: variant.id -%} + {%- endif -%} {%- endif -%} {%- endfor -%} +
      + + + + + + + +

      + + +
      + +

      Start typing to see quick results.

      +
      +
      + +
      + +{% schema %} +{ + "name": "Search drawer", + "settings": [] +} +{% endschema %} diff --git a/smoke-checklist.sh b/smoke-checklist.sh deleted file mode 100755 index a390ae367..000000000 --- a/smoke-checklist.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -echo "Running smoke checklist..." - -echo "1) Typecheck" -bun run typecheck - -echo "2) Build" -bun run build - -echo "3) Theme check (optional local)" -if command -v theme-check >/dev/null 2>&1; then - theme-check -elif command -v shopify >/dev/null 2>&1; then - shopify theme check -else - echo "theme-check not available locally; rely on CI Theme Check job" -fi - -echo "4) Generated asset consistency" -git diff --exit-code -- assets snippets/vite-tag.liquid - -echo "Smoke checklist passed." diff --git a/snippets/cart-drawer.liquid b/snippets/cart-drawer.liquid deleted file mode 100644 index 7832058d6..000000000 --- a/snippets/cart-drawer.liquid +++ /dev/null @@ -1,45 +0,0 @@ - diff --git a/snippets/vite-tag.liquid b/snippets/vite-tag.liquid index 6d1b82fc3..ceb43e4db 100644 --- a/snippets/vite-tag.liquid +++ b/snippets/vite-tag.liquid @@ -7,7 +7,7 @@ assign path = entry | replace: '@ts/', 'ts/' | replace: '@css/', 'css/' | replace: '~/', '../' | replace: '@/', '../' %} {% if path == "/frontend/entrypoints/css/main.css" or path == "css/main.css" %} - {{ 'main-CZyZ8laC.css' | asset_url | split: '?' | first | stylesheet_tag: preload: preload_stylesheet }} + {{ 'main-BF2QWjU5.css' | asset_url | split: '?' | first | stylesheet_tag: preload: preload_stylesheet }} {% elsif path == "/frontend/entrypoints/ts/404.ts" or path == "ts/404.ts" %} {% elsif path == "/frontend/entrypoints/ts/article.ts" or path == "ts/article.ts" %} @@ -15,17 +15,20 @@ {% elsif path == "/frontend/entrypoints/ts/blog.ts" or path == "ts/blog.ts" %} {% elsif path == "/frontend/entrypoints/ts/cart.ts" or path == "ts/cart.ts" %} - - - + + + + {% elsif path == "/frontend/entrypoints/ts/cart/drawer.ts" or path == "ts/cart/drawer.ts" %} - - + + + {% elsif path == "/frontend/entrypoints/ts/cart/page.ts" or path == "ts/cart/page.ts" %} - - + + + {% elsif path == "/frontend/entrypoints/ts/collection.ts" or path == "ts/collection.ts" %} - + {% elsif path == "/frontend/entrypoints/ts/gift-card.ts" or path == "ts/gift-card.ts" %} {% elsif path == "/frontend/entrypoints/ts/index.ts" or path == "ts/index.ts" %} @@ -37,33 +40,37 @@ {% elsif path == "/frontend/entrypoints/ts/password.ts" or path == "ts/password.ts" %} {% elsif path == "/frontend/entrypoints/ts/product.ts" or path == "ts/product.ts" %} - - - - + + + + - + {% elsif path == "/frontend/entrypoints/ts/product/handlers.ts" or path == "ts/product/handlers.ts" %} - + - - - + + + {% elsif path == "/frontend/entrypoints/ts/product/recommendations.ts" or path == "ts/product/recommendations.ts" %} {% elsif path == "/frontend/entrypoints/ts/product/state.ts" or path == "ts/product/state.ts" %} - + {% elsif path == "/frontend/entrypoints/ts/product/sync.ts" or path == "ts/product/sync.ts" %} - + - + {% elsif path == "/frontend/entrypoints/ts/search.ts" or path == "ts/search.ts" %} +{% elsif path == "/frontend/entrypoints/ts/search/drawer.ts" or path == "ts/search/drawer.ts" %} + {% elsif path == "/frontend/entrypoints/ts/theme.ts" or path == "ts/theme.ts" %} - + {% elsif path == "/frontend/entrypoints/ts/utils/cart.ts" or path == "ts/utils/cart.ts" %} - + +{% elsif path == "/frontend/entrypoints/ts/utils/section-rendering.ts" or path == "ts/utils/section-rendering.ts" %} + {% elsif path == "/frontend/entrypoints/ts/utils/variant-picker.ts" or path == "ts/utils/variant-picker.ts" %} {% endif %} diff --git a/todo.md b/todo.md index 35246a7b1..23cf11dea 100644 --- a/todo.md +++ b/todo.md @@ -1,137 +1,216 @@ -# TODO — Shopify Skeleton Theme + Vite (Post Pre-Core) +# TODO - Shopify Skeleton Theme (Docs-First Optimization) -## 0) Status -- [x] Pre-core architecture baseline completed -- [x] Vite + Shopify integration stabilized -- [x] TS entrypoints structure finalized (`frontend/entrypoints/css` + `frontend/entrypoints/ts`) -- [x] Branch-linked deploy workflow documented -- [x] README and CLAUDE docs updated +## North Star +- [ ] Keep this starter aligned with Shopify documentation first +- [ ] Use Section Rendering/Bundled Section Rendering as the default dynamic UI pattern +- [ ] Keep customization easy for future themes (logic reusable, markup editable in Liquid) --- -## 1) Repository Hygiene (final pass before core features) -- [x] Review tracked/untracked files and confirm what must be committed -- [x] Confirm `.claude/` policy (team-shared vs local-only) -- [x] Confirm `.agents/` policy (likely local-only) -- [x] Ensure `.env` and `shopify.theme.toml` are ignored -- [x] Ensure only `.env.example` and `example.shopify.theme.toml` are committed +## Core Architecture Principles (Non-negotiable) +- [x] No HTML template strings in TS for cart and minicart flows +- [x] Cart and minicart UI updates driven by Section Rendering responses +- [x] No API calls without user interaction +- [x] Loading states without layout shift (pulse + overlay) +- [x] Document the "TS logic only, Liquid markup only" rule in contributor docs + +--- + +## 1) P0 - Section Rendering Foundation + +### Contract and utilities +- [x] Create a shared utility for section parsing/replacement (avoid duplicated logic in cart/drawer) +- [x] Standardize null-section fallback strategy (Shopify docs: sections can return `null`) +- [x] Standardize bundled request payload (`sections`, `sections_url`) across all cart mutations + +### Robustness +- [x] Add request sequencing guard for rapid quantity interactions (prevent out-of-order UI state) +- [x] Ensure cart count, subtotal, and item list always remain synchronized after updates +- [x] Validate graceful fallback when section fetch fails (recoverable error + safe redirect only when necessary) Acceptance: -- [x] Working tree clean and intentional -- [x] No sensitive/local files tracked +- [x] Every cart UI update path uses section rendering response as source of truth +- [x] Rapid interactions do not produce stale UI +- [x] `null`/failed section responses are handled predictably --- -## 2) Quality Gates & CI -- [x] Verify CI runs: `typecheck`, `vite:build`, `theme-check` -- [x] Confirm CI runs on `push` + `pull_request` -- [x] Confirm no regressions in workflow after simplifications - -Step 2 closure checklist: -- [x] Open a recent PR and confirm both checks are green: - - `Typecheck and Build` - - `Theme Check` -- [x] Enable branch protection on `main` and `staging` with required checks: - - `Typecheck and Build` - - `Theme Check` -- [x] Block merge when checks are pending/failing -- [ ] (Optional) Disable direct push to `main` +## 2) P0 - Shopify Docs Parity + +### Cart API parity +- [x] Audit `cart/change.js` usage against Shopify Ajax Cart docs +- [x] Confirm locale-aware URL handling for all fetches +- [x] Confirm `sections_url` is always valid and starts with `/` + +### Section Rendering parity +- [x] Validate `?section_id=` flow for single-section refresh where applicable +- [x] Validate bundled sections behavior for cart mutations +- [x] Add explicit handling for sections that return `null` while response status is `200` Acceptance: -- [x] All checks green on PR -- [x] Failing checks block merge +- [x] Implementation matches documented Shopify patterns for cart and section rendering +- [x] Edge cases from docs are covered in code paths --- -## 3) Dev/Build Workflow Consistency -- [x] Verify `bun run dev` (local) works consistently -- [x] Verify `bun run dev:remote` works for Shopify-domain preview (tunnel) -- [x] Verify `bun run build` generates all expected assets and `vite-tag` mappings -- [x] Confirm team uses full domain in `.env` (`*.myshopify.com`) +## 3) P0 - PDP Media Reliability + +### Variant/media behavior +- [x] Fix regression: selected color + size change must not hide/remove first image +- [x] Change media only when target variant has `featured_media` +- [x] If variant has no dedicated media, keep current color media context + +### Validation +- [x] Validate `data-variant-media` mapping for shared media across variants +- [x] Stress test rapid option changes (no blank gallery, no wrong image jumps) +- [x] Compare with Dawn behavior (`assets/product-info.js`, `snippets/product-media-gallery.liquid`) Acceptance: -- [x] Local + remote preview both reliable -- [x] No CORS/PNA blockers in normal flow +- [x] No media disappearance on size-only changes +- [x] Color/media transitions remain consistent and predictable --- -## 4) Block 3 — Core Features Rollout +## 4) P1 - PLP (Collection) with Same Mindset -### Phase 1 — PDP (Product) -- [x] Audit current PDP markup and data attributes -- [x] Implement variant selection state logic in `ts/product.ts` -- [x] Sync selected variant with URL and form inputs -- [x] Update price/media state on variant change (if media mapping exists) -- [x] Add add-to-cart UX states (loading, success, error) -- [x] Handle unavailable / sold-out variants correctly +### State integrity +- [x] Keep filters/sort fully reflected in URL params +- [x] Keep browser history behavior stable and reversible +- [x] Ensure clear/reset state is deterministic + +### UX and performance +- [x] Keep loading/empty/error states explicit without CLS regressions +- [x] Reduce unnecessary DOM work during filter/sort interactions +- [x] Compare lifecycle behavior with Dawn (`assets/facets.js`) Acceptance: -- [x] Variant change updates UI correctly -- [x] Add-to-cart works and shows clear status -- [x] No JS leakage outside PDP +- [x] PLP interactions are stable across refresh/back/forward +- [ ] Mobile filter UX has no regressions -### Phase 2 — Cart / Drawer -- [x] Implement drawer open/close and focus handling -- [x] Quantity update/remove flows with loading states -- [x] Empty cart state UX -- [x] Error handling and resilience +--- -Acceptance: -- [x] Keyboard accessible drawer behavior -- [x] Cart updates reliable with clear feedback +## 5) P1 - Predictive Search Drawer (Docs-First) -### Phase 3 — Collection (PLP) -- [x] Implement sort behavior -- [x] Implement filters (base behavior first) -- [x] Add optional progressive loading only if needed +### UX and interaction model +- [x] Add search trigger in header (`data-js="search-open"`) with drawer behavior aligned to cart (open/close/overlay/ESC/focus trap) +- [x] Open search as right-side aside with inline search field and predictive results panel +- [x] Keep close behavior consistent across close button, overlay click, and Escape +- [x] Restore focus to trigger on drawer close -Acceptance: -- [x] Filters/sort stable and reversible -- [x] PLP JS isolated to collection templates +### Shopify docs parity (Ajax Predictive Search) +- [x] Use `/search/suggest.json` with locale-aware base URL (`window.Shopify.routes.root`) +- [x] Use documented resources params (`resources[type]`, `resources[limit]`, `resources[options][unavailable_products]`) +- [x] Keep graceful fallback when predictive endpoint is unavailable (full search submit still works) +- [x] Ensure no predictive API request before user interaction (drawer open + input length threshold) + +### Performance and resilience +- [x] Debounce input and cancel stale requests (`AbortController`) +- [x] Prevent out-of-order render on rapid typing +- [x] Keep loading/error states without layout shift (reserved result area + subtle loader) +- [x] Keep drawer fully functional on mobile and desktop -### Phase 4 — Search -- [x] Implement predictive search UI state -- [x] Implement results state handling -- [x] Graceful fallback when predictive endpoint unavailable +### Starter customization surface +- [x] Keep markup in Liquid and behavior in TS (same cart/minicart architecture rule) +- [x] Define stable data attributes contract for search drawer elements +- [x] Document safe customization points (layout, item card markup, result grouping) Acceptance: -- [x] Search interactions stable -- [x] No cross-template JS side effects +- [x] Search drawer opens/closes accessibly and predictably +- [x] Predictive results are fast, cancellable, and stable under rapid input +- [x] Full search remains available as fallback path +- [x] No API calls happen before user interaction --- -## 5) Performance & Loading Validation -- [x] Capture baseline metrics (Home, PDP, PLP): LCP, CLS, JS payload -- [x] Verify each template loads only `ts/theme.ts` + its own entrypoint -- [x] Add dynamic imports for heavy optional logic where useful +## 6) P1 - Accessibility and UX Hardening + +- [x] Verify keyboard-first flows for cart page, minicart dialog, and PDP ATC +- [x] Verify live region messaging for success/error/cart updates +- [x] Verify focus restore paths after dialog close and failed actions +- [x] Keep loading communication visual + accessible without visible status text shifts Acceptance: -- [x] No regressions vs baseline -- [x] Template-specific payload discipline maintained +- [x] Core interactive flows are accessible with keyboard and screen readers --- -## 6) Documentation Finalization -- [x] Keep README aligned with actual scripts and branch-linked workflow -- [x] Keep CLAUDE.md aligned with architecture and conventions -- [x] Add short “how to start new feature branch” section (optional) +## 7) P1 - Starter Customization Surface + +### Theme-extensible design +- [x] Document stable data attributes used by TS modules (public contract) +- [x] Document which Liquid blocks are safe to customize without breaking logic +- [x] Keep behavior modules isolated so teams can swap markup/styles safely + +### Documentation updates +- [x] Update `CLAUDE.md` with docs-first section-rendering conventions +- [x] Add README section: "Customization without breaking core cart flows" +- [x] Add `AGENTS.md` guide for generic AI contributors Acceptance: -- [x] New team member can run project in <10 minutes +- [x] New theme implementations can customize markup/styles without changing core JS logic --- -## 7) Git/Release Workflow -- [x] Enforce branch model: `feat/* -> staging -> main` -- [x] Before merging to `staging`/`main`, always run `bun run build` -- [x] Commit generated artifacts for branch-linked Shopify themes +## 8) Quality Gates (Continuous) + +- [x] `bun run typecheck` +- [x] `bun run build` +- [ ] `theme-check` +- [ ] Manual smoke checks: PDP, PLP, cart page, minicart +- [ ] Verify network: no cart/recommendation API call before first user interaction Acceptance: -- [x] Shopify branch previews always reflect latest built assets +- [ ] Checks pass and behavior is validated after each milestone + +--- + +## Priority Snapshot + +### P0 (now) +- [x] Section Rendering foundation and null/failure hardening +- [x] Full Shopify docs parity for cart + section rendering flows +- [x] PDP variant-media reliability fix + +### P1 (next) +- [ ] PLP state/performance stabilization +- [x] Predictive search drawer (header trigger + aside + docs parity) +- [x] Accessibility hardening across templates +- [x] Starter customization contract + documentation + +### P2 (later) +- [ ] UX polish and final cleanup --- -## 8) Nice-to-have (after core) -- [x] Add import alias usage examples (`@ts`, `@css`) in code/docs -- [x] Add lightweight linting strategy for TS/Liquid (if needed) -- [x] Add automated smoke checklist script (optional) +## P0 Definition of Done (Execution Checklist) + +### DoD - 1) Section Rendering Foundation +- [x] All cart and drawer updates use section HTML returned by API (no custom HTML assembly in TS) +- [x] Shared section replace helper is used in both cart page and drawer modules +- [x] Request sequencing guard prevents stale UI on rapid `+/-/remove` clicks +- [x] `cart-count`, totals, and line items stay synchronized after every mutation +- [x] `null` section payload is handled with visible recoverable error and no broken UI + +Verify: +- [x] Rapid click stress test on cart page keeps consistent quantities/totals +- [x] Rapid click stress test in drawer keeps consistent quantities/totals + +### DoD - 2) Shopify Docs Parity +- [x] `cart/change.js` payload includes `sections` and `sections_url` where UI re-render is required +- [x] `sections_url` always starts with `/` and is locale-safe +- [x] Single section refresh (`?section_id=`) is used only for user-triggered drawer hydration +- [x] Section rendering null-response behavior matches docs and is covered in fallback flow + +Verify: +- [x] Network payloads inspected and match Shopify docs fields +- [x] Error simulation confirms graceful behavior on failed/invalid section response + +### DoD - 3) PDP Media Reliability +- [x] Size change does not reset/remove first image when variant lacks `featured_media` +- [x] Color change updates media only when dedicated media exists +- [x] Thumbnail visibility remains coherent with active media context +- [x] No blank gallery states during rapid option switching + +Verify: +- [x] Manual QA matrix: color -> size -> color, including variants with and without media +- [x] Behavior compared against Dawn expectations on equivalent scenarios From 505fcb4119147c1acd78bd08663f9830276cd664 Mon Sep 17 00:00:00 2001 From: Luca Argentieri Date: Mon, 23 Mar 2026 15:08:43 +0100 Subject: [PATCH 11/14] add cart note attributes and refactor search results rendering --- assets/.vite/manifest.json | 2 +- assets/search-Dt-ufTnA.js | 8 - assets/search-bqQT_R15.js | 1 + frontend/entrypoints/ts/search.ts | 58 ++++---- locales/en.default.json | 2 + sections/cart.liquid | 22 +++ snippets/vite-tag.liquid | 2 +- todo.md | 237 ++++++------------------------ 8 files changed, 102 insertions(+), 230 deletions(-) delete mode 100644 assets/search-Dt-ufTnA.js create mode 100644 assets/search-bqQT_R15.js diff --git a/assets/.vite/manifest.json b/assets/.vite/manifest.json index 0aa5b8af9..b2a4df22b 100644 --- a/assets/.vite/manifest.json +++ b/assets/.vite/manifest.json @@ -143,7 +143,7 @@ ] }, "frontend/entrypoints/ts/search.ts": { - "file": "search-Dt-ufTnA.js", + "file": "search-bqQT_R15.js", "name": "search", "src": "frontend/entrypoints/ts/search.ts", "isEntry": true diff --git a/assets/search-Dt-ufTnA.js b/assets/search-Dt-ufTnA.js deleted file mode 100644 index cddef7a18..000000000 --- a/assets/search-Dt-ufTnA.js +++ /dev/null @@ -1,8 +0,0 @@ -function v(t){return t.replaceAll("&","&").replaceAll("<","<").replaceAll(">",">").replaceAll('"',""").replaceAll("'","'")}function q(t,r=250){let n=null;return(...a)=>{n!==null&&window.clearTimeout(n),n=window.setTimeout(()=>{t(...a)},r)}}document.addEventListener("DOMContentLoaded",()=>{const t=document.querySelector('[data-js="search-root"]');if(!t)return;const r=t.querySelector('[data-js="search-input"]'),n=t.querySelector('[data-js="predictive-results"]'),a=t.querySelector('[data-js="predictive-list"]'),p=t.querySelector('[data-js="predictive-status"]'),h=t.querySelector('[data-js="search-status"]');if(!r||!n||!a||!p||!h)return;let u=null;const f=e=>{h.textContent=e},c=e=>{p.textContent=e},d=()=>{n.hidden=!0,r.setAttribute("aria-expanded","false")},i=()=>{n.hidden=!1,r.setAttribute("aria-expanded","true")},g=e=>{if(e.length===0){a.innerHTML="",c("No quick matches. Press Enter for full results."),i();return}a.innerHTML=e.map(s=>{const o=v(s.title),l=s.image?.url?`${v(s.image.alt??s.title)}`:"";return` -
    • - - ${l} - ${o} - -
    • - `}).join(""),c(`${e.length} quick matches`),i()},m=async e=>{if(e.length<2){d(),c("");return}u?.abort(),u=new AbortController,c("Searching..."),f("Predictive search active"),i();const s=`${window.Shopify.routes.root}search/suggest.json?q=${encodeURIComponent(e)}&resources[type]=product,page,article&resources[limit]=6&resources[options][unavailable_products]=last`;try{const o=await fetch(s,{signal:u.signal,headers:{Accept:"application/json","X-Requested-With":"XMLHttpRequest"}});if(!o.ok)throw new Error("Predictive endpoint unavailable");const l=await o.json(),y=l.resources?.results?.products??[],S=l.resources?.results?.pages??[],b=l.resources?.results?.articles??[];g([...y,...S,...b].slice(0,6))}catch(o){if(o instanceof DOMException&&o.name==="AbortError")return;d(),c(""),f("Predictive search unavailable. Full search still works.")}},w=q(e=>{m(e.trim())});r.addEventListener("input",()=>{w(r.value)}),r.addEventListener("focus",()=>{a.children.length>0&&i()}),t.addEventListener("focusout",()=>{window.setTimeout(()=>{const e=document.activeElement;e&&t.contains(e)||d()},100)})}); diff --git a/assets/search-bqQT_R15.js b/assets/search-bqQT_R15.js new file mode 100644 index 000000000..7da4c1115 --- /dev/null +++ b/assets/search-bqQT_R15.js @@ -0,0 +1 @@ +function S(r,n=250){let s=null;return(...a)=>{s!==null&&window.clearTimeout(s),s=window.setTimeout(()=>{r(...a)},n)}}document.addEventListener("DOMContentLoaded",()=>{const r=document.querySelector('[data-js="search-root"]');if(!r)return;const n=r.querySelector('[data-js="search-input"]'),s=r.querySelector('[data-js="predictive-results"]'),a=r.querySelector('[data-js="predictive-list"]'),h=r.querySelector('[data-js="predictive-status"]'),m=r.querySelector('[data-js="search-status"]');if(!n||!s||!a||!h||!m)return;let d=null;const f=e=>{m.textContent=e},l=e=>{h.textContent=e},p=()=>{s.hidden=!0,n.setAttribute("aria-expanded","false")},u=()=>{s.hidden=!1,n.setAttribute("aria-expanded","true")},g=e=>{const o=document.createElement("li"),t=document.createElement("a");if(t.href=e.url,t.className="flex items-center gap-3 rounded border px-2 py-2 hover:bg-gray-50",e.image?.url){const i=document.createElement("img");i.src=e.image.url,i.alt=e.image.alt??e.title,i.className="h-10 w-10 object-cover",t.appendChild(i)}const c=document.createElement("span");return c.className="text-sm",c.textContent=e.title,t.appendChild(c),o.appendChild(t),o},v=e=>{if(a.replaceChildren(),e.length===0){l("No quick matches. Press Enter for full results."),u();return}const o=document.createDocumentFragment();e.forEach(t=>{o.appendChild(g(t))}),a.appendChild(o),l(`${e.length} quick matches`),u()},w=async e=>{if(e.length<2){p(),l("");return}d?.abort(),d=new AbortController,l("Searching..."),f("Predictive search active"),u();const o=`${window.Shopify.routes.root}search/suggest.json?q=${encodeURIComponent(e)}&resources[type]=product,page,article&resources[limit]=6&resources[options][unavailable_products]=last`;try{const t=await fetch(o,{signal:d.signal,headers:{Accept:"application/json","X-Requested-With":"XMLHttpRequest"}});if(!t.ok)throw new Error("Predictive endpoint unavailable");const c=await t.json(),i=c.resources?.results?.products??[],y=c.resources?.results?.pages??[],C=c.resources?.results?.articles??[];v([...i,...y,...C].slice(0,6))}catch(t){if(t instanceof DOMException&&t.name==="AbortError")return;p(),l(""),f("Predictive search unavailable. Full search still works.")}},E=S(e=>{w(e.trim())});n.addEventListener("input",()=>{E(n.value)}),n.addEventListener("focus",()=>{a.children.length>0&&u()}),r.addEventListener("focusout",()=>{window.setTimeout(()=>{const e=document.activeElement;e&&r.contains(e)||p()},100)})}); diff --git a/frontend/entrypoints/ts/search.ts b/frontend/entrypoints/ts/search.ts index d0303b1a3..853f204fa 100644 --- a/frontend/entrypoints/ts/search.ts +++ b/frontend/entrypoints/ts/search.ts @@ -23,15 +23,6 @@ interface PredictivePayload { }; } -function escapeHtml(value: string): string { - return value - .replaceAll('&', '&') - .replaceAll('<', '<') - .replaceAll('>', '>') - .replaceAll('"', '"') - .replaceAll("'", '''); -} - function debounce void>(fn: T, delay = 250): (...args: Parameters) => void { let timeoutId: number | null = null; @@ -77,31 +68,44 @@ document.addEventListener('DOMContentLoaded', () => { input.setAttribute('aria-expanded', 'true'); }; + const createResultNode = (item: PredictiveItem): HTMLLIElement => { + const listItem = document.createElement('li'); + + const link = document.createElement('a'); + link.href = item.url; + link.className = 'flex items-center gap-3 rounded border px-2 py-2 hover:bg-gray-50'; + + if (item.image?.url) { + const image = document.createElement('img'); + image.src = item.image.url; + image.alt = item.image.alt ?? item.title; + image.className = 'h-10 w-10 object-cover'; + link.appendChild(image); + } + + const title = document.createElement('span'); + title.className = 'text-sm'; + title.textContent = item.title; + link.appendChild(title); + + listItem.appendChild(link); + return listItem; + }; + const renderResults = (results: PredictiveItem[]): void => { + list.replaceChildren(); + if (results.length === 0) { - list.innerHTML = ''; setPredictiveStatus('No quick matches. Press Enter for full results.'); openPanel(); return; } - list.innerHTML = results - .map((item) => { - const title = escapeHtml(item.title); - const image = item.image?.url - ? `${escapeHtml(item.image.alt ?? item.title)}` - : ''; - - return ` -
    • - - ${image} - ${title} - -
    • - `; - }) - .join(''); + const fragment = document.createDocumentFragment(); + results.forEach((item) => { + fragment.appendChild(createResultNode(item)); + }); + list.appendChild(fragment); setPredictiveStatus(`${results.length} quick matches`); openPanel(); diff --git a/locales/en.default.json b/locales/en.default.json index 3a43570d9..a072acaac 100644 --- a/locales/en.default.json +++ b/locales/en.default.json @@ -15,6 +15,8 @@ }, "cart": { "checkout": "Checkout", + "note": "Order note", + "order_reference": "Order reference", "title": "Cart", "update": "Update", "remove": "Remove" diff --git a/sections/cart.liquid b/sections/cart.liquid index 1090d05e5..02382713c 100644 --- a/sections/cart.liquid +++ b/sections/cart.liquid @@ -49,6 +49,28 @@

      Subtotal: {{ cart.total_price | money }}

      + +
      + + + + + + + +
      +
      diff --git a/snippets/vite-tag.liquid b/snippets/vite-tag.liquid index ceb43e4db..a49ab59d4 100644 --- a/snippets/vite-tag.liquid +++ b/snippets/vite-tag.liquid @@ -62,7 +62,7 @@ {% elsif path == "/frontend/entrypoints/ts/search.ts" or path == "ts/search.ts" %} - + {% elsif path == "/frontend/entrypoints/ts/search/drawer.ts" or path == "ts/search/drawer.ts" %} {% elsif path == "/frontend/entrypoints/ts/theme.ts" or path == "ts/theme.ts" %} diff --git a/todo.md b/todo.md index 23cf11dea..205bdd9f4 100644 --- a/todo.md +++ b/todo.md @@ -1,216 +1,67 @@ -# TODO - Shopify Skeleton Theme (Docs-First Optimization) +# TODO - Shopify Skeleton Theme -## North Star -- [ ] Keep this starter aligned with Shopify documentation first -- [ ] Use Section Rendering/Bundled Section Rendering as the default dynamic UI pattern -- [ ] Keep customization easy for future themes (logic reusable, markup editable in Liquid) +## Status Snapshot +- [x] P0 complete: section rendering foundation, Shopify docs parity, PDP media reliability +- [x] P1 mostly complete: PLP stability, predictive search drawer, accessibility hardening, customization docs +- [ ] Final P1 verification pass pending (mobile PLP + quality/network checks) --- -## Core Architecture Principles (Non-negotiable) -- [x] No HTML template strings in TS for cart and minicart flows -- [x] Cart and minicart UI updates driven by Section Rendering responses -- [x] No API calls without user interaction -- [x] Loading states without layout shift (pulse + overlay) -- [x] Document the "TS logic only, Liquid markup only" rule in contributor docs +## Now (Top Priority) ---- - -## 1) P0 - Section Rendering Foundation - -### Contract and utilities -- [x] Create a shared utility for section parsing/replacement (avoid duplicated logic in cart/drawer) -- [x] Standardize null-section fallback strategy (Shopify docs: sections can return `null`) -- [x] Standardize bundled request payload (`sections`, `sections_url`) across all cart mutations - -### Robustness -- [x] Add request sequencing guard for rapid quantity interactions (prevent out-of-order UI state) -- [x] Ensure cart count, subtotal, and item list always remain synchronized after updates -- [x] Validate graceful fallback when section fetch fails (recoverable error + safe redirect only when necessary) - -Acceptance: -- [x] Every cart UI update path uses section rendering response as source of truth -- [x] Rapid interactions do not produce stale UI -- [x] `null`/failed section responses are handled predictably - ---- - -## 2) P0 - Shopify Docs Parity - -### Cart API parity -- [x] Audit `cart/change.js` usage against Shopify Ajax Cart docs -- [x] Confirm locale-aware URL handling for all fetches -- [x] Confirm `sections_url` is always valid and starts with `/` - -### Section Rendering parity -- [x] Validate `?section_id=` flow for single-section refresh where applicable -- [x] Validate bundled sections behavior for cart mutations -- [x] Add explicit handling for sections that return `null` while response status is `200` - -Acceptance: -- [x] Implementation matches documented Shopify patterns for cart and section rendering -- [x] Edge cases from docs are covered in code paths - ---- - -## 3) P0 - PDP Media Reliability - -### Variant/media behavior -- [x] Fix regression: selected color + size change must not hide/remove first image -- [x] Change media only when target variant has `featured_media` -- [x] If variant has no dedicated media, keep current color media context - -### Validation -- [x] Validate `data-variant-media` mapping for shared media across variants -- [x] Stress test rapid option changes (no blank gallery, no wrong image jumps) -- [x] Compare with Dawn behavior (`assets/product-info.js`, `snippets/product-media-gallery.liquid`) - -Acceptance: -- [x] No media disappearance on size-only changes -- [x] Color/media transitions remain consistent and predictable - ---- - -## 4) P1 - PLP (Collection) with Same Mindset - -### State integrity -- [x] Keep filters/sort fully reflected in URL params -- [x] Keep browser history behavior stable and reversible -- [x] Ensure clear/reset state is deterministic - -### UX and performance -- [x] Keep loading/empty/error states explicit without CLS regressions -- [x] Reduce unnecessary DOM work during filter/sort interactions -- [x] Compare lifecycle behavior with Dawn (`assets/facets.js`) +### 1) PLP final mobile verification +- [ ] Verify mobile filter UX has no regressions (open/close/apply/clear/back-forward) Acceptance: -- [x] PLP interactions are stable across refresh/back/forward -- [ ] Mobile filter UX has no regressions +- [ ] PLP interactions are stable on mobile across refresh/back/forward ---- - -## 5) P1 - Predictive Search Drawer (Docs-First) - -### UX and interaction model -- [x] Add search trigger in header (`data-js="search-open"`) with drawer behavior aligned to cart (open/close/overlay/ESC/focus trap) -- [x] Open search as right-side aside with inline search field and predictive results panel -- [x] Keep close behavior consistent across close button, overlay click, and Escape -- [x] Restore focus to trigger on drawer close - -### Shopify docs parity (Ajax Predictive Search) -- [x] Use `/search/suggest.json` with locale-aware base URL (`window.Shopify.routes.root`) -- [x] Use documented resources params (`resources[type]`, `resources[limit]`, `resources[options][unavailable_products]`) -- [x] Keep graceful fallback when predictive endpoint is unavailable (full search submit still works) -- [x] Ensure no predictive API request before user interaction (drawer open + input length threshold) - -### Performance and resilience -- [x] Debounce input and cancel stale requests (`AbortController`) -- [x] Prevent out-of-order render on rapid typing -- [x] Keep loading/error states without layout shift (reserved result area + subtle loader) -- [x] Keep drawer fully functional on mobile and desktop - -### Starter customization surface -- [x] Keep markup in Liquid and behavior in TS (same cart/minicart architecture rule) -- [x] Define stable data attributes contract for search drawer elements -- [x] Document safe customization points (layout, item card markup, result grouping) - -Acceptance: -- [x] Search drawer opens/closes accessibly and predictably -- [x] Predictive results are fast, cancellable, and stable under rapid input -- [x] Full search remains available as fallback path -- [x] No API calls happen before user interaction - ---- - -## 6) P1 - Accessibility and UX Hardening - -- [x] Verify keyboard-first flows for cart page, minicart dialog, and PDP ATC -- [x] Verify live region messaging for success/error/cart updates -- [x] Verify focus restore paths after dialog close and failed actions -- [x] Keep loading communication visual + accessible without visible status text shifts +### 2) Quality gates and behavior checks +- [x] `bun run typecheck` +- [x] `bun run build` +- [ ] `theme-check` (if available locally) +- [ ] Manual smoke checks: PDP, PLP, cart page, minicart, search drawer +- [x] Verify network: no cart/recommendation API call before first user interaction Acceptance: -- [x] Core interactive flows are accessible with keyboard and screen readers - ---- - -## 7) P1 - Starter Customization Surface +- [ ] Checks pass and behavior is validated after the final pass -### Theme-extensible design -- [x] Document stable data attributes used by TS modules (public contract) -- [x] Document which Liquid blocks are safe to customize without breaking logic -- [x] Keep behavior modules isolated so teams can swap markup/styles safely - -### Documentation updates -- [x] Update `CLAUDE.md` with docs-first section-rendering conventions -- [x] Add README section: "Customization without breaking core cart flows" -- [x] Add `AGENTS.md` guide for generic AI contributors - -Acceptance: -- [x] New theme implementations can customize markup/styles without changing core JS logic +Notes: +- `theme-check` is currently unavailable in this environment (`command not found` / local CLI dependency issue) --- -## 8) Quality Gates (Continuous) +## Next -- [x] `bun run typecheck` -- [x] `bun run build` -- [ ] `theme-check` -- [ ] Manual smoke checks: PDP, PLP, cart page, minicart -- [ ] Verify network: no cart/recommendation API call before first user interaction - -Acceptance: -- [ ] Checks pass and behavior is validated after each milestone +### P2 - UX polish and cleanup +- [ ] Final cleanup pass (copy consistency, minor interaction polish, remove dead notes) --- -## Priority Snapshot +## Completed Archive (Condensed) -### P0 (now) -- [x] Section Rendering foundation and null/failure hardening -- [x] Full Shopify docs parity for cart + section rendering flows -- [x] PDP variant-media reliability fix +### Architecture and docs-first contracts +- [x] TS behavior and Liquid markup split documented and enforced +- [x] Stable `data-js` contracts documented for cart, product, collection, search +- [x] AI contributor guides updated (`CLAUDE.md`, `AGENTS.md`, `README.md`) -### P1 (next) -- [ ] PLP state/performance stabilization -- [x] Predictive search drawer (header trigger + aside + docs parity) -- [x] Accessibility hardening across templates -- [x] Starter customization contract + documentation +### Cart and section rendering +- [x] Shared section rendering utility implemented and reused +- [x] Cart page and drawer updates driven by bundled section rendering responses +- [x] `sections_url` normalization and locale-aware routes validated +- [x] `null`/invalid section responses handled with recoverable UI fallback +- [x] Request sequencing guard added for rapid quantity/remove actions -### P2 (later) -- [ ] UX polish and final cleanup +### PDP media reliability +- [x] Variant/media behavior aligned to Dawn expectations +- [x] No media disappearance on size-only changes +- [x] Rapid option-switch handling avoids blank gallery states ---- +### PLP and search drawer +- [x] Collection interactions stabilized (URL/history/state, deterministic clear/reset) +- [x] Predictive search drawer added (open/close, focus trap, ESC, restore focus) +- [x] Predictive API integration follows docs (`/search/suggest.json`, resources params) +- [x] Debounce + `AbortController` + stale-response guards implemented -## P0 Definition of Done (Execution Checklist) - -### DoD - 1) Section Rendering Foundation -- [x] All cart and drawer updates use section HTML returned by API (no custom HTML assembly in TS) -- [x] Shared section replace helper is used in both cart page and drawer modules -- [x] Request sequencing guard prevents stale UI on rapid `+/-/remove` clicks -- [x] `cart-count`, totals, and line items stay synchronized after every mutation -- [x] `null` section payload is handled with visible recoverable error and no broken UI - -Verify: -- [x] Rapid click stress test on cart page keeps consistent quantities/totals -- [x] Rapid click stress test in drawer keeps consistent quantities/totals - -### DoD - 2) Shopify Docs Parity -- [x] `cart/change.js` payload includes `sections` and `sections_url` where UI re-render is required -- [x] `sections_url` always starts with `/` and is locale-safe -- [x] Single section refresh (`?section_id=`) is used only for user-triggered drawer hydration -- [x] Section rendering null-response behavior matches docs and is covered in fallback flow - -Verify: -- [x] Network payloads inspected and match Shopify docs fields -- [x] Error simulation confirms graceful behavior on failed/invalid section response - -### DoD - 3) PDP Media Reliability -- [x] Size change does not reset/remove first image when variant lacks `featured_media` -- [x] Color change updates media only when dedicated media exists -- [x] Thumbnail visibility remains coherent with active media context -- [x] No blank gallery states during rapid option switching - -Verify: -- [x] Manual QA matrix: color -> size -> color, including variants with and without media -- [x] Behavior compared against Dawn expectations on equivalent scenarios +### Accessibility hardening +- [x] Keyboard-first flows verified for core cart/search/product interactions +- [x] Live-region and error/status communication improved without layout shift From c78b12f9e712c2cbdeafc7349db740c6292f67f4 Mon Sep 17 00:00:00 2001 From: Luca Argentieri Date: Mon, 23 Mar 2026 15:41:06 +0100 Subject: [PATCH 12/14] stabilize pdp media ordering and decouple recommendations from variant changes Keep variant-primary media first in both gallery and thumbnails, load recommendations only after first PDP interaction, and simplify the project todo to active items. --- assets/.vite/manifest.json | 8 +- ...dlers-DnmO5tfH.js => handlers-BekC_4hw.js} | 2 +- assets/main-BF2QWjU5.css | 1 - assets/main-BedzLXBi.css | 1 + assets/product-BxBL01GO.js | 1 - assets/product-CbP3NaeG.js | 1 + assets/sync-BRYiu7LQ.js | 1 + assets/sync-YStD_kYI.js | 1 - frontend/entrypoints/css/main.css | 94 +++++++++++++++++++ frontend/entrypoints/ts/product.ts | 40 ++++++-- frontend/entrypoints/ts/product/sync.ts | 66 ++++++++----- snippets/vite-tag.liquid | 91 +++++------------- todo.md | 75 +++------------ 13 files changed, 216 insertions(+), 166 deletions(-) rename assets/{handlers-DnmO5tfH.js => handlers-BekC_4hw.js} (95%) delete mode 100644 assets/main-BF2QWjU5.css create mode 100644 assets/main-BedzLXBi.css delete mode 100644 assets/product-BxBL01GO.js create mode 100644 assets/product-CbP3NaeG.js create mode 100644 assets/sync-BRYiu7LQ.js delete mode 100644 assets/sync-YStD_kYI.js diff --git a/assets/.vite/manifest.json b/assets/.vite/manifest.json index b2a4df22b..26ce89f15 100644 --- a/assets/.vite/manifest.json +++ b/assets/.vite/manifest.json @@ -1,6 +1,6 @@ { "frontend/entrypoints/css/main.css": { - "file": "main-BF2QWjU5.css", + "file": "main-BedzLXBi.css", "src": "frontend/entrypoints/css/main.css", "isEntry": true, "name": "main", @@ -95,7 +95,7 @@ "isEntry": true }, "frontend/entrypoints/ts/product.ts": { - "file": "product-BxBL01GO.js", + "file": "product-CbP3NaeG.js", "name": "product", "src": "frontend/entrypoints/ts/product.ts", "isEntry": true, @@ -109,7 +109,7 @@ ] }, "frontend/entrypoints/ts/product/handlers.ts": { - "file": "handlers-DnmO5tfH.js", + "file": "handlers-BekC_4hw.js", "name": "handlers", "src": "frontend/entrypoints/ts/product/handlers.ts", "isEntry": true, @@ -133,7 +133,7 @@ "isEntry": true }, "frontend/entrypoints/ts/product/sync.ts": { - "file": "sync-YStD_kYI.js", + "file": "sync-BRYiu7LQ.js", "name": "sync", "src": "frontend/entrypoints/ts/product/sync.ts", "isEntry": true, diff --git a/assets/handlers-DnmO5tfH.js b/assets/handlers-BekC_4hw.js similarity index 95% rename from assets/handlers-DnmO5tfH.js rename to assets/handlers-BekC_4hw.js index d16344180..01dd7e840 100644 --- a/assets/handlers-DnmO5tfH.js +++ b/assets/handlers-BekC_4hw.js @@ -1 +1 @@ -import{f as u}from"./variant-picker-DRrQ71gT.js";import{a as m}from"./cart-Kw4e-iq4.js";import{s as t}from"./state-Bwp_aMHz.js";import{s as i}from"./sync-YStD_kYI.js";function I(n){const e=n.target.closest('[data-js="option-value"]');if(!e||e.disabled||e.getAttribute("aria-disabled")==="true")return;const a=Number(e.dataset.optionPosition)-1,r=e.dataset.optionValue??"";if(a<0||!r)return;t.selectedOptions[a]=r;const o=u(t.productData.variants,t.selectedOptions);if(!o){t.selectedOptions[a]=t.currentVariant.options[a]??t.selectedOptions[a];return}const d=t.currentMediaId,c=t.mediaContextVariantId;t.currentVariant=o,o.featured_media?(t.currentMediaId=o.featured_media.id,t.mediaContextVariantId=o.id):(t.currentMediaId=d,t.mediaContextVariantId=c);const s=new URL(window.location.href);s.searchParams.set("variant",String(o.id)),history.replaceState({},"",`${s.pathname}${s.search}${s.hash}`),i()}function h(n){const e=n.target.closest('[data-js="thumbnail"]');if(!e)return;const a=Number(e.dataset.thumbnail);if(!a)return;t.currentMediaId=a;const r=Number((e.dataset.variantMedia??"").split(",")[0]);r&&(t.mediaContextVariantId=r),i()}async function V(n){if(n.preventDefault(),t.cartState==="loading")return;const a=n.target.querySelector('input[name="quantity"]'),r=a?Math.max(1,Number(a.value)||1):1;t.cartState="loading",i();try{await m(t.currentVariant.id,r),t.cartState="success",i(),setTimeout(()=>{t.cartState="idle",i()},2e3)}catch{t.cartState="error",i(),setTimeout(()=>{t.cartState="idle",i()},3e3)}}export{V as a,h as b,I as o}; +import{f as u}from"./variant-picker-DRrQ71gT.js";import{a as m}from"./cart-Kw4e-iq4.js";import{s as t}from"./state-Bwp_aMHz.js";import{s as i}from"./sync-BRYiu7LQ.js";function I(n){const e=n.target.closest('[data-js="option-value"]');if(!e||e.disabled||e.getAttribute("aria-disabled")==="true")return;const a=Number(e.dataset.optionPosition)-1,r=e.dataset.optionValue??"";if(a<0||!r)return;t.selectedOptions[a]=r;const o=u(t.productData.variants,t.selectedOptions);if(!o){t.selectedOptions[a]=t.currentVariant.options[a]??t.selectedOptions[a];return}const d=t.currentMediaId,c=t.mediaContextVariantId;t.currentVariant=o,o.featured_media?(t.currentMediaId=o.featured_media.id,t.mediaContextVariantId=o.id):(t.currentMediaId=d,t.mediaContextVariantId=c);const s=new URL(window.location.href);s.searchParams.set("variant",String(o.id)),history.replaceState({},"",`${s.pathname}${s.search}${s.hash}`),i()}function h(n){const e=n.target.closest('[data-js="thumbnail"]');if(!e)return;const a=Number(e.dataset.thumbnail);if(!a)return;t.currentMediaId=a;const r=Number((e.dataset.variantMedia??"").split(",")[0]);r&&(t.mediaContextVariantId=r),i()}async function V(n){if(n.preventDefault(),t.cartState==="loading")return;const a=n.target.querySelector('input[name="quantity"]'),r=a?Math.max(1,Number(a.value)||1):1;t.cartState="loading",i();try{await m(t.currentVariant.id,r),t.cartState="success",i(),setTimeout(()=>{t.cartState="idle",i()},2e3)}catch{t.cartState="error",i(),setTimeout(()=>{t.cartState="idle",i()},3e3)}}export{V as a,h as b,I as o}; diff --git a/assets/main-BF2QWjU5.css b/assets/main-BF2QWjU5.css deleted file mode 100644 index 94e3a5bf6..000000000 --- a/assets/main-BF2QWjU5.css +++ /dev/null @@ -1 +0,0 @@ -@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-border-style:solid;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-duration:initial;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-500:oklch(63.7% .237 25.331);--color-red-700:oklch(50.5% .213 27.518);--color-yellow-500:oklch(79.5% .184 86.047);--color-green-500:oklch(72.3% .219 149.579);--color-blue-50:oklch(97% .014 254.604);--color-blue-400:oklch(70.7% .165 254.624);--color-blue-500:oklch(62.3% .214 259.815);--color-blue-600:oklch(54.6% .245 262.881);--color-blue-700:oklch(48.8% .243 264.376);--color-gray-50:oklch(98.5% .002 247.839);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-300:oklch(87.2% .01 258.338);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-600:oklch(44.6% .03 256.802);--color-gray-700:oklch(37.3% .034 259.733);--color-gray-800:oklch(27.8% .033 256.848);--color-gray-900:oklch(21% .034 264.665);--color-neutral-100:oklch(97% 0 0);--color-neutral-900:oklch(20.5% 0 0);--color-black:#000;--color-white:#fff;--spacing:.25rem;--breakpoint-sm:40rem;--breakpoint-md:48rem;--breakpoint-lg:64rem;--breakpoint-xl:80rem;--container-md:28rem;--container-lg:32rem;--container-xl:36rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height: 1.5 ;--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-3xl:1.875rem;--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5 / 2.25);--font-weight-thin:100;--font-weight-extralight:200;--font-weight-light:300;--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--font-weight-extrabold:800;--font-weight-black:900;--tracking-wide:.025em;--radius-sm:.25rem;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--shadow-sm:0 1px 3px 0 #0000001a, 0 1px 2px -1px #0000001a;--shadow-md:0 4px 6px -1px #0000001a, 0 2px 4px -2px #0000001a;--animate-spin:spin 1s linear infinite;--animate-pulse:pulse 2s cubic-bezier(.4, 0, .6, 1) infinite;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.pointer-events-none{pointer-events:none}.invisible{visibility:hidden}.visible{visibility:visible}.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.inset-0{inset:calc(var(--spacing) * 0)}.end{inset-inline-end:var(--spacing)}.top-0{top:calc(var(--spacing) * 0)}.top-\[117px\]{top:117px}.right-0{right:calc(var(--spacing) * 0)}.bottom-0{bottom:calc(var(--spacing) * 0)}.left-0{left:calc(var(--spacing) * 0)}.z-10{z-index:10}.z-20{z-index:20}.z-50{z-index:50}.container{width:100%}@media(min-width:40rem){.container{max-width:40rem}}@media(min-width:48rem){.container{max-width:48rem}}@media(min-width:64rem){.container{max-width:64rem}}@media(min-width:80rem){.container{max-width:80rem}}@media(min-width:96rem){.container{max-width:96rem}}.mx-2{margin-inline:calc(var(--spacing) * 2)}.mx-5{margin-inline:calc(var(--spacing) * 5)}.mt-1{margin-top:calc(var(--spacing) * 1)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-8{margin-bottom:calc(var(--spacing) * 8)}.ml-auto{margin-left:auto}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.aspect-square{aspect-ratio:1}.h-4{height:calc(var(--spacing) * 4)}.h-5{height:calc(var(--spacing) * 5)}.h-8{height:calc(var(--spacing) * 8)}.h-9{height:calc(var(--spacing) * 9)}.h-10{height:calc(var(--spacing) * 10)}.h-16{height:calc(var(--spacing) * 16)}.h-20{height:calc(var(--spacing) * 20)}.h-full{height:100%}.max-h-\[calc\(100vh-240px\)\]{max-height:calc(100vh - 240px)}.min-h-5{min-height:calc(var(--spacing) * 5)}.min-h-\[240px\]{min-height:240px}.min-h-svh{min-height:100svh}.w-4{width:calc(var(--spacing) * 4)}.w-5{width:calc(var(--spacing) * 5)}.w-8{width:calc(var(--spacing) * 8)}.w-9{width:calc(var(--spacing) * 9)}.w-10{width:calc(var(--spacing) * 10)}.w-16{width:calc(var(--spacing) * 16)}.w-20{width:calc(var(--spacing) * 20)}.w-\[calc\(100\%-2rem\)\]{width:calc(100% - 2rem)}.w-full{width:100%}.max-w-lg{max-width:var(--container-lg)}.max-w-md{max-width:var(--container-md)}.max-w-xl{max-width:var(--container-xl)}.min-w-6{min-width:calc(var(--spacing) * 6)}.flex-1{flex:1}.shrink-0{flex-shrink:0}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.animate-pulse{animation:var(--animate-pulse)}.animate-spin{animation:var(--animate-spin)}.cursor-pointer{cursor:pointer}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-\[64px_1fr\]{grid-template-columns:64px 1fr}.grid-cols-\[80px_1fr\]{grid-template-columns:80px 1fr}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-end{align-items:flex-end}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-1{gap:calc(var(--spacing) * 1)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 5) * calc(1 - var(--tw-space-y-reverse)))}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-xl{border-radius:var(--radius-xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-black\/20{border-color:#0003}@supports (color:color-mix(in lab,red,red)){.border-black\/20{border-color:color-mix(in oklab,var(--color-black) 20%,transparent)}}.border-gray-300{border-color:var(--color-gray-300)}.border-t-black{border-top-color:var(--color-black)}.bg-\[\#FF5A00\]{background-color:#ff5a00}.bg-black\/40{background-color:#0006}@supports (color:color-mix(in lab,red,red)){.bg-black\/40{background-color:color-mix(in oklab,var(--color-black) 40%,transparent)}}.bg-blue-400{background-color:var(--color-blue-400)}.bg-blue-500{background-color:var(--color-blue-500)}.bg-blue-600{background-color:var(--color-blue-600)}.bg-gray-800{background-color:var(--color-gray-800)}.bg-white{background-color:var(--color-white)}.bg-white\/60{background-color:#fff9}@supports (color:color-mix(in lab,red,red)){.bg-white\/60{background-color:color-mix(in oklab,var(--color-white) 60%,transparent)}}.\[mask-type\:luminance\]{mask-type:luminance}.fill-\(--brand-color\){fill:var(--brand-color)}.object-cover{object-fit:cover}.p-2{padding:calc(var(--spacing) * 2)}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.p-6{padding:calc(var(--spacing) * 6)}.p-8{padding:calc(var(--spacing) * 8)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.py-1{padding-block:calc(var(--spacing) * 1)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-8{padding-block:calc(var(--spacing) * 8)}.pb-3{padding-bottom:calc(var(--spacing) * 3)}.pb-4{padding-bottom:calc(var(--spacing) * 4)}.text-center{text-align:center}.text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.text-\(color\:--brand-text\){color:var(--brand-text)}.text-blue-500{color:var(--color-blue-500)}.text-gray-300{color:var(--color-gray-300)}.text-gray-500{color:var(--color-gray-500)}.text-gray-600{color:var(--color-gray-600)}.text-gray-700{color:var(--color-gray-700)}.text-gray-800{color:var(--color-gray-800)}.text-gray-900{color:var(--color-gray-900)}.text-green-500{color:var(--color-green-500)}.text-red-500{color:var(--color-red-500)}.text-red-700{color:var(--color-red-700)}.text-white{color:var(--color-white)}.text-yellow-500{color:var(--color-yellow-500)}.capitalize{text-transform:capitalize}.uppercase{text-transform:uppercase}.underline{text-decoration-line:underline}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.opacity-100{opacity:1}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-2xl{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a), 0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a), 0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-300{--tw-duration:.3s;transition-duration:.3s}.forced-color-adjust-auto{forced-color-adjust:auto}.forced-color-adjust-none{forced-color-adjust:none}@media(hover:hover){.group-hover\:text-blue-600:is(:where(.group):hover *){color:var(--color-blue-600)}.group-hover\:text-gray-700:is(:where(.group):hover *){color:var(--color-gray-700)}.group-hover\:underline:is(:where(.group):hover *){text-decoration-line:underline}}.peer-checked\:block:is(:where(.peer):checked~*){display:block}.first\:pt-0:first-child{padding-top:calc(var(--spacing) * 0)}.last\:pb-0:last-child{padding-bottom:calc(var(--spacing) * 0)}.odd\:bg-gray-50:nth-child(odd){background-color:var(--color-gray-50)}.even\:bg-white:nth-child(2n){background-color:var(--color-white)}.focus-within\:ring-2:focus-within{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}@media(hover:hover){.hover\:-translate-y-1:hover{--tw-translate-y:calc(var(--spacing) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.hover\:bg-blue-50:hover{background-color:var(--color-blue-50)}.hover\:bg-blue-600:hover{background-color:var(--color-blue-600)}.hover\:bg-blue-700:hover{background-color:var(--color-blue-700)}.hover\:bg-gray-50:hover{background-color:var(--color-gray-50)}.hover\:shadow-lg:hover{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}}.focus\:not-sr-only:focus{clip-path:none;white-space:normal;width:auto;height:auto;margin:0;padding:0;position:static;overflow:visible}.focus\:absolute:focus{position:absolute}.focus\:top-4:focus{top:calc(var(--spacing) * 4)}.focus\:left-4:focus{left:calc(var(--spacing) * 4)}.focus\:z-50:focus{z-index:50}.focus\:rounded:focus{border-radius:.25rem}.focus\:border-blue-500:focus{border-color:var(--color-blue-500)}.focus\:bg-white:focus{background-color:var(--color-white)}.focus\:px-4:focus{padding-inline:calc(var(--spacing) * 4)}.focus\:py-2:focus{padding-block:calc(var(--spacing) * 2)}.focus\:ring-1:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-blue-500:focus{--tw-ring-color:var(--color-blue-500)}.focus\:ring-offset-2:focus{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.focus-visible\:ring-2:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus-visible\:ring-blue-500:focus-visible{--tw-ring-color:var(--color-blue-500)}.focus-visible\:ring-offset-2:focus-visible{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.active\:bg-blue-700:active{background-color:var(--color-blue-700)}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}@media(prefers-reduced-motion:reduce){.motion-reduce\:transform-none{transform:none}.motion-reduce\:transition-none{transition-property:none}}@media(min-width:40rem){.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(min-width:48rem){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:p-8{padding:calc(var(--spacing) * 8)}.md\:px-8{padding-inline:calc(var(--spacing) * 8)}@media(hover:hover){.md\:hover\:px-10:hover{padding-inline:calc(var(--spacing) * 10)}}}@media(min-width:64rem){.lg\:w-16{width:calc(var(--spacing) * 16)}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:flex-col{flex-direction:column}.lg\:flex-row-reverse{flex-direction:row-reverse}.lg\:p-12{padding:calc(var(--spacing) * 12)}}@media(min-width:80rem){.xl\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}}@media(prefers-color-scheme:dark){.dark\:bg-blue-400{background-color:var(--color-blue-400)}.dark\:bg-blue-500{background-color:var(--color-blue-500)}.dark\:bg-gray-900{background-color:var(--color-gray-900)}.dark\:text-gray-100{color:var(--color-gray-100)}.dark\:opacity-90{opacity:.9}.dark\:shadow-none{--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark\:ring-1{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark\:ring-white\/10{--tw-ring-color:#ffffff1a}@supports (color:color-mix(in lab,red,red)){.dark\:ring-white\/10{--tw-ring-color:color-mix(in oklab, var(--color-white) 10%, transparent)}}@media(hover:hover){.dark\:hover\:bg-blue-400:hover{background-color:var(--color-blue-400)}.dark\:hover\:bg-blue-500:hover{background-color:var(--color-blue-500)}}}}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@keyframes spin{to{transform:rotate(360deg)}}@keyframes pulse{50%{opacity:.5}} diff --git a/assets/main-BedzLXBi.css b/assets/main-BedzLXBi.css new file mode 100644 index 000000000..cc57aa3f6 --- /dev/null +++ b/assets/main-BedzLXBi.css @@ -0,0 +1 @@ +@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-border-style:solid;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-duration:initial;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-500:oklch(63.7% .237 25.331);--color-red-700:oklch(50.5% .213 27.518);--color-yellow-500:oklch(79.5% .184 86.047);--color-green-500:oklch(72.3% .219 149.579);--color-blue-50:oklch(97% .014 254.604);--color-blue-400:oklch(70.7% .165 254.624);--color-blue-500:oklch(62.3% .214 259.815);--color-blue-600:oklch(54.6% .245 262.881);--color-blue-700:oklch(48.8% .243 264.376);--color-gray-50:oklch(98.5% .002 247.839);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-300:oklch(87.2% .01 258.338);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-600:oklch(44.6% .03 256.802);--color-gray-700:oklch(37.3% .034 259.733);--color-gray-800:oklch(27.8% .033 256.848);--color-gray-900:oklch(21% .034 264.665);--color-neutral-100:oklch(97% 0 0);--color-neutral-900:oklch(20.5% 0 0);--color-black:#000;--color-white:#fff;--spacing:.25rem;--breakpoint-sm:40rem;--breakpoint-md:48rem;--breakpoint-lg:64rem;--breakpoint-xl:80rem;--container-md:28rem;--container-lg:32rem;--container-xl:36rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height: 1.5 ;--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-3xl:1.875rem;--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5 / 2.25);--font-weight-thin:100;--font-weight-extralight:200;--font-weight-light:300;--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--font-weight-extrabold:800;--font-weight-black:900;--tracking-wide:.025em;--radius-sm:.25rem;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--shadow-sm:0 1px 3px 0 #0000001a, 0 1px 2px -1px #0000001a;--shadow-md:0 4px 6px -1px #0000001a, 0 2px 4px -2px #0000001a;--animate-spin:spin 1s linear infinite;--animate-pulse:pulse 2s cubic-bezier(.4, 0, .6, 1) infinite;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.pointer-events-none{pointer-events:none}.invisible{visibility:hidden}.visible{visibility:visible}.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.inset-0{inset:calc(var(--spacing) * 0)}.end{inset-inline-end:var(--spacing)}.top-0{top:calc(var(--spacing) * 0)}.top-\[117px\]{top:117px}.right-0{right:calc(var(--spacing) * 0)}.bottom-0{bottom:calc(var(--spacing) * 0)}.left-0{left:calc(var(--spacing) * 0)}.z-10{z-index:10}.z-20{z-index:20}.z-50{z-index:50}.container{width:100%}@media(min-width:40rem){.container{max-width:40rem}}@media(min-width:48rem){.container{max-width:48rem}}@media(min-width:64rem){.container{max-width:64rem}}@media(min-width:80rem){.container{max-width:80rem}}@media(min-width:96rem){.container{max-width:96rem}}.mx-2{margin-inline:calc(var(--spacing) * 2)}.mx-5{margin-inline:calc(var(--spacing) * 5)}.mt-1{margin-top:calc(var(--spacing) * 1)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-8{margin-bottom:calc(var(--spacing) * 8)}.ml-auto{margin-left:auto}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.scrollbar-hidden::-webkit-scrollbar{display:none}.aspect-square{aspect-ratio:1}.h-4{height:calc(var(--spacing) * 4)}.h-5{height:calc(var(--spacing) * 5)}.h-8{height:calc(var(--spacing) * 8)}.h-9{height:calc(var(--spacing) * 9)}.h-10{height:calc(var(--spacing) * 10)}.h-16{height:calc(var(--spacing) * 16)}.h-20{height:calc(var(--spacing) * 20)}.h-full{height:100%}.max-h-\[calc\(100vh-240px\)\]{max-height:calc(100vh - 240px)}.min-h-5{min-height:calc(var(--spacing) * 5)}.min-h-\[240px\]{min-height:240px}.min-h-svh{min-height:100svh}.w-4{width:calc(var(--spacing) * 4)}.w-5{width:calc(var(--spacing) * 5)}.w-8{width:calc(var(--spacing) * 8)}.w-9{width:calc(var(--spacing) * 9)}.w-10{width:calc(var(--spacing) * 10)}.w-16{width:calc(var(--spacing) * 16)}.w-20{width:calc(var(--spacing) * 20)}.w-\[calc\(100\%-2rem\)\]{width:calc(100% - 2rem)}.w-full{width:100%}.max-w-lg{max-width:var(--container-lg)}.max-w-md{max-width:var(--container-md)}.max-w-xl{max-width:var(--container-xl)}.min-w-6{min-width:calc(var(--spacing) * 6)}.flex-1{flex:1}.shrink-0{flex-shrink:0}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.animate-pulse{animation:var(--animate-pulse)}.animate-spin{animation:var(--animate-spin)}.cursor-pointer{cursor:pointer}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-\[64px_1fr\]{grid-template-columns:64px 1fr}.grid-cols-\[80px_1fr\]{grid-template-columns:80px 1fr}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-end{align-items:flex-end}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-1{gap:calc(var(--spacing) * 1)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 5) * calc(1 - var(--tw-space-y-reverse)))}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-xl{border-radius:var(--radius-xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-black\/20{border-color:#0003}@supports (color:color-mix(in lab,red,red)){.border-black\/20{border-color:color-mix(in oklab,var(--color-black) 20%,transparent)}}.border-gray-300{border-color:var(--color-gray-300)}.border-t-black{border-top-color:var(--color-black)}.bg-\[\#FF5A00\]{background-color:#ff5a00}.bg-black\/40{background-color:#0006}@supports (color:color-mix(in lab,red,red)){.bg-black\/40{background-color:color-mix(in oklab,var(--color-black) 40%,transparent)}}.bg-blue-400{background-color:var(--color-blue-400)}.bg-blue-500{background-color:var(--color-blue-500)}.bg-blue-600{background-color:var(--color-blue-600)}.bg-gray-800{background-color:var(--color-gray-800)}.bg-white{background-color:var(--color-white)}.bg-white\/60{background-color:#fff9}@supports (color:color-mix(in lab,red,red)){.bg-white\/60{background-color:color-mix(in oklab,var(--color-white) 60%,transparent)}}.\[mask-type\:luminance\]{mask-type:luminance}.fill-\(--brand-color\){fill:var(--brand-color)}.object-cover{object-fit:cover}.p-2{padding:calc(var(--spacing) * 2)}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.p-6{padding:calc(var(--spacing) * 6)}.p-8{padding:calc(var(--spacing) * 8)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.py-1{padding-block:calc(var(--spacing) * 1)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-8{padding-block:calc(var(--spacing) * 8)}.pb-3{padding-bottom:calc(var(--spacing) * 3)}.pb-4{padding-bottom:calc(var(--spacing) * 4)}.text-center{text-align:center}.text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.text-\(color\:--brand-text\){color:var(--brand-text)}.text-blue-500{color:var(--color-blue-500)}.text-gray-300{color:var(--color-gray-300)}.text-gray-500{color:var(--color-gray-500)}.text-gray-600{color:var(--color-gray-600)}.text-gray-700{color:var(--color-gray-700)}.text-gray-800{color:var(--color-gray-800)}.text-gray-900{color:var(--color-gray-900)}.text-green-500{color:var(--color-green-500)}.text-red-500{color:var(--color-red-500)}.text-red-700{color:var(--color-red-700)}.text-white{color:var(--color-white)}.text-yellow-500{color:var(--color-yellow-500)}.capitalize{text-transform:capitalize}.uppercase{text-transform:uppercase}.underline{text-decoration-line:underline}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.opacity-100{opacity:1}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-2xl{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a), 0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a), 0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-300{--tw-duration:.3s;transition-duration:.3s}.forced-color-adjust-auto{forced-color-adjust:auto}.forced-color-adjust-none{forced-color-adjust:none}@media(hover:hover){.group-hover\:text-blue-600:is(:where(.group):hover *){color:var(--color-blue-600)}.group-hover\:text-gray-700:is(:where(.group):hover *){color:var(--color-gray-700)}.group-hover\:underline:is(:where(.group):hover *){text-decoration-line:underline}}.peer-checked\:block:is(:where(.peer):checked~*){display:block}.first\:pt-0:first-child{padding-top:calc(var(--spacing) * 0)}.last\:pb-0:last-child{padding-bottom:calc(var(--spacing) * 0)}.odd\:bg-gray-50:nth-child(odd){background-color:var(--color-gray-50)}.even\:bg-white:nth-child(2n){background-color:var(--color-white)}.focus-within\:ring-2:focus-within{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}@media(hover:hover){.hover\:-translate-y-1:hover{--tw-translate-y:calc(var(--spacing) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.hover\:bg-blue-50:hover{background-color:var(--color-blue-50)}.hover\:bg-blue-600:hover{background-color:var(--color-blue-600)}.hover\:bg-blue-700:hover{background-color:var(--color-blue-700)}.hover\:bg-gray-50:hover{background-color:var(--color-gray-50)}.hover\:shadow-lg:hover{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}}.focus\:not-sr-only:focus{clip-path:none;white-space:normal;width:auto;height:auto;margin:0;padding:0;position:static;overflow:visible}.focus\:absolute:focus{position:absolute}.focus\:top-4:focus{top:calc(var(--spacing) * 4)}.focus\:left-4:focus{left:calc(var(--spacing) * 4)}.focus\:z-50:focus{z-index:50}.focus\:rounded:focus{border-radius:.25rem}.focus\:border-blue-500:focus{border-color:var(--color-blue-500)}.focus\:bg-white:focus{background-color:var(--color-white)}.focus\:px-4:focus{padding-inline:calc(var(--spacing) * 4)}.focus\:py-2:focus{padding-block:calc(var(--spacing) * 2)}.focus\:ring-1:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-blue-500:focus{--tw-ring-color:var(--color-blue-500)}.focus\:ring-offset-2:focus{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.focus-visible\:ring-2:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus-visible\:ring-blue-500:focus-visible{--tw-ring-color:var(--color-blue-500)}.focus-visible\:ring-offset-2:focus-visible{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.active\:bg-blue-700:active{background-color:var(--color-blue-700)}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}@media(prefers-reduced-motion:reduce){.motion-reduce\:transform-none{transform:none}.motion-reduce\:transition-none{transition-property:none}}@media(min-width:40rem){.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(min-width:48rem){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:p-8{padding:calc(var(--spacing) * 8)}.md\:px-8{padding-inline:calc(var(--spacing) * 8)}@media(hover:hover){.md\:hover\:px-10:hover{padding-inline:calc(var(--spacing) * 10)}}}@media(min-width:64rem){.lg\:w-16{width:calc(var(--spacing) * 16)}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:flex-col{flex-direction:column}.lg\:flex-row-reverse{flex-direction:row-reverse}.lg\:p-12{padding:calc(var(--spacing) * 12)}}@media(min-width:80rem){.xl\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}}@media(prefers-color-scheme:dark){.dark\:bg-blue-400{background-color:var(--color-blue-400)}.dark\:bg-blue-500{background-color:var(--color-blue-500)}.dark\:bg-gray-900{background-color:var(--color-gray-900)}.dark\:text-gray-100{color:var(--color-gray-100)}.dark\:opacity-90{opacity:.9}.dark\:shadow-none{--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark\:ring-1{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark\:ring-white\/10{--tw-ring-color:#ffffff1a}@supports (color:color-mix(in lab,red,red)){.dark\:ring-white\/10{--tw-ring-color:color-mix(in oklab, var(--color-white) 10%, transparent)}}@media(hover:hover){.dark\:hover\:bg-blue-400:hover{background-color:var(--color-blue-400)}.dark\:hover\:bg-blue-500:hover{background-color:var(--color-blue-500)}}}}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@keyframes spin{to{transform:rotate(360deg)}}@keyframes pulse{50%{opacity:.5}} diff --git a/assets/product-BxBL01GO.js b/assets/product-BxBL01GO.js deleted file mode 100644 index c9f948105..000000000 --- a/assets/product-BxBL01GO.js +++ /dev/null @@ -1 +0,0 @@ -import{s as e}from"./state-Bwp_aMHz.js";import{s as d}from"./sync-YStD_kYI.js";import{o as m,a as u,b as l}from"./handlers-DnmO5tfH.js";import{l as f}from"./recommendations-H0stnrna.js";import"./variant-picker-DRrQ71gT.js";import"./cart-Kw4e-iq4.js";function s(t){const n=new URLSearchParams(window.location.search),r=Number(n.get("variant"));return t.variants.find(a=>a.id===r)??t.variants.find(a=>a.available)??t.variants[0]}function c(t){if(e.currentVariant=t,e.selectedOptions=[...t.options],t.featured_media){e.currentMediaId=t.featured_media.id,e.mediaContextVariantId=t.id;return}if(e.currentMediaId===null){const n=document.querySelector('[data-js="media-item"]'),r=Number(n?.dataset.mediaId??""),a=Number((n?.dataset.variantMedia??"").split(",")[0]);e.currentMediaId=r||null,e.mediaContextVariantId=a||e.mediaContextVariantId}}function p(t){return!!t.target.closest('[data-js="option-value"]')}function C(t){return!!t.target.closest('[data-js="thumbnail"]')}function v(){const t=document.querySelector('[data-js="product-data"]');if(!t?.textContent)return;e.productData=JSON.parse(t.textContent);const n=document.querySelector('[data-js="variant-prices"]');e.variantPrices=n?.textContent?JSON.parse(n.textContent):{},c(s(e.productData));let r=!1;const a=()=>{r||(r=!0,f())},o=document.querySelector('[data-js="product-form"]');o?.addEventListener("click",i=>{p(i)&&a(),m(i)}),o?.addEventListener("submit",i=>{a(),u(i)}),document.addEventListener("click",i=>{C(i)&&a(),l(i)}),window.addEventListener("popstate",()=>{c(s(e.productData)),e.cartState="idle",d()}),d()}document.addEventListener("DOMContentLoaded",v); diff --git a/assets/product-CbP3NaeG.js b/assets/product-CbP3NaeG.js new file mode 100644 index 000000000..ea4b45d40 --- /dev/null +++ b/assets/product-CbP3NaeG.js @@ -0,0 +1 @@ +import{s as n}from"./state-Bwp_aMHz.js";import{s as c}from"./sync-BRYiu7LQ.js";import{o as f,a as v,b as L}from"./handlers-BekC_4hw.js";import{l as E}from"./recommendations-H0stnrna.js";import"./variant-picker-DRrQ71gT.js";import"./cart-Kw4e-iq4.js";function u(t){const a=new URLSearchParams(window.location.search),o=Number(a.get("variant"));return t.variants.find(r=>r.id===o)??t.variants.find(r=>r.available)??t.variants[0]}function m(t){if(n.currentVariant=t,n.selectedOptions=[...t.options],t.featured_media){n.currentMediaId=t.featured_media.id,n.mediaContextVariantId=t.id;return}if(n.currentMediaId===null){const a=document.querySelector('[data-js="media-item"]'),o=Number(a?.dataset.mediaId??""),r=Number((a?.dataset.variantMedia??"").split(",")[0]);n.currentMediaId=o||null,n.mediaContextVariantId=r||n.mediaContextVariantId}}function l(t){return t instanceof HTMLElement}function I(){const t=document.querySelector('[data-js="product-data"]');if(!t?.textContent)return;n.productData=JSON.parse(t.textContent);const a=document.querySelector('[data-js="variant-prices"]');n.variantPrices=a?.textContent?JSON.parse(a.textContent):{},m(u(n.productData));let o=!1;const r=()=>{o||(o=!0,E())},i=e=>{!l(e.target)||!e.target.closest('[data-js="product-form"], [data-product-media], [data-product-thumbnails]')||(r(),document.removeEventListener("pointerdown",i,!0),document.removeEventListener("keydown",d,!0))},d=e=>{e.key!=="Enter"&&e.key!==" "||!l(e.target)||!e.target.closest('[data-js="product-form"], [data-product-media], [data-product-thumbnails]')||(r(),document.removeEventListener("pointerdown",i,!0),document.removeEventListener("keydown",d,!0))};document.addEventListener("pointerdown",i,!0),document.addEventListener("keydown",d,!0);const s=document.querySelector('[data-js="product-form"]');s?.addEventListener("click",e=>{f(e)}),s?.addEventListener("submit",e=>{r(),v(e)}),document.addEventListener("click",e=>{L(e)}),window.addEventListener("popstate",()=>{m(u(n.productData)),n.cartState="idle",c()}),c()}document.addEventListener("DOMContentLoaded",I); diff --git a/assets/sync-BRYiu7LQ.js b/assets/sync-BRYiu7LQ.js new file mode 100644 index 000000000..be04a28c6 --- /dev/null +++ b/assets/sync-BRYiu7LQ.js @@ -0,0 +1 @@ +import{g as p}from"./variant-picker-DRrQ71gT.js";import{s as a}from"./state-Bwp_aMHz.js";function b(t,e){return t?t.split(",").some(r=>r.trim()===e):!1}function y(t){if(!t)return;const e=document.querySelector("[data-product-media]"),r=document.querySelector("[data-product-thumbnails]");if(e){const n=e.querySelector(`[data-js="media-item"][data-media-id="${t}"]`);n&&e.firstElementChild!==n&&e.prepend(n)}if(r){const n=r.querySelector(`[data-js="thumbnail"][data-thumbnail="${t}"]`);n&&r.firstElementChild!==n&&r.prepend(n)}}function S(t,e){return t.find(n=>b(n.dataset.variantMedia,e))?.dataset.mediaId??null}function E(){v(),h(),V(),A(),g(),I(),M()}function v(){const t=document.querySelector('[data-js="product-price"]');if(!t)return;const e=a.variantPrices[String(a.currentVariant.id)];e&&(t.textContent=e)}function h(){const t=document.querySelector('[data-js="product-availability"]');t&&(t.textContent=a.currentVariant.available?"":"Sold out")}function V(){const t=Array.from(document.querySelectorAll('[data-js="media-item"]'));if(t.length===0)return;const e=a.mediaContextVariantId!==null?String(a.mediaContextVariantId):String(a.currentVariant.id),r=S(t,e),n=a.currentMediaId!==null?String(a.currentMediaId):null,o=n?t.find(i=>i.dataset.mediaId===n):null,d=o?!o.dataset.variantMedia:!1,s=t[0]?.dataset.mediaId??null;let c=!1;t.forEach(i=>{const l=!i.dataset.variantMedia,u=r!==null&&i.dataset.mediaId===r;l||u?(i.removeAttribute("hidden"),c=!0):(i.setAttribute("hidden",""),i.querySelector("video")?.pause())}),!c&&s&&t.forEach(i=>{i.dataset.mediaId===s?i.removeAttribute("hidden"):(i.setAttribute("hidden",""),i.querySelector("video")?.pause())});const m=c?r??(d?n:null)??s:s;y(r??m),document.querySelectorAll('[data-js="thumbnail"]').forEach(i=>{const l=!i.dataset.variantMedia,u=r!==null&&i.dataset.thumbnail===r;l||u?i.removeAttribute("hidden"):i.setAttribute("hidden",""),i.setAttribute("aria-pressed",String(i.dataset.thumbnail===m))})}function g(){const t=document.querySelector('[data-js="add-to-cart"]');if(!t)return;const e={idle:a.currentVariant.available?"Add to cart":"Sold out",loading:"Adding...",success:"Added!",error:"Try again"};t.disabled=!a.currentVariant.available||a.cartState==="loading",t.setAttribute("aria-busy",String(a.cartState==="loading")),t.textContent=e[a.cartState]}function A(){const t=document.querySelector('[data-js="variant-id"]');t&&(t.value=String(a.currentVariant.id))}function I(){const t=document.querySelector('[data-js="cart-status"]');if(!t)return;const e={idle:a.currentVariant.available?"":"This variant is sold out.",loading:"",success:"Added to cart.",error:"Could not add to cart. Please try again."};t.textContent=e[a.cartState]}function M(){document.querySelectorAll('[data-js="option-value"]').forEach(t=>{const e=Number(t.dataset.optionPosition)-1,r=t.dataset.optionValue??"",n=a.selectedOptions[e]===r;t.setAttribute("aria-pressed",String(n));const d=p(a.productData.variants,a.selectedOptions,e).has(r);t.setAttribute("aria-disabled",String(!d)),t.disabled=!d}),document.querySelectorAll('[data-js="option-label"]').forEach(t=>{const e=Number(t.dataset.optionLabel)-1;t.textContent=a.selectedOptions[e]??""})}export{E as s}; diff --git a/assets/sync-YStD_kYI.js b/assets/sync-YStD_kYI.js deleted file mode 100644 index 47450335e..000000000 --- a/assets/sync-YStD_kYI.js +++ /dev/null @@ -1 +0,0 @@ -import{g as v}from"./variant-picker-DRrQ71gT.js";import{s as e}from"./state-Bwp_aMHz.js";function m(t,i){return t?t.split(",").some(n=>n.trim()===i):!1}function C(){b(),p(),g(),A(),y(),h(),V()}function b(){const t=document.querySelector('[data-js="product-price"]');if(!t)return;const i=e.variantPrices[String(e.currentVariant.id)];i&&(t.textContent=i)}function p(){const t=document.querySelector('[data-js="product-availability"]');t&&(t.textContent=e.currentVariant.available?"":"Sold out")}function g(){const t=document.querySelectorAll('[data-js="media-item"]');if(t.length===0)return;const i=e.mediaContextVariantId!==null?String(e.mediaContextVariantId):String(e.currentVariant.id),n=e.currentMediaId!==null?String(e.currentMediaId):e.currentVariant.featured_media!==null?String(e.currentVariant.featured_media.id):null,o=n!==null&&Array.from(t).some(a=>a.dataset.mediaId===n)?n:null,r=t[0]?.dataset.mediaId??null;let d=!1;t.forEach(a=>{const s=a.dataset.variantMedia,c=!s,l=m(s,i);let u;o!==null?u=a.dataset.mediaId===o:u=c||l,u?(a.removeAttribute("hidden"),d=!0):(a.setAttribute("hidden",""),a.querySelector("video")?.pause())}),!d&&r&&t.forEach(a=>{a.dataset.mediaId===r?a.removeAttribute("hidden"):(a.setAttribute("hidden",""),a.querySelector("video")?.pause())});const S=d?o:r;document.querySelectorAll('[data-js="thumbnail"]').forEach(a=>{const s=a.dataset.variantMedia,c=!s,l=m(s,i);c||l?a.removeAttribute("hidden"):a.setAttribute("hidden",""),a.setAttribute("aria-pressed",String(a.dataset.thumbnail===S))})}function y(){const t=document.querySelector('[data-js="add-to-cart"]');if(!t)return;const i={idle:e.currentVariant.available?"Add to cart":"Sold out",loading:"Adding...",success:"Added!",error:"Try again"};t.disabled=!e.currentVariant.available||e.cartState==="loading",t.setAttribute("aria-busy",String(e.cartState==="loading")),t.textContent=i[e.cartState]}function A(){const t=document.querySelector('[data-js="variant-id"]');t&&(t.value=String(e.currentVariant.id))}function h(){const t=document.querySelector('[data-js="cart-status"]');if(!t)return;const i={idle:e.currentVariant.available?"":"This variant is sold out.",loading:"",success:"Added to cart.",error:"Could not add to cart. Please try again."};t.textContent=i[e.cartState]}function V(){document.querySelectorAll('[data-js="option-value"]').forEach(t=>{const i=Number(t.dataset.optionPosition)-1,n=t.dataset.optionValue??"",f=e.selectedOptions[i]===n;t.setAttribute("aria-pressed",String(f));const r=v(e.productData.variants,e.selectedOptions,i).has(n);t.setAttribute("aria-disabled",String(!r)),t.disabled=!r}),document.querySelectorAll('[data-js="option-label"]').forEach(t=>{const i=Number(t.dataset.optionLabel)-1;t.textContent=e.selectedOptions[i]??""})}export{C as s}; diff --git a/frontend/entrypoints/css/main.css b/frontend/entrypoints/css/main.css index f298ffa02..9193a932d 100644 --- a/frontend/entrypoints/css/main.css +++ b/frontend/entrypoints/css/main.css @@ -7,4 +7,98 @@ @source "../../../templates"; @theme { + /* Colors */ + --color-*: initial; + + /* Typo */ + --text-*: initial; + /*--text-h1: var(--h1); + --text-h1--line-height: var(--h1--line-height); + --text-h1--font-weight: var(--h1--font-weight);*/ + + /* Spacing */ + --spacing-*: initial; + --spacing-0: 0; + --spacing-col1: 8.33333333333%; + --spacing-col2: 16.66666666667%; + --spacing-col3: 25%; + --spacing-col4: 33.33333333333%; + --spacing-col5: 41.66666666667%; + --spacing-col6: 50%; + --spacing-col7: 58.33333333333%; + --spacing-col8: 66.66666666667%; + --spacing-col9: 75%; + --spacing-col10: 83.33333333333%; + --spacing-col11: 91.66666666667%; + --spacing-col12: 100%; + + /* Gap */ + --gap-*: initial; + + /* Margin */ + --margin-*: initial; + + /* Flex */ + --flex-col1: 8.33333333333%; + --flex-col2: 16.66666666667%; + --flex-col3: 25%; + --flex-col4: 33.33333333333%; + --flex-col5: 41.66666666667%; + --flex-col6: 50%; + --flex-col7: 58.33333333333%; + --flex-col8: 66.66666666667%; + --flex-col9: 75%; + --flex-col10: 83.33333333333%; + --flex-col11: 91.66666666667%; + --flex-col12: 100%; +} + +html { + overscroll-behavior: none; + + @media (min-width: 768px) { + scrollbar-gutter: stable; + -webkit-scroll-gutter: stable; + } +} + +body { + font-smooth: antialiased; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + overscroll-behavior: none; +} + +img, +svg, +video { + pointer-events: none; + user-select: none; +} + +dialog { + max-width: none; + max-height: none; +} + +@utility scrollbar-hidden { + &::-webkit-scrollbar { + display: none; + } +} + +@utility scrollbar-visible { + &::-webkit-scrollbar { + width: 2px; + height: 2px; + } + + &::-webkit-scrollbar-track { + background-color: var(--color-bg-lightgrey); + } + + &::-webkit-scrollbar-thumb { + background-color: var(--color-border-lightgrey); + border-radius: 2px; + } } diff --git a/frontend/entrypoints/ts/product.ts b/frontend/entrypoints/ts/product.ts index 2b72828d2..0bca608ca 100644 --- a/frontend/entrypoints/ts/product.ts +++ b/frontend/entrypoints/ts/product.ts @@ -38,8 +38,8 @@ function isOptionClick(event: Event): boolean { return Boolean((event.target as HTMLElement).closest('[data-js="option-value"]')); } -function isThumbnailClick(event: Event): boolean { - return Boolean((event.target as HTMLElement).closest('[data-js="thumbnail"]')); +function isProductInteractionTarget(target: EventTarget | null): target is HTMLElement { + return target instanceof HTMLElement; } /** @@ -66,11 +66,38 @@ function init(): void { loadRecommendations(); }; + const onFirstPointerDown = (event: PointerEvent): void => { + if (!isProductInteractionTarget(event.target)) return; + + const isProductInteraction = Boolean( + event.target.closest('[data-js="product-form"], [data-product-media], [data-product-thumbnails]'), + ); + if (!isProductInteraction) return; + + loadRecommendationsOnce(); + document.removeEventListener('pointerdown', onFirstPointerDown, true); + document.removeEventListener('keydown', onFirstKeyDown, true); + }; + + const onFirstKeyDown = (event: KeyboardEvent): void => { + if (event.key !== 'Enter' && event.key !== ' ') return; + if (!isProductInteractionTarget(event.target)) return; + + const isProductInteraction = Boolean( + event.target.closest('[data-js="product-form"], [data-product-media], [data-product-thumbnails]'), + ); + if (!isProductInteraction) return; + + loadRecommendationsOnce(); + document.removeEventListener('pointerdown', onFirstPointerDown, true); + document.removeEventListener('keydown', onFirstKeyDown, true); + }; + + document.addEventListener('pointerdown', onFirstPointerDown, true); + document.addEventListener('keydown', onFirstKeyDown, true); + const form = document.querySelector('[data-js="product-form"]'); form?.addEventListener('click', (event) => { - if (isOptionClick(event)) { - loadRecommendationsOnce(); - } onOptionClick(event); }); form?.addEventListener('submit', (e) => { @@ -79,9 +106,6 @@ function init(): void { }); document.addEventListener('click', (event) => { - if (isThumbnailClick(event)) { - loadRecommendationsOnce(); - } onThumbnailClick(event); }); window.addEventListener('popstate', () => { diff --git a/frontend/entrypoints/ts/product/sync.ts b/frontend/entrypoints/ts/product/sync.ts index 685e7252d..f884e9323 100644 --- a/frontend/entrypoints/ts/product/sync.ts +++ b/frontend/entrypoints/ts/product/sync.ts @@ -6,6 +6,36 @@ function matchesVariantOwner(ownerVariantIds: string | undefined, variantId: str return ownerVariantIds.split(',').some((id) => id.trim() === variantId); } +function moveActiveMediaToFront(activeMediaId: string | null): void { + if (!activeMediaId) return; + + const mediaContainer = document.querySelector('[data-product-media]'); + const thumbnailContainer = document.querySelector('[data-product-thumbnails]'); + + if (mediaContainer) { + const activeMedia = mediaContainer.querySelector( + `[data-js="media-item"][data-media-id="${activeMediaId}"]`, + ); + if (activeMedia && mediaContainer.firstElementChild !== activeMedia) { + mediaContainer.prepend(activeMedia); + } + } + + if (thumbnailContainer) { + const activeThumbnail = thumbnailContainer.querySelector( + `[data-js="thumbnail"][data-thumbnail="${activeMediaId}"]`, + ); + if (activeThumbnail && thumbnailContainer.firstElementChild !== activeThumbnail) { + thumbnailContainer.prepend(activeThumbnail); + } + } +} + +function resolvePrimaryVariantMediaId(items: HTMLElement[], variantId: string): string | null { + const primary = items.find((item) => matchesVariantOwner(item.dataset.variantMedia, variantId)); + return primary?.dataset.mediaId ?? null; +} + /** Calls all sync helpers in sequence after any state change. */ export function syncDOM(): void { syncPrice(); @@ -34,7 +64,7 @@ function syncAvailability(): void { /** Shows the media item matching `currentMediaId` (or variant featured media), hides all others. */ function syncMedia(): void { - const items = document.querySelectorAll('[data-js="media-item"]'); + const items = Array.from(document.querySelectorAll('[data-js="media-item"]')); if (items.length === 0) return; const activeMediaContextVariantId = @@ -42,16 +72,12 @@ function syncMedia(): void { ? String(state.mediaContextVariantId) : String(state.currentVariant.id); - const targetId = - state.currentMediaId !== null - ? String(state.currentMediaId) - : state.currentVariant.featured_media !== null - ? String(state.currentVariant.featured_media.id) - : null; + const primaryVariantMediaId = resolvePrimaryVariantMediaId(items, activeMediaContextVariantId); + + const targetId = state.currentMediaId !== null ? String(state.currentMediaId) : null; + const targetNode = targetId ? items.find((item) => item.dataset.mediaId === targetId) : null; + const targetIsSharedMedia = targetNode ? !targetNode.dataset.variantMedia : false; - const hasTargetMedia = - targetId !== null && Array.from(items).some((item) => item.dataset.mediaId === targetId); - const effectiveTargetId = hasTargetMedia ? targetId : null; const fallbackVisibleMediaId = items[0]?.dataset.mediaId ?? null; let hasVisibleMedia = false; @@ -59,16 +85,9 @@ function syncMedia(): void { items.forEach((item) => { const ownerVariantIds = item.dataset.variantMedia; const isShared = !ownerVariantIds; - const isActiveContextMedia = matchesVariantOwner(ownerVariantIds, activeMediaContextVariantId); + const isPrimaryVariantMedia = primaryVariantMediaId !== null && item.dataset.mediaId === primaryVariantMediaId; - let isVisible: boolean; - if (effectiveTargetId !== null) { - isVisible = item.dataset.mediaId === effectiveTargetId; - } else { - // No targeted media: show shared + active media context - // If no variant images exist at all (all isShared), this shows everything — unchanged behaviour - isVisible = isShared || isActiveContextMedia; - } + const isVisible = isShared || isPrimaryVariantMedia; if (isVisible) { item.removeAttribute('hidden'); @@ -90,15 +109,18 @@ function syncMedia(): void { }); } - const activeMediaId = hasVisibleMedia ? effectiveTargetId : fallbackVisibleMediaId; + const preferredVisibleMediaId = primaryVariantMediaId ?? (targetIsSharedMedia ? targetId : null) ?? fallbackVisibleMediaId; + const activeMediaId = hasVisibleMedia ? preferredVisibleMediaId : fallbackVisibleMediaId; + moveActiveMediaToFront(primaryVariantMediaId ?? activeMediaId); // Thumbnail visibility: shared always visible; variant thumbnails only when active document.querySelectorAll('[data-js="thumbnail"]').forEach((btn) => { const ownerVariantIds = btn.dataset.variantMedia; const isShared = !ownerVariantIds; - const isActiveContextMedia = matchesVariantOwner(ownerVariantIds, activeMediaContextVariantId); + const isPrimaryVariantMedia = + primaryVariantMediaId !== null && btn.dataset.thumbnail === primaryVariantMediaId; - if (isShared || isActiveContextMedia) { + if (isShared || isPrimaryVariantMedia) { btn.removeAttribute('hidden'); } else { btn.setAttribute('hidden', ''); diff --git a/snippets/vite-tag.liquid b/snippets/vite-tag.liquid index a49ab59d4..c9cd99e7e 100644 --- a/snippets/vite-tag.liquid +++ b/snippets/vite-tag.liquid @@ -6,71 +6,28 @@ assign entry = entry | default: vite-tag assign path = entry | replace: '@ts/', 'ts/' | replace: '@css/', 'css/' | replace: '~/', '../' | replace: '@/', '../' %} -{% if path == "/frontend/entrypoints/css/main.css" or path == "css/main.css" %} - {{ 'main-BF2QWjU5.css' | asset_url | split: '?' | first | stylesheet_tag: preload: preload_stylesheet }} -{% elsif path == "/frontend/entrypoints/ts/404.ts" or path == "ts/404.ts" %} - -{% elsif path == "/frontend/entrypoints/ts/article.ts" or path == "ts/article.ts" %} - -{% elsif path == "/frontend/entrypoints/ts/blog.ts" or path == "ts/blog.ts" %} - -{% elsif path == "/frontend/entrypoints/ts/cart.ts" or path == "ts/cart.ts" %} - - - - -{% elsif path == "/frontend/entrypoints/ts/cart/drawer.ts" or path == "ts/cart/drawer.ts" %} - - - -{% elsif path == "/frontend/entrypoints/ts/cart/page.ts" or path == "ts/cart/page.ts" %} - - - -{% elsif path == "/frontend/entrypoints/ts/collection.ts" or path == "ts/collection.ts" %} - -{% elsif path == "/frontend/entrypoints/ts/gift-card.ts" or path == "ts/gift-card.ts" %} - -{% elsif path == "/frontend/entrypoints/ts/index.ts" or path == "ts/index.ts" %} - -{% elsif path == "/frontend/entrypoints/ts/list-collections.ts" or path == "ts/list-collections.ts" %} - -{% elsif path == "/frontend/entrypoints/ts/page.ts" or path == "ts/page.ts" %} - -{% elsif path == "/frontend/entrypoints/ts/password.ts" or path == "ts/password.ts" %} - -{% elsif path == "/frontend/entrypoints/ts/product.ts" or path == "ts/product.ts" %} - - - - - - - -{% elsif path == "/frontend/entrypoints/ts/product/handlers.ts" or path == "ts/product/handlers.ts" %} - - - - - -{% elsif path == "/frontend/entrypoints/ts/product/recommendations.ts" or path == "ts/product/recommendations.ts" %} - -{% elsif path == "/frontend/entrypoints/ts/product/state.ts" or path == "ts/product/state.ts" %} - -{% elsif path == "/frontend/entrypoints/ts/product/sync.ts" or path == "ts/product/sync.ts" %} - - - -{% elsif path == "/frontend/entrypoints/ts/search.ts" or path == "ts/search.ts" %} - -{% elsif path == "/frontend/entrypoints/ts/search/drawer.ts" or path == "ts/search/drawer.ts" %} - -{% elsif path == "/frontend/entrypoints/ts/theme.ts" or path == "ts/theme.ts" %} - -{% elsif path == "/frontend/entrypoints/ts/utils/cart.ts" or path == "ts/utils/cart.ts" %} - -{% elsif path == "/frontend/entrypoints/ts/utils/section-rendering.ts" or path == "ts/utils/section-rendering.ts" %} - -{% elsif path == "/frontend/entrypoints/ts/utils/variant-picker.ts" or path == "ts/utils/variant-picker.ts" %} - +{% liquid + assign path_prefix = path | slice: 0 + if path_prefix == '/' + assign file_url_prefix = 'http://localhost:5173' + else + assign file_url_prefix = 'http://localhost:5173/frontend/entrypoints/' + endif + assign file_url = path | prepend: file_url_prefix + assign file_name = path | split: '/' | last + if file_name contains '.' + assign file_extension = file_name | split: '.' | last + endif + assign css_extensions = 'css|less|sass|scss|styl|stylus|pcss|postcss' | split: '|' + assign is_css = false + if css_extensions contains file_extension + assign is_css = true + endif +%} + + +{% if is_css == true %} + +{% else %} + {% endif %} diff --git a/todo.md b/todo.md index 205bdd9f4..2d2eb702a 100644 --- a/todo.md +++ b/todo.md @@ -1,67 +1,20 @@ # TODO - Shopify Skeleton Theme -## Status Snapshot -- [x] P0 complete: section rendering foundation, Shopify docs parity, PDP media reliability -- [x] P1 mostly complete: PLP stability, predictive search drawer, accessibility hardening, customization docs -- [ ] Final P1 verification pass pending (mobile PLP + quality/network checks) - ---- - -## Now (Top Priority) - -### 1) PLP final mobile verification -- [ ] Verify mobile filter UX has no regressions (open/close/apply/clear/back-forward) - -Acceptance: -- [ ] PLP interactions are stable on mobile across refresh/back/forward - -### 2) Quality gates and behavior checks -- [x] `bun run typecheck` -- [x] `bun run build` -- [ ] `theme-check` (if available locally) -- [ ] Manual smoke checks: PDP, PLP, cart page, minicart, search drawer -- [x] Verify network: no cart/recommendation API call before first user interaction - -Acceptance: -- [ ] Checks pass and behavior is validated after the final pass - -Notes: -- `theme-check` is currently unavailable in this environment (`command not found` / local CLI dependency issue) - ---- +## Now +- [ ] Verify mobile PLP filters on real devices (open/close/apply/clear/back-forward) +- [ ] Run manual smoke pass: PDP, PLP, cart page, minicart, search drawer +- [ ] Run `theme-check` when local Shopify CLI/Node deps are fixed ## Next +- [ ] P2 cleanup: copy consistency, small interaction polish, remove dead notes -### P2 - UX polish and cleanup -- [ ] Final cleanup pass (copy consistency, minor interaction polish, remove dead notes) - ---- - -## Completed Archive (Condensed) - -### Architecture and docs-first contracts -- [x] TS behavior and Liquid markup split documented and enforced -- [x] Stable `data-js` contracts documented for cart, product, collection, search -- [x] AI contributor guides updated (`CLAUDE.md`, `AGENTS.md`, `README.md`) - -### Cart and section rendering -- [x] Shared section rendering utility implemented and reused -- [x] Cart page and drawer updates driven by bundled section rendering responses -- [x] `sections_url` normalization and locale-aware routes validated -- [x] `null`/invalid section responses handled with recoverable UI fallback -- [x] Request sequencing guard added for rapid quantity/remove actions - -### PDP media reliability -- [x] Variant/media behavior aligned to Dawn expectations -- [x] No media disappearance on size-only changes -- [x] Rapid option-switch handling avoids blank gallery states - -### PLP and search drawer -- [x] Collection interactions stabilized (URL/history/state, deterministic clear/reset) -- [x] Predictive search drawer added (open/close, focus trap, ESC, restore focus) -- [x] Predictive API integration follows docs (`/search/suggest.json`, resources params) -- [x] Debounce + `AbortController` + stale-response guards implemented +## Done (Latest) +- [x] P0 complete: section rendering foundation, Shopify docs parity, PDP media reliability +- [x] P1 complete except final manual verification +- [x] Search and cart core hardening shipped (predictive drawer, cart notes/attributes) +- [x] PDP recommendations no longer tied to variant switching +- [x] PDP media now keeps variant-primary media first in viewer + thumbnails while keeping shared media +- [x] Core checks passing in this environment: `bun run typecheck`, `bun run build` -### Accessibility hardening -- [x] Keyboard-first flows verified for core cart/search/product interactions -- [x] Live-region and error/status communication improved without layout shift +## Notes +- `theme-check` is currently unavailable in this environment (`command not found` / local CLI dependency issue) From 85a54cde2bdbe8949cd3b50f3ec8efb7da2cecba Mon Sep 17 00:00:00 2001 From: Luca Argentieri Date: Mon, 23 Mar 2026 16:03:42 +0100 Subject: [PATCH 13/14] add reusable product-card quick buy and cart update events Extract product cards into reusable snippets, add Italian locale strings, and wire cart:updated/cart:open events so quick buy and PDP add-to-cart keep header count in sync and open the minicart. --- AGENTS.md | 2 +- README.md | 2 +- assets/.vite/manifest.json | 29 ++++++--- assets/cart-events-C4m0PwR1.js | 1 + assets/collection-Bqtxr7aJ.js | 1 + assets/collection-KDkQRAB3.js | 1 - assets/drawer-CJwC366z.js | 1 + assets/drawer-DzmJHKNX.js | 1 - assets/handlers-B949bjPl.js | 1 + assets/handlers-BekC_4hw.js | 1 - assets/main-BedzLXBi.css | 1 - assets/main-DNSsntQO.css | 1 + assets/product-CbP3NaeG.js | 1 - assets/product-_rxK6zOl.js | 1 + .../{theme-C1WNKfRD.js => theme-skURl5jI.js} | 4 +- frontend/entrypoints/css/main.css | 18 +++--- frontend/entrypoints/ts/cart/drawer.ts | 26 ++++++++ frontend/entrypoints/ts/collection.ts | 53 +++++++++++++++++ frontend/entrypoints/ts/product/handlers.ts | 3 + frontend/entrypoints/ts/utils/cart-events.ts | 16 +++++ locales/en.default.json | 7 ++- locales/it.json | 59 +++++++++++++++++++ sections/collection.liquid | 17 +----- sections/product-recommendations.liquid | 9 +-- snippets/product-card-quick-buy.liquid | 27 +++++++++ snippets/product-card.liquid | 39 ++++++++++++ todo.md | 1 + 27 files changed, 272 insertions(+), 51 deletions(-) create mode 100644 assets/cart-events-C4m0PwR1.js create mode 100644 assets/collection-Bqtxr7aJ.js delete mode 100644 assets/collection-KDkQRAB3.js create mode 100644 assets/drawer-CJwC366z.js delete mode 100644 assets/drawer-DzmJHKNX.js create mode 100644 assets/handlers-B949bjPl.js delete mode 100644 assets/handlers-BekC_4hw.js delete mode 100644 assets/main-BedzLXBi.css create mode 100644 assets/main-DNSsntQO.css delete mode 100644 assets/product-CbP3NaeG.js create mode 100644 assets/product-_rxK6zOl.js rename assets/{theme-C1WNKfRD.js => theme-skURl5jI.js} (84%) create mode 100644 frontend/entrypoints/ts/utils/cart-events.ts create mode 100644 locales/it.json create mode 100644 snippets/product-card-quick-buy.liquid create mode 100644 snippets/product-card.liquid diff --git a/AGENTS.md b/AGENTS.md index a482c9123..f4af68d88 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -36,7 +36,7 @@ This file is for any AI coding assistant (Claude, Codex, Cursor, etc.) working i - Cart drawer: `cart-drawer`, `cart-open`, `cart-close`, `cart-items`, `cart-empty`, `cart-subtotal` - Cart page: `cart-page`, `cart-page-items`, `cart-page-empty`, `cart-page-footer`, `cart-page-subtotal` - Product: `product-form`, `option-value`, `thumbnail`, `add-to-cart`, `cart-status` -- Collection: `collection-root`, `collection-controls`, `collection-products`, `collection-load-more` +- Collection: `collection-root`, `collection-controls`, `collection-products`, `collection-load-more`, `collection-quick-buy` - Search drawer: `search-drawer`, `search-open`, `search-close`, `search-drawer-input`, `search-drawer-groups` ## Local Validation diff --git a/README.md b/README.md index 876e65c63..8ca5cf60f 100644 --- a/README.md +++ b/README.md @@ -154,7 +154,7 @@ - Cart drawer: `cart-drawer`, `cart-open`, `cart-close`, `cart-items`, `cart-empty`, `cart-subtotal` - Cart page: `cart-page`, `cart-page-items`, `cart-page-empty`, `cart-page-footer`, `cart-page-subtotal` - Product: `product-form`, `option-value`, `thumbnail`, `add-to-cart`, `cart-status` - - Collection: `collection-root`, `collection-controls`, `collection-products`, `collection-load-more` + - Collection: `collection-root`, `collection-controls`, `collection-products`, `collection-load-more`, `collection-quick-buy` - Search drawer: `search-drawer`, `search-open`, `search-close`, `search-drawer-input`, `search-drawer-groups` --- diff --git a/assets/.vite/manifest.json b/assets/.vite/manifest.json index 26ce89f15..628eb2b0f 100644 --- a/assets/.vite/manifest.json +++ b/assets/.vite/manifest.json @@ -1,6 +1,6 @@ { "frontend/entrypoints/css/main.css": { - "file": "main-BedzLXBi.css", + "file": "main-DNSsntQO.css", "src": "frontend/entrypoints/css/main.css", "isEntry": true, "name": "main", @@ -38,13 +38,14 @@ ] }, "frontend/entrypoints/ts/cart/drawer.ts": { - "file": "drawer-DzmJHKNX.js", + "file": "drawer-CJwC366z.js", "name": "drawer", "src": "frontend/entrypoints/ts/cart/drawer.ts", "isEntry": true, "isDynamicEntry": true, "imports": [ "frontend/entrypoints/ts/utils/cart.ts", + "frontend/entrypoints/ts/utils/cart-events.ts", "frontend/entrypoints/ts/utils/section-rendering.ts" ] }, @@ -59,10 +60,14 @@ ] }, "frontend/entrypoints/ts/collection.ts": { - "file": "collection-KDkQRAB3.js", + "file": "collection-Bqtxr7aJ.js", "name": "collection", "src": "frontend/entrypoints/ts/collection.ts", - "isEntry": true + "isEntry": true, + "imports": [ + "frontend/entrypoints/ts/utils/cart.ts", + "frontend/entrypoints/ts/utils/cart-events.ts" + ] }, "frontend/entrypoints/ts/gift-card.ts": { "file": "gift-card-l0sNRNKZ.js", @@ -95,7 +100,7 @@ "isEntry": true }, "frontend/entrypoints/ts/product.ts": { - "file": "product-CbP3NaeG.js", + "file": "product-_rxK6zOl.js", "name": "product", "src": "frontend/entrypoints/ts/product.ts", "isEntry": true, @@ -105,17 +110,19 @@ "frontend/entrypoints/ts/product/handlers.ts", "frontend/entrypoints/ts/product/recommendations.ts", "frontend/entrypoints/ts/utils/variant-picker.ts", - "frontend/entrypoints/ts/utils/cart.ts" + "frontend/entrypoints/ts/utils/cart.ts", + "frontend/entrypoints/ts/utils/cart-events.ts" ] }, "frontend/entrypoints/ts/product/handlers.ts": { - "file": "handlers-BekC_4hw.js", + "file": "handlers-B949bjPl.js", "name": "handlers", "src": "frontend/entrypoints/ts/product/handlers.ts", "isEntry": true, "imports": [ "frontend/entrypoints/ts/utils/variant-picker.ts", "frontend/entrypoints/ts/utils/cart.ts", + "frontend/entrypoints/ts/utils/cart-events.ts", "frontend/entrypoints/ts/product/state.ts", "frontend/entrypoints/ts/product/sync.ts" ] @@ -156,7 +163,7 @@ "isDynamicEntry": true }, "frontend/entrypoints/ts/theme.ts": { - "file": "theme-C1WNKfRD.js", + "file": "theme-skURl5jI.js", "name": "theme", "src": "frontend/entrypoints/ts/theme.ts", "isEntry": true, @@ -165,6 +172,12 @@ "frontend/entrypoints/ts/search/drawer.ts" ] }, + "frontend/entrypoints/ts/utils/cart-events.ts": { + "file": "cart-events-C4m0PwR1.js", + "name": "cart-events", + "src": "frontend/entrypoints/ts/utils/cart-events.ts", + "isEntry": true + }, "frontend/entrypoints/ts/utils/cart.ts": { "file": "cart-Kw4e-iq4.js", "name": "cart", diff --git a/assets/cart-events-C4m0PwR1.js b/assets/cart-events-C4m0PwR1.js new file mode 100644 index 000000000..6d51f8a7c --- /dev/null +++ b/assets/cart-events-C4m0PwR1.js @@ -0,0 +1 @@ +const n="cart:updated",e="cart:open";function a(t){window.dispatchEvent(new CustomEvent(n,{detail:t}))}function o(){window.dispatchEvent(new CustomEvent(e))}export{e as C,o as a,n as b,a as e}; diff --git a/assets/collection-Bqtxr7aJ.js b/assets/collection-Bqtxr7aJ.js new file mode 100644 index 000000000..7da665f2a --- /dev/null +++ b/assets/collection-Bqtxr7aJ.js @@ -0,0 +1 @@ +import{a as v}from"./cart-Kw4e-iq4.js";import{e as j,a as A}from"./cart-events-C4m0PwR1.js";const L='[data-js="collection-root"]';function b(t){return{root:t,sectionId:t.dataset.sectionId??"",controls:t.querySelector('[data-js="collection-controls"]'),products:t.querySelector('[data-js="collection-products"]'),paginationWrap:t.querySelector('[data-js="collection-pagination-wrap"]'),loadStatus:t.querySelector('[data-js="collection-load-status"]'),status:t.querySelector('[data-js="collection-status"]'),error:t.querySelector('[data-js="collection-error"]')}}function w(t,o){t.status&&(t.status.textContent=o)}function y(t,o){t.loadStatus&&(t.loadStatus.textContent=o)}function C(t,o){t.error&&(t.error.textContent=o)}function S(t,o){t.root.setAttribute("aria-busy",String(o))}async function D(t,o){const e=Number(t.dataset.variantId??"");if(!e){o&&window.location.assign(o);return}const c=t.dataset.labelIdle??"Quick buy",l=t.dataset.labelLoading??"Adding...",f=t.dataset.labelSuccess??"Added",d=t.dataset.labelError??"Try again";t.disabled=!0,t.setAttribute("aria-busy","true"),t.textContent=l;try{await v(e,1),j({itemCountDelta:1}),A(),t.textContent=f,window.setTimeout(()=>{t.textContent=c,t.disabled=!1,t.setAttribute("aria-busy","false")},1200)}catch{t.textContent=d,window.setTimeout(()=>{t.textContent=c,t.disabled=!1,t.setAttribute("aria-busy","false")},1500)}}function u(t){return new URL(t,window.location.origin)}function k(t,o){const e=new URL(t.toString());return e.searchParams.set("section_id",o),`${e.pathname}${e.search}`}function I(t){const o=u(t.action||window.location.href),e=new URLSearchParams;return new FormData(t).forEach((l,f)=>{const d=String(l).trim();d.length>0&&e.append(f,d)}),o.search=e.toString(),o}function U(t){return new DOMParser().parseFromString(t,"text/html").querySelector(L)}document.addEventListener("DOMContentLoaded",()=>{const t=document.querySelector(L);if(!t)return;let o=t,e=b(o);if(!e.sectionId)return;let c=0,l=null;const f=a=>{o=a,e=b(a)},d=async(a,n)=>{l?.abort(),l=new AbortController;const i=await fetch(k(a,e.sectionId),{signal:l.signal,headers:{"X-Requested-With":"XMLHttpRequest"}});if(!i.ok)throw new Error("Collection section request failed");const r=await i.text();if(n!==c)throw new DOMException("Stale collection request","AbortError");const s=U(r);if(!s)throw new Error("Collection section parse failed");return s},p=async(a,n)=>{const i=++c;S(e,!0),C(e,""),w(e,"Loading products...");try{const r=await d(a,i);o.replaceWith(r),f(r),n&&window.history.pushState({source:"collection-replace"},"",`${a.pathname}${a.search}`),w(e,"Products updated."),y(e,"")}catch(r){if(r instanceof DOMException&&r.name==="AbortError")return;C(e,"Could not update collection right now. Please try again."),w(e,"Collection update failed.")}finally{i===c&&S(e,!1)}},q=async a=>{const n=u(a),i=++c;S(e,!0),C(e,""),y(e,"Loading more products...");try{const r=await d(n,i),s=b(r);if(!e.products||!s.products||!e.paginationWrap||!s.paginationWrap)throw new Error("Collection append targets missing");const g=document.createDocumentFragment();s.products.querySelectorAll('[data-js="collection-product-card"]').forEach(m=>{g.appendChild(m)}),e.products.appendChild(g),e.paginationWrap.replaceWith(s.paginationWrap),e.paginationWrap=s.paginationWrap,window.history.pushState({source:"collection-append"},"",`${n.pathname}${n.search}`),y(e,"More products loaded."),w(e,"Collection updated.")}catch(r){if(r instanceof DOMException&&r.name==="AbortError")return;C(e,"Could not load more products. Please try again."),y(e,"Load more failed.")}finally{i===c&&S(e,!1)}},x=a=>{const n=a.target,i=n.closest('[data-js="collection-quick-buy"]');if(i){if(a.preventDefault(),i.disabled)return;const h=i.dataset.productUrl??"";D(i,h);return}const r=n.closest('[data-js="collection-load-more"]');if(r){a.preventDefault();const h=r.dataset.nextUrl;if(!h||r.disabled)return;r.disabled=!0,r.setAttribute("aria-busy","true"),q(h).finally(()=>{r.disabled=!1,r.setAttribute("aria-busy","false")});return}const s=n.closest('[data-js="collection-clear"]');if(s){a.preventDefault(),p(u(s.href),!0);return}const g=n.closest('[data-js="collection-filter-remove"]');if(g){a.preventDefault(),p(u(g.href),!0);return}const m=n.closest('[data-js="collection-default-pagination"] a');m&&(a.preventDefault(),p(u(m.href),!0))},E=a=>{!a.target.closest("input, select")||!e.controls||e.controls.requestSubmit()},R=a=>{const n=a.target;n.matches('[data-js="collection-controls"]')&&(a.preventDefault(),p(I(n),!0))};document.addEventListener("click",a=>{o.contains(a.target)&&x(a)}),document.addEventListener("change",a=>{o.contains(a.target)&&E(a)}),document.addEventListener("submit",a=>{o.contains(a.target)&&R(a)}),window.addEventListener("popstate",()=>{p(u(window.location.href),!1)})}); diff --git a/assets/collection-KDkQRAB3.js b/assets/collection-KDkQRAB3.js deleted file mode 100644 index 84fbd6ca7..000000000 --- a/assets/collection-KDkQRAB3.js +++ /dev/null @@ -1 +0,0 @@ -const C='[data-js="collection-root"]';function q(e){return{root:e,sectionId:e.dataset.sectionId??"",controls:e.querySelector('[data-js="collection-controls"]'),products:e.querySelector('[data-js="collection-products"]'),paginationWrap:e.querySelector('[data-js="collection-pagination-wrap"]'),loadStatus:e.querySelector('[data-js="collection-load-status"]'),status:e.querySelector('[data-js="collection-status"]'),error:e.querySelector('[data-js="collection-error"]')}}function g(e,n){e.status&&(e.status.textContent=n)}function w(e,n){e.loadStatus&&(e.loadStatus.textContent=n)}function m(e,n){e.error&&(e.error.textContent=n)}function y(e,n){e.root.setAttribute("aria-busy",String(n))}function l(e){return new URL(e,window.location.origin)}function x(e,n){const t=new URL(e.toString());return t.searchParams.set("section_id",n),`${t.pathname}${t.search}`}function j(e){const n=l(e.action||window.location.href),t=new URLSearchParams;return new FormData(e).forEach((u,S)=>{const d=String(u).trim();d.length>0&&t.append(S,d)}),n.search=t.toString(),n}function v(e){return new DOMParser().parseFromString(e,"text/html").querySelector(C)}document.addEventListener("DOMContentLoaded",()=>{const e=document.querySelector(C);if(!e)return;let n=e,t=q(n);if(!t.sectionId)return;let s=0,u=null;const S=o=>{n=o,t=q(o)},d=async(o,a)=>{u?.abort(),u=new AbortController;const r=await fetch(x(o,t.sectionId),{signal:u.signal,headers:{"X-Requested-With":"XMLHttpRequest"}});if(!r.ok)throw new Error("Collection section request failed");const i=await r.text();if(a!==s)throw new DOMException("Stale collection request","AbortError");const c=v(i);if(!c)throw new Error("Collection section parse failed");return c},p=async(o,a)=>{const r=++s;y(t,!0),m(t,""),g(t,"Loading products...");try{const i=await d(o,r);n.replaceWith(i),S(i),a&&window.history.pushState({source:"collection-replace"},"",`${o.pathname}${o.search}`),g(t,"Products updated."),w(t,"")}catch(i){if(i instanceof DOMException&&i.name==="AbortError")return;m(t,"Could not update collection right now. Please try again."),g(t,"Collection update failed.")}finally{r===s&&y(t,!1)}},E=async o=>{const a=l(o),r=++s;y(t,!0),m(t,""),w(t,"Loading more products...");try{const i=await d(a,r),c=q(i);if(!t.products||!c.products||!t.paginationWrap||!c.paginationWrap)throw new Error("Collection append targets missing");const f=document.createDocumentFragment();c.products.querySelectorAll('[data-js="collection-product-card"]').forEach(h=>{f.appendChild(h)}),t.products.appendChild(f),t.paginationWrap.replaceWith(c.paginationWrap),t.paginationWrap=c.paginationWrap,window.history.pushState({source:"collection-append"},"",`${a.pathname}${a.search}`),w(t,"More products loaded."),g(t,"Collection updated.")}catch(i){if(i instanceof DOMException&&i.name==="AbortError")return;m(t,"Could not load more products. Please try again."),w(t,"Load more failed.")}finally{r===s&&y(t,!1)}},R=o=>{const a=o.target,r=a.closest('[data-js="collection-load-more"]');if(r){o.preventDefault();const h=r.dataset.nextUrl;if(!h||r.disabled)return;r.disabled=!0,r.setAttribute("aria-busy","true"),E(h).finally(()=>{r.disabled=!1,r.setAttribute("aria-busy","false")});return}const i=a.closest('[data-js="collection-clear"]');if(i){o.preventDefault(),p(l(i.href),!0);return}const c=a.closest('[data-js="collection-filter-remove"]');if(c){o.preventDefault(),p(l(c.href),!0);return}const f=a.closest('[data-js="collection-default-pagination"] a');f&&(o.preventDefault(),p(l(f.href),!0))},b=o=>{!o.target.closest("input, select")||!t.controls||t.controls.requestSubmit()},L=o=>{const a=o.target;a.matches('[data-js="collection-controls"]')&&(o.preventDefault(),p(j(a),!0))};document.addEventListener("click",o=>{n.contains(o.target)&&R(o)}),document.addEventListener("change",o=>{n.contains(o.target)&&b(o)}),document.addEventListener("submit",o=>{n.contains(o.target)&&L(o)}),window.addEventListener("popstate",()=>{p(l(window.location.href),!1)})}); diff --git a/assets/drawer-CJwC366z.js b/assets/drawer-CJwC366z.js new file mode 100644 index 000000000..b05e3427a --- /dev/null +++ b/assets/drawer-CJwC366z.js @@ -0,0 +1 @@ +import{c as W}from"./cart-Kw4e-iq4.js";import{C as X,b as Y}from"./cart-events-C4m0PwR1.js";import{n as Z,f as tt,a as et}from"./section-rendering-vVqNxfcB.js";const rt='a[href], button:not([disabled]), textarea, input, select, [tabindex]:not([tabindex="-1"])';function st(){const n=document.querySelector('[data-js="cart-drawer"]');if(!n)return;const b=n.querySelector('[data-js="cart-drawer-overlay"]'),C=n.querySelector('[data-js="cart-drawer-panel"]'),v=n.querySelector('[data-js="cart-close"]'),q=n.querySelector('[data-js="cart-items"]'),j=n.querySelector('[data-js="cart-empty"]'),x=n.querySelector('[data-js="cart-subtotal"]'),L=n.querySelector('[data-js="cart-drawer-status"]'),A=n.querySelector('[data-js="cart-drawer-error"]'),g=document.querySelectorAll('[data-js="cart-count"]');if(!b||!C||!v||!q||!j||!x||!L||!A)return;const o=n,O=b,D=C,U=v,P=L,H=A,k=Z(window.location.pathname),f=document.querySelectorAll('[data-js="cart-open"]');let s=q,R=j,N=x,c=!1,m=!1,y=0,i=null,p=null;const w=t=>{P.textContent=t},d=t=>{H.textContent=t},T=t=>{D.setAttribute("aria-busy",String(t)),o.querySelectorAll("button").forEach(e=>{e.dataset.js==="cart-close"||(e.disabled=t)})},l=t=>{g.forEach(e=>{e.textContent=String(t),e.hidden=t<1})},K=()=>{const t=g[0];return t&&Number(t.textContent??"0")||0},Q=()=>{const t=s.querySelectorAll('[data-js="cart-qty-value"]');return Array.from(t).reduce((e,r)=>{const a=Number(r.textContent??"0");return e+(Number.isFinite(a)?a:0)},0)},B=(t,e)=>{const r=s.querySelector(`[data-line="${t}"]`);if(!r)return;r.classList.toggle("animate-pulse",e);const a=r.querySelector('[data-js="cart-drawer-line-overlay"]');a&&(e?(a.classList.remove("hidden"),a.classList.add("flex")):(a.classList.add("hidden"),a.classList.remove("flex")))},I=t=>{const e=et(t,'[data-js="cart-drawer"]',[{key:"items",current:s,selector:'[data-js="cart-items"]'},{key:"empty",current:R,selector:'[data-js="cart-empty"]'},{key:"subtotal",current:N,selector:'[data-js="cart-subtotal"]'}]);if(!e.ok)return!1;const r=e.nodes.items,a=e.nodes.empty,u=e.nodes.subtotal;return!r||!a||!u?!1:(s=r,R=a,N=u,l(Q()),!0)},V=t=>I(t),$=async()=>{d("");try{const t=await tt("cart-drawer",k);if(!I(t))throw new Error("Drawer section rendering failed")}catch{d("Could not load your cart right now. Please try again."),w("Cart load failed.")}},z=async(t,e)=>{const r=++y;m=!0,d(""),T(!0),B(t,!0);try{const a=await W(t,e,{sections:["cart-drawer"],sectionsUrl:k});if(!V(a.sections?.["cart-drawer"])||r!==y)throw new Error("Drawer section rendering failed");l(a.item_count),w(e===0?"Item removed.":"Cart updated.")}catch{d("Could not refresh cart UI. Please try again."),w("Cart update failed.")}finally{r===y&&(m=!1,T(!1),B(t,!1),i&&M())}},M=async()=>{if(!m)for(;i;){const t=i;i=null,await z(t.line,t.quantity)}},h=(t,e)=>{i={line:t,quantity:e},M()},G=()=>Array.from(D.querySelectorAll(rt)).filter(t=>!t.hasAttribute("disabled")&&!t.getAttribute("aria-hidden")),_=t=>{if(!c)return;if(t.key==="Escape"){t.preventDefault(),S();return}if(t.key!=="Tab")return;const e=G();if(e.length===0)return;const r=e[0],a=e[e.length-1];if(t.shiftKey&&document.activeElement===r){t.preventDefault(),a.focus();return}!t.shiftKey&&document.activeElement===a&&(t.preventDefault(),r.focus())};function E(t){c||(c=!0,p=t??(document.activeElement instanceof HTMLElement?document.activeElement:null),o.hidden=!1,o.setAttribute("aria-hidden","false"),f.forEach(e=>e.setAttribute("aria-expanded","true")),document.body.style.overflow="hidden",document.addEventListener("keydown",_),U.focus(),$())}function S(){c&&(c=!1,o.hidden=!0,o.setAttribute("aria-hidden","true"),f.forEach(t=>t.setAttribute("aria-expanded","false")),document.body.style.overflow="",document.removeEventListener("keydown",_),p&&p.focus())}f.forEach(t=>{t.addEventListener("click",e=>{e.preventDefault(),E(t)}),t.setAttribute("aria-expanded","false")}),U.addEventListener("click",S),O.addEventListener("click",S),window.addEventListener(X,()=>{E()}),window.addEventListener(Y,t=>{const r=t.detail;typeof r?.itemCount=="number"?l(r.itemCount):typeof r?.itemCountDelta=="number"&&l(Math.max(0,K()+r.itemCountDelta)),r?.openDrawer&&E()}),o.addEventListener("click",t=>{const r=t.target.closest('[data-js="cart-qty-dec"], [data-js="cart-qty-inc"], [data-js="cart-remove"]');if(!r)return;const a=Number(r.dataset.line);if(!a)return;const J=s.querySelector(`[data-line="${a}"]`)?.querySelector('[data-js="cart-qty-value"]'),F=Number(J?.textContent??"1");if(r.dataset.js==="cart-remove"){h(a,0);return}if(r.dataset.js==="cart-qty-inc"){h(a,F+1);return}r.dataset.js==="cart-qty-dec"&&h(a,Math.max(0,F-1))})}export{st as initCartDrawer}; diff --git a/assets/drawer-DzmJHKNX.js b/assets/drawer-DzmJHKNX.js deleted file mode 100644 index d8d4f8768..000000000 --- a/assets/drawer-DzmJHKNX.js +++ /dev/null @@ -1 +0,0 @@ -import{c as V}from"./cart-Kw4e-iq4.js";import{n as W,f as X,a as Y}from"./section-rendering-vVqNxfcB.js";const Z='a[href], button:not([disabled]), textarea, input, select, [tabindex]:not([tabindex="-1"])';function rt(){const n=document.querySelector('[data-js="cart-drawer"]');if(!n)return;const S=n.querySelector('[data-js="cart-drawer-overlay"]'),q=n.querySelector('[data-js="cart-drawer-panel"]'),b=n.querySelector('[data-js="cart-close"]'),v=n.querySelector('[data-js="cart-items"]'),j=n.querySelector('[data-js="cart-empty"]'),E=n.querySelector('[data-js="cart-subtotal"]'),x=n.querySelector('[data-js="cart-drawer-status"]'),C=n.querySelector('[data-js="cart-drawer-error"]'),O=document.querySelectorAll('[data-js="cart-count"]');if(!S||!q||!b||!v||!j||!E||!x||!C)return;const o=n,H=S,L=q,g=b,K=x,T=C,A=W(window.location.pathname),u=document.querySelectorAll('[data-js="cart-open"]');let s=v,k=j,U=E,c=!1,f=!1,y=0,i=null,m=null;const p=t=>{K.textContent=t},l=t=>{T.textContent=t},D=t=>{L.setAttribute("aria-busy",String(t)),o.querySelectorAll("button").forEach(e=>{e.dataset.js==="cart-close"||(e.disabled=t)})},R=t=>{O.forEach(e=>{e.textContent=String(t),e.hidden=t<1})},P=()=>{const t=s.querySelectorAll('[data-js="cart-qty-value"]');return Array.from(t).reduce((e,a)=>{const r=Number(a.textContent??"0");return e+(Number.isFinite(r)?r:0)},0)},B=(t,e)=>{const a=s.querySelector(`[data-line="${t}"]`);if(!a)return;a.classList.toggle("animate-pulse",e);const r=a.querySelector('[data-js="cart-drawer-line-overlay"]');r&&(e?(r.classList.remove("hidden"),r.classList.add("flex")):(r.classList.add("hidden"),r.classList.remove("flex")))},N=t=>{const e=Y(t,'[data-js="cart-drawer"]',[{key:"items",current:s,selector:'[data-js="cart-items"]'},{key:"empty",current:k,selector:'[data-js="cart-empty"]'},{key:"subtotal",current:U,selector:'[data-js="cart-subtotal"]'}]);if(!e.ok)return!1;const a=e.nodes.items,r=e.nodes.empty,d=e.nodes.subtotal;return!a||!r||!d?!1:(s=a,k=r,U=d,R(P()),!0)},Q=t=>N(t),_=async()=>{l("");try{const t=await X("cart-drawer",A);if(!N(t))throw new Error("Drawer section rendering failed")}catch{l("Could not load your cart right now. Please try again."),p("Cart load failed.")}},$=async(t,e)=>{const a=++y;f=!0,l(""),D(!0),B(t,!0);try{const r=await V(t,e,{sections:["cart-drawer"],sectionsUrl:A});if(!Q(r.sections?.["cart-drawer"])||a!==y)throw new Error("Drawer section rendering failed");R(r.item_count),p(e===0?"Item removed.":"Cart updated.")}catch{l("Could not refresh cart UI. Please try again."),p("Cart update failed.")}finally{a===y&&(f=!1,D(!1),B(t,!1),i&&I())}},I=async()=>{if(!f)for(;i;){const t=i;i=null,await $(t.line,t.quantity)}},w=(t,e)=>{i={line:t,quantity:e},I()},z=()=>Array.from(L.querySelectorAll(Z)).filter(t=>!t.hasAttribute("disabled")&&!t.getAttribute("aria-hidden")),F=t=>{if(!c)return;if(t.key==="Escape"){t.preventDefault(),h();return}if(t.key!=="Tab")return;const e=z();if(e.length===0)return;const a=e[0],r=e[e.length-1];if(t.shiftKey&&document.activeElement===a){t.preventDefault(),r.focus();return}!t.shiftKey&&document.activeElement===r&&(t.preventDefault(),a.focus())};function G(t){c||(c=!0,m=t??(document.activeElement instanceof HTMLElement?document.activeElement:null),o.hidden=!1,o.setAttribute("aria-hidden","false"),u.forEach(e=>e.setAttribute("aria-expanded","true")),document.body.style.overflow="hidden",document.addEventListener("keydown",F),g.focus(),_())}function h(){c&&(c=!1,o.hidden=!0,o.setAttribute("aria-hidden","true"),u.forEach(t=>t.setAttribute("aria-expanded","false")),document.body.style.overflow="",document.removeEventListener("keydown",F),m&&m.focus())}u.forEach(t=>{t.addEventListener("click",e=>{e.preventDefault(),G(t)}),t.setAttribute("aria-expanded","false")}),g.addEventListener("click",h),H.addEventListener("click",h),o.addEventListener("click",t=>{const a=t.target.closest('[data-js="cart-qty-dec"], [data-js="cart-qty-inc"], [data-js="cart-remove"]');if(!a)return;const r=Number(a.dataset.line);if(!r)return;const J=s.querySelector(`[data-line="${r}"]`)?.querySelector('[data-js="cart-qty-value"]'),M=Number(J?.textContent??"1");if(a.dataset.js==="cart-remove"){w(r,0);return}if(a.dataset.js==="cart-qty-inc"){w(r,M+1);return}a.dataset.js==="cart-qty-dec"&&w(r,Math.max(0,M-1))})}export{rt as initCartDrawer}; diff --git a/assets/handlers-B949bjPl.js b/assets/handlers-B949bjPl.js new file mode 100644 index 000000000..719716893 --- /dev/null +++ b/assets/handlers-B949bjPl.js @@ -0,0 +1 @@ +import{f as u}from"./variant-picker-DRrQ71gT.js";import{a as m}from"./cart-Kw4e-iq4.js";import{e as l,a as p}from"./cart-events-C4m0PwR1.js";import{s as t}from"./state-Bwp_aMHz.js";import{s as i}from"./sync-BRYiu7LQ.js";function V(n){const e=n.target.closest('[data-js="option-value"]');if(!e||e.disabled||e.getAttribute("aria-disabled")==="true")return;const a=Number(e.dataset.optionPosition)-1,r=e.dataset.optionValue??"";if(a<0||!r)return;t.selectedOptions[a]=r;const o=u(t.productData.variants,t.selectedOptions);if(!o){t.selectedOptions[a]=t.currentVariant.options[a]??t.selectedOptions[a];return}const d=t.currentMediaId,c=t.mediaContextVariantId;t.currentVariant=o,o.featured_media?(t.currentMediaId=o.featured_media.id,t.mediaContextVariantId=o.id):(t.currentMediaId=d,t.mediaContextVariantId=c);const s=new URL(window.location.href);s.searchParams.set("variant",String(o.id)),history.replaceState({},"",`${s.pathname}${s.search}${s.hash}`),i()}function v(n){const e=n.target.closest('[data-js="thumbnail"]');if(!e)return;const a=Number(e.dataset.thumbnail);if(!a)return;t.currentMediaId=a;const r=Number((e.dataset.variantMedia??"").split(",")[0]);r&&(t.mediaContextVariantId=r),i()}async function y(n){if(n.preventDefault(),t.cartState==="loading")return;const a=n.target.querySelector('input[name="quantity"]'),r=a?Math.max(1,Number(a.value)||1):1;t.cartState="loading",i();try{await m(t.currentVariant.id,r),l({itemCountDelta:r}),p(),t.cartState="success",i(),setTimeout(()=>{t.cartState="idle",i()},2e3)}catch{t.cartState="error",i(),setTimeout(()=>{t.cartState="idle",i()},3e3)}}export{y as a,v as b,V as o}; diff --git a/assets/handlers-BekC_4hw.js b/assets/handlers-BekC_4hw.js deleted file mode 100644 index 01dd7e840..000000000 --- a/assets/handlers-BekC_4hw.js +++ /dev/null @@ -1 +0,0 @@ -import{f as u}from"./variant-picker-DRrQ71gT.js";import{a as m}from"./cart-Kw4e-iq4.js";import{s as t}from"./state-Bwp_aMHz.js";import{s as i}from"./sync-BRYiu7LQ.js";function I(n){const e=n.target.closest('[data-js="option-value"]');if(!e||e.disabled||e.getAttribute("aria-disabled")==="true")return;const a=Number(e.dataset.optionPosition)-1,r=e.dataset.optionValue??"";if(a<0||!r)return;t.selectedOptions[a]=r;const o=u(t.productData.variants,t.selectedOptions);if(!o){t.selectedOptions[a]=t.currentVariant.options[a]??t.selectedOptions[a];return}const d=t.currentMediaId,c=t.mediaContextVariantId;t.currentVariant=o,o.featured_media?(t.currentMediaId=o.featured_media.id,t.mediaContextVariantId=o.id):(t.currentMediaId=d,t.mediaContextVariantId=c);const s=new URL(window.location.href);s.searchParams.set("variant",String(o.id)),history.replaceState({},"",`${s.pathname}${s.search}${s.hash}`),i()}function h(n){const e=n.target.closest('[data-js="thumbnail"]');if(!e)return;const a=Number(e.dataset.thumbnail);if(!a)return;t.currentMediaId=a;const r=Number((e.dataset.variantMedia??"").split(",")[0]);r&&(t.mediaContextVariantId=r),i()}async function V(n){if(n.preventDefault(),t.cartState==="loading")return;const a=n.target.querySelector('input[name="quantity"]'),r=a?Math.max(1,Number(a.value)||1):1;t.cartState="loading",i();try{await m(t.currentVariant.id,r),t.cartState="success",i(),setTimeout(()=>{t.cartState="idle",i()},2e3)}catch{t.cartState="error",i(),setTimeout(()=>{t.cartState="idle",i()},3e3)}}export{V as a,h as b,I as o}; diff --git a/assets/main-BedzLXBi.css b/assets/main-BedzLXBi.css deleted file mode 100644 index cc57aa3f6..000000000 --- a/assets/main-BedzLXBi.css +++ /dev/null @@ -1 +0,0 @@ -@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-border-style:solid;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-duration:initial;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-500:oklch(63.7% .237 25.331);--color-red-700:oklch(50.5% .213 27.518);--color-yellow-500:oklch(79.5% .184 86.047);--color-green-500:oklch(72.3% .219 149.579);--color-blue-50:oklch(97% .014 254.604);--color-blue-400:oklch(70.7% .165 254.624);--color-blue-500:oklch(62.3% .214 259.815);--color-blue-600:oklch(54.6% .245 262.881);--color-blue-700:oklch(48.8% .243 264.376);--color-gray-50:oklch(98.5% .002 247.839);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-300:oklch(87.2% .01 258.338);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-600:oklch(44.6% .03 256.802);--color-gray-700:oklch(37.3% .034 259.733);--color-gray-800:oklch(27.8% .033 256.848);--color-gray-900:oklch(21% .034 264.665);--color-neutral-100:oklch(97% 0 0);--color-neutral-900:oklch(20.5% 0 0);--color-black:#000;--color-white:#fff;--spacing:.25rem;--breakpoint-sm:40rem;--breakpoint-md:48rem;--breakpoint-lg:64rem;--breakpoint-xl:80rem;--container-md:28rem;--container-lg:32rem;--container-xl:36rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height: 1.5 ;--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-3xl:1.875rem;--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5 / 2.25);--font-weight-thin:100;--font-weight-extralight:200;--font-weight-light:300;--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--font-weight-extrabold:800;--font-weight-black:900;--tracking-wide:.025em;--radius-sm:.25rem;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--shadow-sm:0 1px 3px 0 #0000001a, 0 1px 2px -1px #0000001a;--shadow-md:0 4px 6px -1px #0000001a, 0 2px 4px -2px #0000001a;--animate-spin:spin 1s linear infinite;--animate-pulse:pulse 2s cubic-bezier(.4, 0, .6, 1) infinite;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.pointer-events-none{pointer-events:none}.invisible{visibility:hidden}.visible{visibility:visible}.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.inset-0{inset:calc(var(--spacing) * 0)}.end{inset-inline-end:var(--spacing)}.top-0{top:calc(var(--spacing) * 0)}.top-\[117px\]{top:117px}.right-0{right:calc(var(--spacing) * 0)}.bottom-0{bottom:calc(var(--spacing) * 0)}.left-0{left:calc(var(--spacing) * 0)}.z-10{z-index:10}.z-20{z-index:20}.z-50{z-index:50}.container{width:100%}@media(min-width:40rem){.container{max-width:40rem}}@media(min-width:48rem){.container{max-width:48rem}}@media(min-width:64rem){.container{max-width:64rem}}@media(min-width:80rem){.container{max-width:80rem}}@media(min-width:96rem){.container{max-width:96rem}}.mx-2{margin-inline:calc(var(--spacing) * 2)}.mx-5{margin-inline:calc(var(--spacing) * 5)}.mt-1{margin-top:calc(var(--spacing) * 1)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-8{margin-bottom:calc(var(--spacing) * 8)}.ml-auto{margin-left:auto}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.scrollbar-hidden::-webkit-scrollbar{display:none}.aspect-square{aspect-ratio:1}.h-4{height:calc(var(--spacing) * 4)}.h-5{height:calc(var(--spacing) * 5)}.h-8{height:calc(var(--spacing) * 8)}.h-9{height:calc(var(--spacing) * 9)}.h-10{height:calc(var(--spacing) * 10)}.h-16{height:calc(var(--spacing) * 16)}.h-20{height:calc(var(--spacing) * 20)}.h-full{height:100%}.max-h-\[calc\(100vh-240px\)\]{max-height:calc(100vh - 240px)}.min-h-5{min-height:calc(var(--spacing) * 5)}.min-h-\[240px\]{min-height:240px}.min-h-svh{min-height:100svh}.w-4{width:calc(var(--spacing) * 4)}.w-5{width:calc(var(--spacing) * 5)}.w-8{width:calc(var(--spacing) * 8)}.w-9{width:calc(var(--spacing) * 9)}.w-10{width:calc(var(--spacing) * 10)}.w-16{width:calc(var(--spacing) * 16)}.w-20{width:calc(var(--spacing) * 20)}.w-\[calc\(100\%-2rem\)\]{width:calc(100% - 2rem)}.w-full{width:100%}.max-w-lg{max-width:var(--container-lg)}.max-w-md{max-width:var(--container-md)}.max-w-xl{max-width:var(--container-xl)}.min-w-6{min-width:calc(var(--spacing) * 6)}.flex-1{flex:1}.shrink-0{flex-shrink:0}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.animate-pulse{animation:var(--animate-pulse)}.animate-spin{animation:var(--animate-spin)}.cursor-pointer{cursor:pointer}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-\[64px_1fr\]{grid-template-columns:64px 1fr}.grid-cols-\[80px_1fr\]{grid-template-columns:80px 1fr}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-end{align-items:flex-end}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-1{gap:calc(var(--spacing) * 1)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 5) * calc(1 - var(--tw-space-y-reverse)))}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-xl{border-radius:var(--radius-xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-black\/20{border-color:#0003}@supports (color:color-mix(in lab,red,red)){.border-black\/20{border-color:color-mix(in oklab,var(--color-black) 20%,transparent)}}.border-gray-300{border-color:var(--color-gray-300)}.border-t-black{border-top-color:var(--color-black)}.bg-\[\#FF5A00\]{background-color:#ff5a00}.bg-black\/40{background-color:#0006}@supports (color:color-mix(in lab,red,red)){.bg-black\/40{background-color:color-mix(in oklab,var(--color-black) 40%,transparent)}}.bg-blue-400{background-color:var(--color-blue-400)}.bg-blue-500{background-color:var(--color-blue-500)}.bg-blue-600{background-color:var(--color-blue-600)}.bg-gray-800{background-color:var(--color-gray-800)}.bg-white{background-color:var(--color-white)}.bg-white\/60{background-color:#fff9}@supports (color:color-mix(in lab,red,red)){.bg-white\/60{background-color:color-mix(in oklab,var(--color-white) 60%,transparent)}}.\[mask-type\:luminance\]{mask-type:luminance}.fill-\(--brand-color\){fill:var(--brand-color)}.object-cover{object-fit:cover}.p-2{padding:calc(var(--spacing) * 2)}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.p-6{padding:calc(var(--spacing) * 6)}.p-8{padding:calc(var(--spacing) * 8)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.py-1{padding-block:calc(var(--spacing) * 1)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-8{padding-block:calc(var(--spacing) * 8)}.pb-3{padding-bottom:calc(var(--spacing) * 3)}.pb-4{padding-bottom:calc(var(--spacing) * 4)}.text-center{text-align:center}.text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.text-\(color\:--brand-text\){color:var(--brand-text)}.text-blue-500{color:var(--color-blue-500)}.text-gray-300{color:var(--color-gray-300)}.text-gray-500{color:var(--color-gray-500)}.text-gray-600{color:var(--color-gray-600)}.text-gray-700{color:var(--color-gray-700)}.text-gray-800{color:var(--color-gray-800)}.text-gray-900{color:var(--color-gray-900)}.text-green-500{color:var(--color-green-500)}.text-red-500{color:var(--color-red-500)}.text-red-700{color:var(--color-red-700)}.text-white{color:var(--color-white)}.text-yellow-500{color:var(--color-yellow-500)}.capitalize{text-transform:capitalize}.uppercase{text-transform:uppercase}.underline{text-decoration-line:underline}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.opacity-100{opacity:1}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-2xl{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a), 0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a), 0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-300{--tw-duration:.3s;transition-duration:.3s}.forced-color-adjust-auto{forced-color-adjust:auto}.forced-color-adjust-none{forced-color-adjust:none}@media(hover:hover){.group-hover\:text-blue-600:is(:where(.group):hover *){color:var(--color-blue-600)}.group-hover\:text-gray-700:is(:where(.group):hover *){color:var(--color-gray-700)}.group-hover\:underline:is(:where(.group):hover *){text-decoration-line:underline}}.peer-checked\:block:is(:where(.peer):checked~*){display:block}.first\:pt-0:first-child{padding-top:calc(var(--spacing) * 0)}.last\:pb-0:last-child{padding-bottom:calc(var(--spacing) * 0)}.odd\:bg-gray-50:nth-child(odd){background-color:var(--color-gray-50)}.even\:bg-white:nth-child(2n){background-color:var(--color-white)}.focus-within\:ring-2:focus-within{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}@media(hover:hover){.hover\:-translate-y-1:hover{--tw-translate-y:calc(var(--spacing) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.hover\:bg-blue-50:hover{background-color:var(--color-blue-50)}.hover\:bg-blue-600:hover{background-color:var(--color-blue-600)}.hover\:bg-blue-700:hover{background-color:var(--color-blue-700)}.hover\:bg-gray-50:hover{background-color:var(--color-gray-50)}.hover\:shadow-lg:hover{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}}.focus\:not-sr-only:focus{clip-path:none;white-space:normal;width:auto;height:auto;margin:0;padding:0;position:static;overflow:visible}.focus\:absolute:focus{position:absolute}.focus\:top-4:focus{top:calc(var(--spacing) * 4)}.focus\:left-4:focus{left:calc(var(--spacing) * 4)}.focus\:z-50:focus{z-index:50}.focus\:rounded:focus{border-radius:.25rem}.focus\:border-blue-500:focus{border-color:var(--color-blue-500)}.focus\:bg-white:focus{background-color:var(--color-white)}.focus\:px-4:focus{padding-inline:calc(var(--spacing) * 4)}.focus\:py-2:focus{padding-block:calc(var(--spacing) * 2)}.focus\:ring-1:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-blue-500:focus{--tw-ring-color:var(--color-blue-500)}.focus\:ring-offset-2:focus{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.focus-visible\:ring-2:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus-visible\:ring-blue-500:focus-visible{--tw-ring-color:var(--color-blue-500)}.focus-visible\:ring-offset-2:focus-visible{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.active\:bg-blue-700:active{background-color:var(--color-blue-700)}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}@media(prefers-reduced-motion:reduce){.motion-reduce\:transform-none{transform:none}.motion-reduce\:transition-none{transition-property:none}}@media(min-width:40rem){.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(min-width:48rem){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:p-8{padding:calc(var(--spacing) * 8)}.md\:px-8{padding-inline:calc(var(--spacing) * 8)}@media(hover:hover){.md\:hover\:px-10:hover{padding-inline:calc(var(--spacing) * 10)}}}@media(min-width:64rem){.lg\:w-16{width:calc(var(--spacing) * 16)}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:flex-col{flex-direction:column}.lg\:flex-row-reverse{flex-direction:row-reverse}.lg\:p-12{padding:calc(var(--spacing) * 12)}}@media(min-width:80rem){.xl\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}}@media(prefers-color-scheme:dark){.dark\:bg-blue-400{background-color:var(--color-blue-400)}.dark\:bg-blue-500{background-color:var(--color-blue-500)}.dark\:bg-gray-900{background-color:var(--color-gray-900)}.dark\:text-gray-100{color:var(--color-gray-100)}.dark\:opacity-90{opacity:.9}.dark\:shadow-none{--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark\:ring-1{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark\:ring-white\/10{--tw-ring-color:#ffffff1a}@supports (color:color-mix(in lab,red,red)){.dark\:ring-white\/10{--tw-ring-color:color-mix(in oklab, var(--color-white) 10%, transparent)}}@media(hover:hover){.dark\:hover\:bg-blue-400:hover{background-color:var(--color-blue-400)}.dark\:hover\:bg-blue-500:hover{background-color:var(--color-blue-500)}}}}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@keyframes spin{to{transform:rotate(360deg)}}@keyframes pulse{50%{opacity:.5}} diff --git a/assets/main-DNSsntQO.css b/assets/main-DNSsntQO.css new file mode 100644 index 000000000..fb3883c25 --- /dev/null +++ b/assets/main-DNSsntQO.css @@ -0,0 +1 @@ +@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-border-style:solid;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-duration:initial;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-500:oklch(63.7% .237 25.331);--color-red-700:oklch(50.5% .213 27.518);--color-yellow-500:oklch(79.5% .184 86.047);--color-green-500:oklch(72.3% .219 149.579);--color-blue-50:oklch(97% .014 254.604);--color-blue-400:oklch(70.7% .165 254.624);--color-blue-500:oklch(62.3% .214 259.815);--color-blue-600:oklch(54.6% .245 262.881);--color-blue-700:oklch(48.8% .243 264.376);--color-gray-50:oklch(98.5% .002 247.839);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-300:oklch(87.2% .01 258.338);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-700:oklch(37.3% .034 259.733);--color-gray-800:oklch(27.8% .033 256.848);--color-gray-900:oklch(21% .034 264.665);--color-neutral-100:oklch(97% 0 0);--color-neutral-900:oklch(20.5% 0 0);--color-black:#000;--color-white:#fff;--spacing:.25rem;--breakpoint-sm:40rem;--breakpoint-md:48rem;--breakpoint-lg:64rem;--breakpoint-xl:80rem;--container-md:28rem;--container-lg:32rem;--container-xl:36rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height: 1.5 ;--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-3xl:1.875rem;--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5 / 2.25);--font-weight-thin:100;--font-weight-extralight:200;--font-weight-light:300;--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--font-weight-extrabold:800;--font-weight-black:900;--tracking-wide:.025em;--radius-sm:.25rem;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--shadow-sm:0 1px 3px 0 #0000001a, 0 1px 2px -1px #0000001a;--shadow-md:0 4px 6px -1px #0000001a, 0 2px 4px -2px #0000001a;--animate-spin:spin 1s linear infinite;--animate-pulse:pulse 2s cubic-bezier(.4, 0, .6, 1) infinite;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.pointer-events-none{pointer-events:none}.invisible{visibility:hidden}.visible{visibility:visible}.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.inset-0{inset:calc(var(--spacing) * 0)}.end{inset-inline-end:var(--spacing)}.top-0{top:calc(var(--spacing) * 0)}.top-\[117px\]{top:117px}.right-0{right:calc(var(--spacing) * 0)}.bottom-0{bottom:calc(var(--spacing) * 0)}.left-0{left:calc(var(--spacing) * 0)}.z-10{z-index:10}.z-20{z-index:20}.z-50{z-index:50}.container{width:100%}@media(min-width:40rem){.container{max-width:40rem}}@media(min-width:48rem){.container{max-width:48rem}}@media(min-width:64rem){.container{max-width:64rem}}@media(min-width:80rem){.container{max-width:80rem}}@media(min-width:96rem){.container{max-width:96rem}}.mx-2{margin-inline:calc(var(--spacing) * 2)}.mx-5{margin-inline:calc(var(--spacing) * 5)}.mt-1{margin-top:calc(var(--spacing) * 1)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-8{margin-bottom:calc(var(--spacing) * 8)}.ml-auto{margin-left:auto}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.scrollbar-hidden::-webkit-scrollbar{display:none}.aspect-square{aspect-ratio:1}.h-4{height:calc(var(--spacing) * 4)}.h-5{height:calc(var(--spacing) * 5)}.h-8{height:calc(var(--spacing) * 8)}.h-9{height:calc(var(--spacing) * 9)}.h-10{height:calc(var(--spacing) * 10)}.h-16{height:calc(var(--spacing) * 16)}.h-20{height:calc(var(--spacing) * 20)}.h-full{height:100%}.max-h-\[calc\(100vh-240px\)\]{max-height:calc(100vh - 240px)}.min-h-5{min-height:calc(var(--spacing) * 5)}.min-h-\[240px\]{min-height:240px}.min-h-svh{min-height:100svh}.w-4{width:calc(var(--spacing) * 4)}.w-5{width:calc(var(--spacing) * 5)}.w-8{width:calc(var(--spacing) * 8)}.w-9{width:calc(var(--spacing) * 9)}.w-10{width:calc(var(--spacing) * 10)}.w-16{width:calc(var(--spacing) * 16)}.w-20{width:calc(var(--spacing) * 20)}.w-\[calc\(100\%-2rem\)\]{width:calc(100% - 2rem)}.w-full{width:100%}.max-w-lg{max-width:var(--container-lg)}.max-w-md{max-width:var(--container-md)}.max-w-xl{max-width:var(--container-xl)}.min-w-6{min-width:calc(var(--spacing) * 6)}.flex-1{flex:1}.shrink-0{flex-shrink:0}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.animate-pulse{animation:var(--animate-pulse)}.animate-spin{animation:var(--animate-spin)}.cursor-pointer{cursor:pointer}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-\[64px_1fr\]{grid-template-columns:64px 1fr}.grid-cols-\[80px_1fr\]{grid-template-columns:80px 1fr}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-end{align-items:flex-end}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-1{gap:calc(var(--spacing) * 1)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 5) * calc(1 - var(--tw-space-y-reverse)))}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-xl{border-radius:var(--radius-xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-black\/20{border-color:#0003}@supports (color:color-mix(in lab,red,red)){.border-black\/20{border-color:color-mix(in oklab,var(--color-black) 20%,transparent)}}.border-gray-300{border-color:var(--color-gray-300)}.border-t-black{border-top-color:var(--color-black)}.bg-\[\#FF5A00\]{background-color:#ff5a00}.bg-black\/40{background-color:#0006}@supports (color:color-mix(in lab,red,red)){.bg-black\/40{background-color:color-mix(in oklab,var(--color-black) 40%,transparent)}}.bg-blue-400{background-color:var(--color-blue-400)}.bg-blue-500{background-color:var(--color-blue-500)}.bg-blue-600{background-color:var(--color-blue-600)}.bg-gray-800{background-color:var(--color-gray-800)}.bg-white{background-color:var(--color-white)}.bg-white\/60{background-color:#fff9}@supports (color:color-mix(in lab,red,red)){.bg-white\/60{background-color:color-mix(in oklab,var(--color-white) 60%,transparent)}}.\[mask-type\:luminance\]{mask-type:luminance}.fill-\(--brand-color\){fill:var(--brand-color)}.object-cover{object-fit:cover}.p-2{padding:calc(var(--spacing) * 2)}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.p-6{padding:calc(var(--spacing) * 6)}.p-8{padding:calc(var(--spacing) * 8)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.py-1{padding-block:calc(var(--spacing) * 1)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-8{padding-block:calc(var(--spacing) * 8)}.pb-3{padding-bottom:calc(var(--spacing) * 3)}.pb-4{padding-bottom:calc(var(--spacing) * 4)}.text-center{text-align:center}.text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.text-\(color\:--brand-text\){color:var(--brand-text)}.text-blue-500{color:var(--color-blue-500)}.text-gray-300{color:var(--color-gray-300)}.text-gray-500{color:var(--color-gray-500)}.text-gray-700{color:var(--color-gray-700)}.text-gray-800{color:var(--color-gray-800)}.text-gray-900{color:var(--color-gray-900)}.text-green-500{color:var(--color-green-500)}.text-red-500{color:var(--color-red-500)}.text-red-700{color:var(--color-red-700)}.text-white{color:var(--color-white)}.text-yellow-500{color:var(--color-yellow-500)}.capitalize{text-transform:capitalize}.uppercase{text-transform:uppercase}.underline{text-decoration-line:underline}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.opacity-100{opacity:1}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-2xl{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a), 0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a), 0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-300{--tw-duration:.3s;transition-duration:.3s}.forced-color-adjust-auto{forced-color-adjust:auto}.forced-color-adjust-none{forced-color-adjust:none}@media(hover:hover){.group-hover\:text-blue-600:is(:where(.group):hover *){color:var(--color-blue-600)}.group-hover\:text-gray-700:is(:where(.group):hover *){color:var(--color-gray-700)}}.peer-checked\:block:is(:where(.peer):checked~*){display:block}.first\:pt-0:first-child{padding-top:calc(var(--spacing) * 0)}.last\:pb-0:last-child{padding-bottom:calc(var(--spacing) * 0)}.odd\:bg-gray-50:nth-child(odd){background-color:var(--color-gray-50)}.even\:bg-white:nth-child(2n){background-color:var(--color-white)}.focus-within\:ring-2:focus-within{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}@media(hover:hover){.hover\:-translate-y-1:hover{--tw-translate-y:calc(var(--spacing) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.hover\:bg-blue-50:hover{background-color:var(--color-blue-50)}.hover\:bg-blue-600:hover{background-color:var(--color-blue-600)}.hover\:bg-blue-700:hover{background-color:var(--color-blue-700)}.hover\:bg-gray-50:hover{background-color:var(--color-gray-50)}.hover\:shadow-lg:hover{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}}.focus\:not-sr-only:focus{clip-path:none;white-space:normal;width:auto;height:auto;margin:0;padding:0;position:static;overflow:visible}.focus\:absolute:focus{position:absolute}.focus\:top-4:focus{top:calc(var(--spacing) * 4)}.focus\:left-4:focus{left:calc(var(--spacing) * 4)}.focus\:z-50:focus{z-index:50}.focus\:rounded:focus{border-radius:.25rem}.focus\:border-blue-500:focus{border-color:var(--color-blue-500)}.focus\:bg-white:focus{background-color:var(--color-white)}.focus\:px-4:focus{padding-inline:calc(var(--spacing) * 4)}.focus\:py-2:focus{padding-block:calc(var(--spacing) * 2)}.focus\:ring-1:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-blue-500:focus{--tw-ring-color:var(--color-blue-500)}.focus\:ring-offset-2:focus{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.focus-visible\:ring-2:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus-visible\:ring-blue-500:focus-visible{--tw-ring-color:var(--color-blue-500)}.focus-visible\:ring-offset-2:focus-visible{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.active\:bg-blue-700:active{background-color:var(--color-blue-700)}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}@media(prefers-reduced-motion:reduce){.motion-reduce\:transform-none{transform:none}.motion-reduce\:transition-none{transition-property:none}}@media(min-width:40rem){.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(min-width:48rem){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:p-8{padding:calc(var(--spacing) * 8)}.md\:px-8{padding-inline:calc(var(--spacing) * 8)}@media(hover:hover){.md\:hover\:px-10:hover{padding-inline:calc(var(--spacing) * 10)}}}@media(min-width:64rem){.lg\:w-16{width:calc(var(--spacing) * 16)}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:flex-col{flex-direction:column}.lg\:flex-row-reverse{flex-direction:row-reverse}.lg\:p-12{padding:calc(var(--spacing) * 12)}}@media(min-width:80rem){.xl\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}}@media(prefers-color-scheme:dark){.dark\:bg-blue-400{background-color:var(--color-blue-400)}.dark\:bg-blue-500{background-color:var(--color-blue-500)}.dark\:bg-gray-900{background-color:var(--color-gray-900)}.dark\:text-gray-100{color:var(--color-gray-100)}.dark\:opacity-90{opacity:.9}.dark\:shadow-none{--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark\:ring-1{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark\:ring-white\/10{--tw-ring-color:#ffffff1a}@supports (color:color-mix(in lab,red,red)){.dark\:ring-white\/10{--tw-ring-color:color-mix(in oklab, var(--color-white) 10%, transparent)}}@media(hover:hover){.dark\:hover\:bg-blue-400:hover{background-color:var(--color-blue-400)}.dark\:hover\:bg-blue-500:hover{background-color:var(--color-blue-500)}}}}html{overscroll-behavior:none}@media(min-width:768px){html{scrollbar-gutter:stable;-webkit-scroll-gutter:stable}}body{font-smooth:antialiased;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;overscroll-behavior:none}img,svg,video{pointer-events:none;-webkit-user-select:none;user-select:none}dialog{max-width:none;max-height:none}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@keyframes spin{to{transform:rotate(360deg)}}@keyframes pulse{50%{opacity:.5}} diff --git a/assets/product-CbP3NaeG.js b/assets/product-CbP3NaeG.js deleted file mode 100644 index ea4b45d40..000000000 --- a/assets/product-CbP3NaeG.js +++ /dev/null @@ -1 +0,0 @@ -import{s as n}from"./state-Bwp_aMHz.js";import{s as c}from"./sync-BRYiu7LQ.js";import{o as f,a as v,b as L}from"./handlers-BekC_4hw.js";import{l as E}from"./recommendations-H0stnrna.js";import"./variant-picker-DRrQ71gT.js";import"./cart-Kw4e-iq4.js";function u(t){const a=new URLSearchParams(window.location.search),o=Number(a.get("variant"));return t.variants.find(r=>r.id===o)??t.variants.find(r=>r.available)??t.variants[0]}function m(t){if(n.currentVariant=t,n.selectedOptions=[...t.options],t.featured_media){n.currentMediaId=t.featured_media.id,n.mediaContextVariantId=t.id;return}if(n.currentMediaId===null){const a=document.querySelector('[data-js="media-item"]'),o=Number(a?.dataset.mediaId??""),r=Number((a?.dataset.variantMedia??"").split(",")[0]);n.currentMediaId=o||null,n.mediaContextVariantId=r||n.mediaContextVariantId}}function l(t){return t instanceof HTMLElement}function I(){const t=document.querySelector('[data-js="product-data"]');if(!t?.textContent)return;n.productData=JSON.parse(t.textContent);const a=document.querySelector('[data-js="variant-prices"]');n.variantPrices=a?.textContent?JSON.parse(a.textContent):{},m(u(n.productData));let o=!1;const r=()=>{o||(o=!0,E())},i=e=>{!l(e.target)||!e.target.closest('[data-js="product-form"], [data-product-media], [data-product-thumbnails]')||(r(),document.removeEventListener("pointerdown",i,!0),document.removeEventListener("keydown",d,!0))},d=e=>{e.key!=="Enter"&&e.key!==" "||!l(e.target)||!e.target.closest('[data-js="product-form"], [data-product-media], [data-product-thumbnails]')||(r(),document.removeEventListener("pointerdown",i,!0),document.removeEventListener("keydown",d,!0))};document.addEventListener("pointerdown",i,!0),document.addEventListener("keydown",d,!0);const s=document.querySelector('[data-js="product-form"]');s?.addEventListener("click",e=>{f(e)}),s?.addEventListener("submit",e=>{r(),v(e)}),document.addEventListener("click",e=>{L(e)}),window.addEventListener("popstate",()=>{m(u(n.productData)),n.cartState="idle",c()}),c()}document.addEventListener("DOMContentLoaded",I); diff --git a/assets/product-_rxK6zOl.js b/assets/product-_rxK6zOl.js new file mode 100644 index 000000000..ba2f35c17 --- /dev/null +++ b/assets/product-_rxK6zOl.js @@ -0,0 +1 @@ +import{s as n}from"./state-Bwp_aMHz.js";import{s as c}from"./sync-BRYiu7LQ.js";import{o as f,a as v,b as L}from"./handlers-B949bjPl.js";import{l as E}from"./recommendations-H0stnrna.js";import"./variant-picker-DRrQ71gT.js";import"./cart-Kw4e-iq4.js";import"./cart-events-C4m0PwR1.js";function u(t){const a=new URLSearchParams(window.location.search),o=Number(a.get("variant"));return t.variants.find(r=>r.id===o)??t.variants.find(r=>r.available)??t.variants[0]}function m(t){if(n.currentVariant=t,n.selectedOptions=[...t.options],t.featured_media){n.currentMediaId=t.featured_media.id,n.mediaContextVariantId=t.id;return}if(n.currentMediaId===null){const a=document.querySelector('[data-js="media-item"]'),o=Number(a?.dataset.mediaId??""),r=Number((a?.dataset.variantMedia??"").split(",")[0]);n.currentMediaId=o||null,n.mediaContextVariantId=r||n.mediaContextVariantId}}function l(t){return t instanceof HTMLElement}function I(){const t=document.querySelector('[data-js="product-data"]');if(!t?.textContent)return;n.productData=JSON.parse(t.textContent);const a=document.querySelector('[data-js="variant-prices"]');n.variantPrices=a?.textContent?JSON.parse(a.textContent):{},m(u(n.productData));let o=!1;const r=()=>{o||(o=!0,E())},i=e=>{!l(e.target)||!e.target.closest('[data-js="product-form"], [data-product-media], [data-product-thumbnails]')||(r(),document.removeEventListener("pointerdown",i,!0),document.removeEventListener("keydown",d,!0))},d=e=>{e.key!=="Enter"&&e.key!==" "||!l(e.target)||!e.target.closest('[data-js="product-form"], [data-product-media], [data-product-thumbnails]')||(r(),document.removeEventListener("pointerdown",i,!0),document.removeEventListener("keydown",d,!0))};document.addEventListener("pointerdown",i,!0),document.addEventListener("keydown",d,!0);const s=document.querySelector('[data-js="product-form"]');s?.addEventListener("click",e=>{f(e)}),s?.addEventListener("submit",e=>{r(),v(e)}),document.addEventListener("click",e=>{L(e)}),window.addEventListener("popstate",()=>{m(u(n.productData)),n.cartState="idle",c()}),c()}document.addEventListener("DOMContentLoaded",I); diff --git a/assets/theme-C1WNKfRD.js b/assets/theme-skURl5jI.js similarity index 84% rename from assets/theme-C1WNKfRD.js rename to assets/theme-skURl5jI.js index 41d90d021..6b86b1b4c 100644 --- a/assets/theme-C1WNKfRD.js +++ b/assets/theme-skURl5jI.js @@ -1,2 +1,2 @@ -const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["./drawer-DzmJHKNX.js","./cart-Kw4e-iq4.js","./section-rendering-vVqNxfcB.js"])))=>i.map(i=>d[i]); -const v="modulepreload",w=function(u,o){return new URL(u,o).href},p={},y=function(o,i,a){let e=Promise.resolve();if(i&&i.length>0){let g=function(n){return Promise.all(n.map(l=>Promise.resolve(l).then(d=>({status:"fulfilled",value:d}),d=>({status:"rejected",reason:d}))))};const r=document.getElementsByTagName("link"),s=document.querySelector("meta[property=csp-nonce]"),h=s?.nonce||s?.getAttribute("nonce");e=g(i.map(n=>{if(n=w(n,a),n in p)return;p[n]=!0;const l=n.endsWith(".css"),d=l?'[rel="stylesheet"]':"";if(a)for(let f=r.length-1;f>=0;f--){const m=r[f];if(m.href===n&&(!l||m.rel==="stylesheet"))return}else if(document.querySelector(`link[href="${n}"]${d}`))return;const c=document.createElement("link");if(c.rel=l?"stylesheet":v,l||(c.as="script"),c.crossOrigin="",c.href=n,h&&c.setAttribute("nonce",h),document.head.appendChild(c),l)return new Promise((f,m)=>{c.addEventListener("load",f),c.addEventListener("error",()=>m(new Error(`Unable to preload CSS for ${n}`)))})}))}function t(r){const s=new Event("vite:preloadError",{cancelable:!0});if(s.payload=r,window.dispatchEvent(s),!s.defaultPrevented)throw r}return e.then(r=>{for(const s of r||[])s.status==="rejected"&&t(s.reason);return o().catch(t)})};(function(){const o=document.createElement("link").relList;if(o&&o.supports&&o.supports("modulepreload"))return;for(const e of document.querySelectorAll('link[rel="modulepreload"]'))a(e);new MutationObserver(e=>{for(const t of e)if(t.type==="childList")for(const r of t.addedNodes)r.tagName==="LINK"&&r.rel==="modulepreload"&&a(r)}).observe(document,{childList:!0,subtree:!0});function i(e){const t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),e.crossOrigin==="use-credentials"?t.credentials="include":e.crossOrigin==="anonymous"?t.credentials="omit":t.credentials="same-origin",t}function a(e){if(e.ep)return;e.ep=!0;const t=i(e);fetch(e.href,t)}})();document.addEventListener("DOMContentLoaded",()=>{const u=document.querySelector('[data-js="cart-drawer"]'),o=document.querySelector('[data-js="cart-open"]'),i=document.querySelector('[data-js="search-drawer"]'),a=document.querySelector('[data-js="search-open"]');(u||o)&&y(async()=>{const{initCartDrawer:e}=await import("./drawer-DzmJHKNX.js");return{initCartDrawer:e}},__vite__mapDeps([0,1,2]),import.meta.url).then(({initCartDrawer:e})=>{e()}),(i||a)&&y(async()=>{const{initSearchDrawer:e}=await import("./drawer-BOuNB4a_.js");return{initSearchDrawer:e}},[],import.meta.url).then(({initSearchDrawer:e})=>{e()})}); +const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["./drawer-CJwC366z.js","./cart-Kw4e-iq4.js","./cart-events-C4m0PwR1.js","./section-rendering-vVqNxfcB.js"])))=>i.map(i=>d[i]); +const v="modulepreload",w=function(u,o){return new URL(u,o).href},p={},y=function(o,i,a){let e=Promise.resolve();if(i&&i.length>0){let g=function(n){return Promise.all(n.map(l=>Promise.resolve(l).then(d=>({status:"fulfilled",value:d}),d=>({status:"rejected",reason:d}))))};const r=document.getElementsByTagName("link"),s=document.querySelector("meta[property=csp-nonce]"),h=s?.nonce||s?.getAttribute("nonce");e=g(i.map(n=>{if(n=w(n,a),n in p)return;p[n]=!0;const l=n.endsWith(".css"),d=l?'[rel="stylesheet"]':"";if(a)for(let f=r.length-1;f>=0;f--){const m=r[f];if(m.href===n&&(!l||m.rel==="stylesheet"))return}else if(document.querySelector(`link[href="${n}"]${d}`))return;const c=document.createElement("link");if(c.rel=l?"stylesheet":v,l||(c.as="script"),c.crossOrigin="",c.href=n,h&&c.setAttribute("nonce",h),document.head.appendChild(c),l)return new Promise((f,m)=>{c.addEventListener("load",f),c.addEventListener("error",()=>m(new Error(`Unable to preload CSS for ${n}`)))})}))}function t(r){const s=new Event("vite:preloadError",{cancelable:!0});if(s.payload=r,window.dispatchEvent(s),!s.defaultPrevented)throw r}return e.then(r=>{for(const s of r||[])s.status==="rejected"&&t(s.reason);return o().catch(t)})};(function(){const o=document.createElement("link").relList;if(o&&o.supports&&o.supports("modulepreload"))return;for(const e of document.querySelectorAll('link[rel="modulepreload"]'))a(e);new MutationObserver(e=>{for(const t of e)if(t.type==="childList")for(const r of t.addedNodes)r.tagName==="LINK"&&r.rel==="modulepreload"&&a(r)}).observe(document,{childList:!0,subtree:!0});function i(e){const t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),e.crossOrigin==="use-credentials"?t.credentials="include":e.crossOrigin==="anonymous"?t.credentials="omit":t.credentials="same-origin",t}function a(e){if(e.ep)return;e.ep=!0;const t=i(e);fetch(e.href,t)}})();document.addEventListener("DOMContentLoaded",()=>{const u=document.querySelector('[data-js="cart-drawer"]'),o=document.querySelector('[data-js="cart-open"]'),i=document.querySelector('[data-js="search-drawer"]'),a=document.querySelector('[data-js="search-open"]');(u||o)&&y(async()=>{const{initCartDrawer:e}=await import("./drawer-CJwC366z.js");return{initCartDrawer:e}},__vite__mapDeps([0,1,2,3]),import.meta.url).then(({initCartDrawer:e})=>{e()}),(i||a)&&y(async()=>{const{initSearchDrawer:e}=await import("./drawer-BOuNB4a_.js");return{initSearchDrawer:e}},[],import.meta.url).then(({initSearchDrawer:e})=>{e()})}); diff --git a/frontend/entrypoints/css/main.css b/frontend/entrypoints/css/main.css index 9193a932d..37f63911b 100644 --- a/frontend/entrypoints/css/main.css +++ b/frontend/entrypoints/css/main.css @@ -8,16 +8,16 @@ @theme { /* Colors */ - --color-*: initial; + /*--color-*: initial;*/ /* Typo */ - --text-*: initial; - /*--text-h1: var(--h1); + /*--text-*: initial; + --text-h1: var(--h1); --text-h1--line-height: var(--h1--line-height); --text-h1--font-weight: var(--h1--font-weight);*/ /* Spacing */ - --spacing-*: initial; + /*--spacing-*: initial; --spacing-0: 0; --spacing-col1: 8.33333333333%; --spacing-col2: 16.66666666667%; @@ -30,16 +30,16 @@ --spacing-col9: 75%; --spacing-col10: 83.33333333333%; --spacing-col11: 91.66666666667%; - --spacing-col12: 100%; + --spacing-col12: 100%;*/ /* Gap */ - --gap-*: initial; + /*--gap-*: initial;*/ /* Margin */ - --margin-*: initial; + /*--margin-*: initial;*/ /* Flex */ - --flex-col1: 8.33333333333%; + /*--flex-col1: 8.33333333333%; --flex-col2: 16.66666666667%; --flex-col3: 25%; --flex-col4: 33.33333333333%; @@ -50,7 +50,7 @@ --flex-col9: 75%; --flex-col10: 83.33333333333%; --flex-col11: 91.66666666667%; - --flex-col12: 100%; + --flex-col12: 100%;*/ } html { diff --git a/frontend/entrypoints/ts/cart/drawer.ts b/frontend/entrypoints/ts/cart/drawer.ts index c7cf93df6..47caa90a9 100644 --- a/frontend/entrypoints/ts/cart/drawer.ts +++ b/frontend/entrypoints/ts/cart/drawer.ts @@ -1,4 +1,5 @@ import { changeCartLine } from '../utils/cart'; +import { CART_OPEN_EVENT, CART_UPDATED_EVENT } from '../utils/cart-events'; import { applySectionReplace, fetchSingleSectionHtml, @@ -79,6 +80,12 @@ export function initCartDrawer(): void { }); }; + const getCurrentCount = (): number => { + const firstCount = countNodes[0]; + if (!firstCount) return 0; + return Number(firstCount.textContent ?? '0') || 0; + }; + const countItemsFromDrawerMarkup = (): number => { const qtyNodes = itemsContainer.querySelectorAll('[data-js="cart-qty-value"]'); return Array.from(qtyNodes).reduce((sum, node) => { @@ -272,6 +279,25 @@ export function initCartDrawer(): void { closeButton.addEventListener('click', closeDrawer); overlay.addEventListener('click', closeDrawer); + window.addEventListener(CART_OPEN_EVENT, () => { + openDrawer(); + }); + + window.addEventListener(CART_UPDATED_EVENT, (event) => { + const customEvent = event as CustomEvent<{ itemCount?: number; itemCountDelta?: number; openDrawer?: boolean }>; + const detail = customEvent.detail; + + if (typeof detail?.itemCount === 'number') { + updateCount(detail.itemCount); + } else if (typeof detail?.itemCountDelta === 'number') { + updateCount(Math.max(0, getCurrentCount() + detail.itemCountDelta)); + } + + if (detail?.openDrawer) { + openDrawer(); + } + }); + drawer.addEventListener('click', (event) => { const target = event.target as HTMLElement; const actionButton = target.closest( diff --git a/frontend/entrypoints/ts/collection.ts b/frontend/entrypoints/ts/collection.ts index 5f86d2c79..fea3dfe9d 100644 --- a/frontend/entrypoints/ts/collection.ts +++ b/frontend/entrypoints/ts/collection.ts @@ -1,3 +1,6 @@ +import { addToCart } from './utils/cart'; +import { emitCartOpen, emitCartUpdated } from './utils/cart-events'; + const ROOT_SELECTOR = '[data-js="collection-root"]'; interface CollectionView { @@ -46,6 +49,46 @@ function setBusy(view: CollectionView, busy: boolean): void { view.root.setAttribute('aria-busy', String(busy)); } +async function runQuickBuy(button: HTMLButtonElement, fallbackUrl: string): Promise { + const variantId = Number(button.dataset.variantId ?? ''); + if (!variantId) { + if (fallbackUrl) { + window.location.assign(fallbackUrl); + } + return; + } + + const idleLabel = button.dataset.labelIdle ?? 'Quick buy'; + const loadingLabel = button.dataset.labelLoading ?? 'Adding...'; + const successLabel = button.dataset.labelSuccess ?? 'Added'; + const errorLabel = button.dataset.labelError ?? 'Try again'; + + button.disabled = true; + button.setAttribute('aria-busy', 'true'); + button.textContent = loadingLabel; + + try { + await addToCart(variantId, 1); + emitCartUpdated({ itemCountDelta: 1 }); + emitCartOpen(); + + button.textContent = successLabel; + window.setTimeout(() => { + button.textContent = idleLabel; + button.disabled = false; + button.setAttribute('aria-busy', 'false'); + }, 1200); + } catch { + button.textContent = errorLabel; + + window.setTimeout(() => { + button.textContent = idleLabel; + button.disabled = false; + button.setAttribute('aria-busy', 'false'); + }, 1500); + } +} + function toUrl(pathOrUrl: string): URL { return new URL(pathOrUrl, window.location.origin); } @@ -199,6 +242,16 @@ document.addEventListener('DOMContentLoaded', () => { const onRootClick = (event: MouseEvent): void => { const target = event.target as HTMLElement; + const quickBuyButton = target.closest('[data-js="collection-quick-buy"]'); + if (quickBuyButton) { + event.preventDefault(); + if (quickBuyButton.disabled) return; + + const fallbackUrl = quickBuyButton.dataset.productUrl ?? ''; + void runQuickBuy(quickBuyButton, fallbackUrl); + return; + } + const loadMoreButton = target.closest('[data-js="collection-load-more"]'); if (loadMoreButton) { event.preventDefault(); diff --git a/frontend/entrypoints/ts/product/handlers.ts b/frontend/entrypoints/ts/product/handlers.ts index 32512548a..28fcadfe6 100644 --- a/frontend/entrypoints/ts/product/handlers.ts +++ b/frontend/entrypoints/ts/product/handlers.ts @@ -1,5 +1,6 @@ import { findVariantByOptions } from '../utils/variant-picker'; import { addToCart } from '../utils/cart'; +import { emitCartOpen, emitCartUpdated } from '../utils/cart-events'; import { state } from './state'; import { syncDOM } from './sync'; @@ -79,6 +80,8 @@ export async function onAddToCart(e: Event): Promise { try { await addToCart(state.currentVariant.id, quantity); + emitCartUpdated({ itemCountDelta: quantity }); + emitCartOpen(); state.cartState = 'success'; syncDOM(); diff --git a/frontend/entrypoints/ts/utils/cart-events.ts b/frontend/entrypoints/ts/utils/cart-events.ts new file mode 100644 index 000000000..8c35e9be5 --- /dev/null +++ b/frontend/entrypoints/ts/utils/cart-events.ts @@ -0,0 +1,16 @@ +export interface CartUpdatedDetail { + itemCount?: number; + itemCountDelta?: number; + openDrawer?: boolean; +} + +export const CART_UPDATED_EVENT = 'cart:updated'; +export const CART_OPEN_EVENT = 'cart:open'; + +export function emitCartUpdated(detail: CartUpdatedDetail): void { + window.dispatchEvent(new CustomEvent(CART_UPDATED_EVENT, { detail })); +} + +export function emitCartOpen(): void { + window.dispatchEvent(new CustomEvent(CART_OPEN_EVENT)); +} diff --git a/locales/en.default.json b/locales/en.default.json index a072acaac..d87fbed59 100644 --- a/locales/en.default.json +++ b/locales/en.default.json @@ -30,7 +30,12 @@ } }, "collections": { - "title": "Collections" + "title": "Collections", + "quick_buy": "Quick buy", + "quick_buy_loading": "Adding...", + "quick_buy_success": "Added", + "quick_buy_error": "Try again", + "view_options": "View options" }, "gift_card": { "add_to_apple_wallet": "Add to Apple Wallet", diff --git a/locales/it.json b/locales/it.json new file mode 100644 index 000000000..6c0ec5a32 --- /dev/null +++ b/locales/it.json @@ -0,0 +1,59 @@ +{ + "404": { + "title": "404", + "not_found": "Pagina non trovata.", + "back_to_shopping": "Torna agli acquisti" + }, + "blog": { + "article_comments": "Commenti", + "article_metadata_html": "{{ date }} di {{ author }}", + "comment_form_body": "Commento", + "comment_form_email": "Email", + "comment_form_name": "Nome", + "comment_form_submit": "Pubblica", + "comment_form_title": "Aggiungi un commento" + }, + "cart": { + "checkout": "Checkout", + "note": "Nota ordine", + "order_reference": "Riferimento ordine", + "title": "Carrello", + "update": "Aggiorna", + "remove": "Rimuovi" + }, + "customers": { + "login": { + "email": "Email", + "password": "Password", + "submit": "Accedi", + "title": "Accesso" + } + }, + "collections": { + "title": "Collezioni", + "quick_buy": "Acquista subito", + "quick_buy_loading": "Aggiunta...", + "quick_buy_success": "Aggiunto", + "quick_buy_error": "Riprova", + "view_options": "Vedi opzioni" + }, + "gift_card": { + "add_to_apple_wallet": "Aggiungi ad Apple Wallet", + "card": "Carta regalo", + "expired": "Questa carta regalo e scaduta", + "expires_on": "Scade il {{ expires_on }}", + "use_at_checkout": "Usa questa carta regalo al checkout" + }, + "password": { + "title": "Questo negozio e privato", + "password": "Password", + "enter": "Entra" + }, + "search": { + "title": "Cerca", + "placeholder": "Cerca articoli, pagine o prodotti", + "submit": "Cerca", + "no_results_html": "Nessun risultato per {{ terms }}", + "results_for_html": "{{ count }} risultati per {{ terms }}" + } +} diff --git a/sections/collection.liquid b/sections/collection.liquid index 492ad7d94..31e44cfd6 100644 --- a/sections/collection.liquid +++ b/sections/collection.liquid @@ -125,22 +125,7 @@
      {% for product in collection.products %} -
      - {% if product.featured_image %} - {% render 'image', - class: 'collection-product__image', - image: product.featured_image, - url: product.url, - width: 400, - height: 400, - crop: 'center' - %} - {% endif %} -
      -

      {{ product.title | escape | link_to: product.url }}

      -

      {{ product.price | money }}

      -
      -
      + {% render 'product-card', product: product, enable_quick_buy: true %} {% else %}

      No products found for current filters.

      {% endfor %} diff --git a/sections/product-recommendations.liquid b/sections/product-recommendations.liquid index 852703348..afbd2ce4b 100644 --- a/sections/product-recommendations.liquid +++ b/sections/product-recommendations.liquid @@ -5,14 +5,7 @@ diff --git a/snippets/product-card-quick-buy.liquid b/snippets/product-card-quick-buy.liquid new file mode 100644 index 000000000..505b5d976 --- /dev/null +++ b/snippets/product-card-quick-buy.liquid @@ -0,0 +1,27 @@ +{% doc %} + Product-card quick-buy molecule. + + Renders quick-buy CTA when possible, otherwise a product-page fallback link. + + @param {product} product - Product object + @param {variant} [quick_buy_variant] - Variant used for quick buy + @param {boolean} [enable_quick_buy] - Enables quick-buy behavior +{% enddoc %} + +{% if enable_quick_buy and quick_buy_variant %} + +{% else %} + {{ 'collections.view_options' | t }} +{% endif %} diff --git a/snippets/product-card.liquid b/snippets/product-card.liquid new file mode 100644 index 000000000..c7ca089fd --- /dev/null +++ b/snippets/product-card.liquid @@ -0,0 +1,39 @@ +{% doc %} + Reusable product card for collection-like grids. + + Shows image, title, price, and a quick-buy control when the product can be + added directly (single default variant available). + + @param {product} product - Product object to render + @param {boolean} [enable_quick_buy] - Enables quick buy CTA when possible +{% enddoc %} + +{%- liquid + assign quick_buy_variant = null + if product.available and product.has_only_default_variant + assign quick_buy_variant = product.selected_or_first_available_variant + endif +-%} + +
      + {% if product.featured_image %} + {% render 'image', + class: 'collection-product__image', + image: product.featured_image, + url: product.url, + width: 400, + height: 400, + crop: 'center' + %} + {% endif %} + +
      +

      {{ product.title | escape | link_to: product.url }}

      +

      {{ product.price | money }}

      + {% render 'product-card-quick-buy', + product: product, + quick_buy_variant: quick_buy_variant, + enable_quick_buy: enable_quick_buy + %} +
      +
      diff --git a/todo.md b/todo.md index 2d2eb702a..f94869761 100644 --- a/todo.md +++ b/todo.md @@ -12,6 +12,7 @@ - [x] P0 complete: section rendering foundation, Shopify docs parity, PDP media reliability - [x] P1 complete except final manual verification - [x] Search and cart core hardening shipped (predictive drawer, cart notes/attributes) +- [x] Reusable `product-card` snippet added with PLP quick buy (single-variant) + PDP fallback - [x] PDP recommendations no longer tied to variant switching - [x] PDP media now keeps variant-primary media first in viewer + thumbnails while keeping shared media - [x] Core checks passing in this environment: `bun run typecheck`, `bun run build` From 98450d25148bff08d5d3b88002bfe9e0b7343483 Mon Sep 17 00:00:00 2001 From: Luca Argentieri Date: Mon, 23 Mar 2026 16:08:24 +0100 Subject: [PATCH 14/14] upgrade to vite 8 --- assets/.vite/manifest.json | 95 +++++++------- assets/404-BPQrMuxs.js | 0 assets/404-l0sNRNKZ.js | 1 - assets/article-C-l2wKs3.js | 0 assets/article-l0sNRNKZ.js | 1 - assets/blog-Ch1z4KXd.js | 0 assets/blog-l0sNRNKZ.js | 1 - assets/cart-DExqgdDq.js | 1 - assets/cart-DFJfubsI.js | 1 + assets/cart-Dkg_Sf-E.js | 1 + assets/cart-Kw4e-iq4.js | 1 - assets/cart-events-C4m0PwR1.js | 1 - assets/cart-events-DFMQ7aoZ.js | 1 + assets/collection-B8aRXGib.js | 1 + assets/collection-Bqtxr7aJ.js | 1 - assets/drawer-BOuNB4a_.js | 1 - assets/drawer-BY7bJn32.js | 1 + assets/drawer-CJwC366z.js | 1 - assets/drawer-DwueD-Lo.js | 1 + assets/gift-card-Ciee4aDH.js | 0 assets/gift-card-l0sNRNKZ.js | 1 - assets/handlers-B949bjPl.js | 1 - assets/handlers-CMuQJ9_L.js | 1 + assets/index-D_EjmhRG.js | 0 assets/index-l0sNRNKZ.js | 1 - assets/list-collections-87bPu3lg.js | 0 assets/list-collections-l0sNRNKZ.js | 1 - assets/main-DNSsntQO.css | 1 - assets/main-JmcnsjPw.css | 2 + assets/page-Bwhrow8I.js | 1 - assets/page-CKX_Lx8T.js | 1 + assets/page-EITKwFv5.js | 0 assets/page-l0sNRNKZ.js | 1 - assets/password-BArd4X24.js | 0 assets/password-l0sNRNKZ.js | 1 - assets/product-B6uO69WW.js | 1 + assets/product-_rxK6zOl.js | 1 - assets/recommendations-CnpPRP46.js | 1 + assets/recommendations-H0stnrna.js | 1 - assets/search-EaqtnZZe.js | 1 + assets/search-bqQT_R15.js | 1 - assets/section-rendering-COdqImLj.js | 1 + assets/section-rendering-vVqNxfcB.js | 1 - assets/state-Bwp_aMHz.js | 1 - assets/state-yJSoUqNP.js | 1 + assets/sync-BRYiu7LQ.js | 1 - assets/sync-CuhjKoMp.js | 1 + assets/theme-DcEodf02.js | 2 + assets/theme-skURl5jI.js | 2 - assets/variant-picker-BcihOqVL.js | 1 + assets/variant-picker-DRrQ71gT.js | 1 - bun.lock | 184 +++++++++------------------ package.json | 10 +- snippets/vite-tag.liquid | 101 +++++++++++---- 54 files changed, 210 insertions(+), 225 deletions(-) create mode 100644 assets/404-BPQrMuxs.js delete mode 100644 assets/404-l0sNRNKZ.js create mode 100644 assets/article-C-l2wKs3.js delete mode 100644 assets/article-l0sNRNKZ.js create mode 100644 assets/blog-Ch1z4KXd.js delete mode 100644 assets/blog-l0sNRNKZ.js delete mode 100644 assets/cart-DExqgdDq.js create mode 100644 assets/cart-DFJfubsI.js create mode 100644 assets/cart-Dkg_Sf-E.js delete mode 100644 assets/cart-Kw4e-iq4.js delete mode 100644 assets/cart-events-C4m0PwR1.js create mode 100644 assets/cart-events-DFMQ7aoZ.js create mode 100644 assets/collection-B8aRXGib.js delete mode 100644 assets/collection-Bqtxr7aJ.js delete mode 100644 assets/drawer-BOuNB4a_.js create mode 100644 assets/drawer-BY7bJn32.js delete mode 100644 assets/drawer-CJwC366z.js create mode 100644 assets/drawer-DwueD-Lo.js create mode 100644 assets/gift-card-Ciee4aDH.js delete mode 100644 assets/gift-card-l0sNRNKZ.js delete mode 100644 assets/handlers-B949bjPl.js create mode 100644 assets/handlers-CMuQJ9_L.js create mode 100644 assets/index-D_EjmhRG.js delete mode 100644 assets/index-l0sNRNKZ.js create mode 100644 assets/list-collections-87bPu3lg.js delete mode 100644 assets/list-collections-l0sNRNKZ.js delete mode 100644 assets/main-DNSsntQO.css create mode 100644 assets/main-JmcnsjPw.css delete mode 100644 assets/page-Bwhrow8I.js create mode 100644 assets/page-CKX_Lx8T.js create mode 100644 assets/page-EITKwFv5.js delete mode 100644 assets/page-l0sNRNKZ.js create mode 100644 assets/password-BArd4X24.js delete mode 100644 assets/password-l0sNRNKZ.js create mode 100644 assets/product-B6uO69WW.js delete mode 100644 assets/product-_rxK6zOl.js create mode 100644 assets/recommendations-CnpPRP46.js delete mode 100644 assets/recommendations-H0stnrna.js create mode 100644 assets/search-EaqtnZZe.js delete mode 100644 assets/search-bqQT_R15.js create mode 100644 assets/section-rendering-COdqImLj.js delete mode 100644 assets/section-rendering-vVqNxfcB.js delete mode 100644 assets/state-Bwp_aMHz.js create mode 100644 assets/state-yJSoUqNP.js delete mode 100644 assets/sync-BRYiu7LQ.js create mode 100644 assets/sync-CuhjKoMp.js create mode 100644 assets/theme-DcEodf02.js delete mode 100644 assets/theme-skURl5jI.js create mode 100644 assets/variant-picker-BcihOqVL.js delete mode 100644 assets/variant-picker-DRrQ71gT.js diff --git a/assets/.vite/manifest.json b/assets/.vite/manifest.json index 628eb2b0f..936fb0f89 100644 --- a/assets/.vite/manifest.json +++ b/assets/.vite/manifest.json @@ -1,56 +1,54 @@ { "frontend/entrypoints/css/main.css": { - "file": "main-DNSsntQO.css", - "src": "frontend/entrypoints/css/main.css", - "isEntry": true, + "file": "main-JmcnsjPw.css", "name": "main", "names": [ "main.css" - ] + ], + "src": "frontend/entrypoints/css/main.css", + "isEntry": true }, "frontend/entrypoints/ts/404.ts": { - "file": "404-l0sNRNKZ.js", + "file": "404-BPQrMuxs.js", "name": "404", "src": "frontend/entrypoints/ts/404.ts", "isEntry": true }, "frontend/entrypoints/ts/article.ts": { - "file": "article-l0sNRNKZ.js", + "file": "article-C-l2wKs3.js", "name": "article", "src": "frontend/entrypoints/ts/article.ts", "isEntry": true }, "frontend/entrypoints/ts/blog.ts": { - "file": "blog-l0sNRNKZ.js", + "file": "blog-Ch1z4KXd.js", "name": "blog", "src": "frontend/entrypoints/ts/blog.ts", "isEntry": true }, "frontend/entrypoints/ts/cart.ts": { - "file": "cart-DExqgdDq.js", + "file": "cart-Dkg_Sf-E.js", "name": "cart", "src": "frontend/entrypoints/ts/cart.ts", "isEntry": true, "imports": [ "frontend/entrypoints/ts/cart/page.ts", - "frontend/entrypoints/ts/utils/cart.ts", - "frontend/entrypoints/ts/utils/section-rendering.ts" + "frontend/entrypoints/ts/cart/page.ts" ] }, "frontend/entrypoints/ts/cart/drawer.ts": { - "file": "drawer-CJwC366z.js", + "file": "drawer-BY7bJn32.js", "name": "drawer", "src": "frontend/entrypoints/ts/cart/drawer.ts", "isEntry": true, - "isDynamicEntry": true, "imports": [ - "frontend/entrypoints/ts/utils/cart.ts", "frontend/entrypoints/ts/utils/cart-events.ts", + "frontend/entrypoints/ts/utils/cart.ts", "frontend/entrypoints/ts/utils/section-rendering.ts" ] }, "frontend/entrypoints/ts/cart/page.ts": { - "file": "page-Bwhrow8I.js", + "file": "page-CKX_Lx8T.js", "name": "page", "src": "frontend/entrypoints/ts/cart/page.ts", "isEntry": true, @@ -60,110 +58,113 @@ ] }, "frontend/entrypoints/ts/collection.ts": { - "file": "collection-Bqtxr7aJ.js", + "file": "collection-B8aRXGib.js", "name": "collection", "src": "frontend/entrypoints/ts/collection.ts", "isEntry": true, "imports": [ - "frontend/entrypoints/ts/utils/cart.ts", - "frontend/entrypoints/ts/utils/cart-events.ts" + "frontend/entrypoints/ts/utils/cart-events.ts", + "frontend/entrypoints/ts/utils/cart.ts" ] }, "frontend/entrypoints/ts/gift-card.ts": { - "file": "gift-card-l0sNRNKZ.js", + "file": "gift-card-Ciee4aDH.js", "name": "gift-card", "src": "frontend/entrypoints/ts/gift-card.ts", "isEntry": true }, "frontend/entrypoints/ts/index.ts": { - "file": "index-l0sNRNKZ.js", + "file": "index-D_EjmhRG.js", "name": "index", "src": "frontend/entrypoints/ts/index.ts", "isEntry": true }, "frontend/entrypoints/ts/list-collections.ts": { - "file": "list-collections-l0sNRNKZ.js", + "file": "list-collections-87bPu3lg.js", "name": "list-collections", "src": "frontend/entrypoints/ts/list-collections.ts", "isEntry": true }, "frontend/entrypoints/ts/page.ts": { - "file": "page-l0sNRNKZ.js", + "file": "page-EITKwFv5.js", "name": "page", "src": "frontend/entrypoints/ts/page.ts", "isEntry": true }, "frontend/entrypoints/ts/password.ts": { - "file": "password-l0sNRNKZ.js", + "file": "password-BArd4X24.js", "name": "password", "src": "frontend/entrypoints/ts/password.ts", "isEntry": true }, "frontend/entrypoints/ts/product.ts": { - "file": "product-_rxK6zOl.js", + "file": "product-B6uO69WW.js", "name": "product", "src": "frontend/entrypoints/ts/product.ts", "isEntry": true, "imports": [ - "frontend/entrypoints/ts/product/state.ts", - "frontend/entrypoints/ts/product/sync.ts", + "frontend/entrypoints/ts/product/handlers.ts", "frontend/entrypoints/ts/product/handlers.ts", "frontend/entrypoints/ts/product/recommendations.ts", - "frontend/entrypoints/ts/utils/variant-picker.ts", - "frontend/entrypoints/ts/utils/cart.ts", - "frontend/entrypoints/ts/utils/cart-events.ts" + "frontend/entrypoints/ts/product/recommendations.ts", + "frontend/entrypoints/ts/product/state.ts", + "frontend/entrypoints/ts/product/state.ts", + "frontend/entrypoints/ts/product/sync.ts", + "frontend/entrypoints/ts/product/sync.ts" ] }, "frontend/entrypoints/ts/product/handlers.ts": { - "file": "handlers-B949bjPl.js", + "file": "handlers-CMuQJ9_L.js", "name": "handlers", "src": "frontend/entrypoints/ts/product/handlers.ts", "isEntry": true, "imports": [ - "frontend/entrypoints/ts/utils/variant-picker.ts", - "frontend/entrypoints/ts/utils/cart.ts", - "frontend/entrypoints/ts/utils/cart-events.ts", "frontend/entrypoints/ts/product/state.ts", - "frontend/entrypoints/ts/product/sync.ts" + "frontend/entrypoints/ts/product/state.ts", + "frontend/entrypoints/ts/product/sync.ts", + "frontend/entrypoints/ts/product/sync.ts", + "frontend/entrypoints/ts/utils/cart-events.ts", + "frontend/entrypoints/ts/utils/cart.ts", + "frontend/entrypoints/ts/utils/variant-picker.ts" ] }, "frontend/entrypoints/ts/product/recommendations.ts": { - "file": "recommendations-H0stnrna.js", + "file": "recommendations-CnpPRP46.js", "name": "recommendations", "src": "frontend/entrypoints/ts/product/recommendations.ts", "isEntry": true }, "frontend/entrypoints/ts/product/state.ts": { - "file": "state-Bwp_aMHz.js", + "file": "state-yJSoUqNP.js", "name": "state", "src": "frontend/entrypoints/ts/product/state.ts", "isEntry": true }, "frontend/entrypoints/ts/product/sync.ts": { - "file": "sync-BRYiu7LQ.js", + "file": "sync-CuhjKoMp.js", "name": "sync", "src": "frontend/entrypoints/ts/product/sync.ts", "isEntry": true, "imports": [ - "frontend/entrypoints/ts/utils/variant-picker.ts", - "frontend/entrypoints/ts/product/state.ts" + "frontend/entrypoints/ts/product/state.ts", + "frontend/entrypoints/ts/product/state.ts", + "frontend/entrypoints/ts/utils/variant-picker.ts" ] }, "frontend/entrypoints/ts/search.ts": { - "file": "search-bqQT_R15.js", + "file": "search-EaqtnZZe.js", "name": "search", "src": "frontend/entrypoints/ts/search.ts", "isEntry": true }, "frontend/entrypoints/ts/search/drawer.ts": { - "file": "drawer-BOuNB4a_.js", + "file": "drawer-DwueD-Lo.js", "name": "drawer", "src": "frontend/entrypoints/ts/search/drawer.ts", - "isEntry": true, - "isDynamicEntry": true + "isEntry": true }, "frontend/entrypoints/ts/theme.ts": { - "file": "theme-skURl5jI.js", + "file": "theme-DcEodf02.js", "name": "theme", "src": "frontend/entrypoints/ts/theme.ts", "isEntry": true, @@ -173,25 +174,25 @@ ] }, "frontend/entrypoints/ts/utils/cart-events.ts": { - "file": "cart-events-C4m0PwR1.js", + "file": "cart-events-DFMQ7aoZ.js", "name": "cart-events", "src": "frontend/entrypoints/ts/utils/cart-events.ts", "isEntry": true }, "frontend/entrypoints/ts/utils/cart.ts": { - "file": "cart-Kw4e-iq4.js", + "file": "cart-DFJfubsI.js", "name": "cart", "src": "frontend/entrypoints/ts/utils/cart.ts", "isEntry": true }, "frontend/entrypoints/ts/utils/section-rendering.ts": { - "file": "section-rendering-vVqNxfcB.js", + "file": "section-rendering-COdqImLj.js", "name": "section-rendering", "src": "frontend/entrypoints/ts/utils/section-rendering.ts", "isEntry": true }, "frontend/entrypoints/ts/utils/variant-picker.ts": { - "file": "variant-picker-DRrQ71gT.js", + "file": "variant-picker-BcihOqVL.js", "name": "variant-picker", "src": "frontend/entrypoints/ts/utils/variant-picker.ts", "isEntry": true diff --git a/assets/404-BPQrMuxs.js b/assets/404-BPQrMuxs.js new file mode 100644 index 000000000..e69de29bb diff --git a/assets/404-l0sNRNKZ.js b/assets/404-l0sNRNKZ.js deleted file mode 100644 index 8b1378917..000000000 --- a/assets/404-l0sNRNKZ.js +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/article-C-l2wKs3.js b/assets/article-C-l2wKs3.js new file mode 100644 index 000000000..e69de29bb diff --git a/assets/article-l0sNRNKZ.js b/assets/article-l0sNRNKZ.js deleted file mode 100644 index 8b1378917..000000000 --- a/assets/article-l0sNRNKZ.js +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/blog-Ch1z4KXd.js b/assets/blog-Ch1z4KXd.js new file mode 100644 index 000000000..e69de29bb diff --git a/assets/blog-l0sNRNKZ.js b/assets/blog-l0sNRNKZ.js deleted file mode 100644 index 8b1378917..000000000 --- a/assets/blog-l0sNRNKZ.js +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/cart-DExqgdDq.js b/assets/cart-DExqgdDq.js deleted file mode 100644 index 26f89ee40..000000000 --- a/assets/cart-DExqgdDq.js +++ /dev/null @@ -1 +0,0 @@ -import{i as t}from"./page-Bwhrow8I.js";import"./cart-Kw4e-iq4.js";import"./section-rendering-vVqNxfcB.js";document.addEventListener("DOMContentLoaded",()=>{t()}); diff --git a/assets/cart-DFJfubsI.js b/assets/cart-DFJfubsI.js new file mode 100644 index 000000000..f23e0470a --- /dev/null +++ b/assets/cart-DFJfubsI.js @@ -0,0 +1 @@ +async function e(e){let t=await e.json().catch(()=>({}));return Error(t.description??`Cart request failed`)}function t(e){return e?e.startsWith(`/`)?e:`/${e}`:`/`}async function n(n,r,i={}){let a={line:n,quantity:r};i.sections&&(a.sections=i.sections),i.sectionsUrl&&(a.sections_url=t(i.sectionsUrl));let o=await fetch(`${window.Shopify.routes.root}cart/change.js`,{method:`POST`,headers:{"Content-Type":`application/json`,Accept:`application/json`,"X-Requested-With":`XMLHttpRequest`},body:JSON.stringify(a)});if(!o.ok)throw await e(o);return await o.json()}async function r(t,n){let r=await fetch(`${window.Shopify.routes.root}cart/add.js`,{method:`POST`,headers:{"Content-Type":`application/json`,"X-Requested-With":`XMLHttpRequest`},body:JSON.stringify({id:t,quantity:n})});if(!r.ok)throw await e(r)}export{r as addToCart,n as changeCartLine}; \ No newline at end of file diff --git a/assets/cart-Dkg_Sf-E.js b/assets/cart-Dkg_Sf-E.js new file mode 100644 index 000000000..6a0037c7d --- /dev/null +++ b/assets/cart-Dkg_Sf-E.js @@ -0,0 +1 @@ +import{initCartPage as e}from"./page-CKX_Lx8T.js";document.addEventListener(`DOMContentLoaded`,()=>{e()}); \ No newline at end of file diff --git a/assets/cart-Kw4e-iq4.js b/assets/cart-Kw4e-iq4.js deleted file mode 100644 index 2ccaa632b..000000000 --- a/assets/cart-Kw4e-iq4.js +++ /dev/null @@ -1 +0,0 @@ -async function s(t){const n=await t.json().catch(()=>({}));return new Error(n.description??"Cart request failed")}function i(t){return t?t.startsWith("/")?t:`/${t}`:"/"}async function r(t,n,e={}){const o={line:t,quantity:n};e.sections&&(o.sections=e.sections),e.sectionsUrl&&(o.sections_url=i(e.sectionsUrl));const a=await fetch(`${window.Shopify.routes.root}cart/change.js`,{method:"POST",headers:{"Content-Type":"application/json",Accept:"application/json","X-Requested-With":"XMLHttpRequest"},body:JSON.stringify(o)});if(!a.ok)throw await s(a);return await a.json()}async function c(t,n){const e=await fetch(`${window.Shopify.routes.root}cart/add.js`,{method:"POST",headers:{"Content-Type":"application/json","X-Requested-With":"XMLHttpRequest"},body:JSON.stringify({id:t,quantity:n})});if(!e.ok)throw await s(e)}export{c as a,r as c}; diff --git a/assets/cart-events-C4m0PwR1.js b/assets/cart-events-C4m0PwR1.js deleted file mode 100644 index 6d51f8a7c..000000000 --- a/assets/cart-events-C4m0PwR1.js +++ /dev/null @@ -1 +0,0 @@ -const n="cart:updated",e="cart:open";function a(t){window.dispatchEvent(new CustomEvent(n,{detail:t}))}function o(){window.dispatchEvent(new CustomEvent(e))}export{e as C,o as a,n as b,a as e}; diff --git a/assets/cart-events-DFMQ7aoZ.js b/assets/cart-events-DFMQ7aoZ.js new file mode 100644 index 000000000..1f861ba69 --- /dev/null +++ b/assets/cart-events-DFMQ7aoZ.js @@ -0,0 +1 @@ +var e=`cart:updated`,t=`cart:open`;function n(t){window.dispatchEvent(new CustomEvent(e,{detail:t}))}function r(){window.dispatchEvent(new CustomEvent(t))}export{t as CART_OPEN_EVENT,e as CART_UPDATED_EVENT,r as emitCartOpen,n as emitCartUpdated}; \ No newline at end of file diff --git a/assets/collection-B8aRXGib.js b/assets/collection-B8aRXGib.js new file mode 100644 index 000000000..1c1b3b8b0 --- /dev/null +++ b/assets/collection-B8aRXGib.js @@ -0,0 +1 @@ +import{addToCart as e}from"./cart-DFJfubsI.js";import{emitCartOpen as t,emitCartUpdated as n}from"./cart-events-DFMQ7aoZ.js";var r=`[data-js="collection-root"]`;function i(e){return{root:e,sectionId:e.dataset.sectionId??``,controls:e.querySelector(`[data-js="collection-controls"]`),products:e.querySelector(`[data-js="collection-products"]`),paginationWrap:e.querySelector(`[data-js="collection-pagination-wrap"]`),loadStatus:e.querySelector(`[data-js="collection-load-status"]`),status:e.querySelector(`[data-js="collection-status"]`),error:e.querySelector(`[data-js="collection-error"]`)}}function a(e,t){e.status&&(e.status.textContent=t)}function o(e,t){e.loadStatus&&(e.loadStatus.textContent=t)}function s(e,t){e.error&&(e.error.textContent=t)}function c(e,t){e.root.setAttribute(`aria-busy`,String(t))}async function l(r,i){let a=Number(r.dataset.variantId??``);if(!a){i&&window.location.assign(i);return}let o=r.dataset.labelIdle??`Quick buy`,s=r.dataset.labelLoading??`Adding...`,c=r.dataset.labelSuccess??`Added`,l=r.dataset.labelError??`Try again`;r.disabled=!0,r.setAttribute(`aria-busy`,`true`),r.textContent=s;try{await e(a,1),n({itemCountDelta:1}),t(),r.textContent=c,window.setTimeout(()=>{r.textContent=o,r.disabled=!1,r.setAttribute(`aria-busy`,`false`)},1200)}catch{r.textContent=l,window.setTimeout(()=>{r.textContent=o,r.disabled=!1,r.setAttribute(`aria-busy`,`false`)},1500)}}function u(e){return new URL(e,window.location.origin)}function d(e,t){let n=new URL(e.toString());return n.searchParams.set(`section_id`,t),`${n.pathname}${n.search}`}function f(e){let t=u(e.action||window.location.href),n=new URLSearchParams;return new FormData(e).forEach((e,t)=>{let r=String(e).trim();r.length>0&&n.append(t,r)}),t.search=n.toString(),t}function p(e){return new DOMParser().parseFromString(e,`text/html`).querySelector(r)}document.addEventListener(`DOMContentLoaded`,()=>{let e=document.querySelector(r);if(!e)return;let t=e,n=i(t);if(!n.sectionId)return;let m=0,h=null,g=e=>{t=e,n=i(e)},_=async(e,t)=>{h?.abort(),h=new AbortController;let r=await fetch(d(e,n.sectionId),{signal:h.signal,headers:{"X-Requested-With":`XMLHttpRequest`}});if(!r.ok)throw Error(`Collection section request failed`);let i=await r.text();if(t!==m)throw new DOMException(`Stale collection request`,`AbortError`);let a=p(i);if(!a)throw Error(`Collection section parse failed`);return a},v=async(e,r)=>{let i=++m;c(n,!0),s(n,``),a(n,`Loading products...`);try{let s=await _(e,i);t.replaceWith(s),g(s),r&&window.history.pushState({source:`collection-replace`},``,`${e.pathname}${e.search}`),a(n,`Products updated.`),o(n,``)}catch(e){if(e instanceof DOMException&&e.name===`AbortError`)return;s(n,`Could not update collection right now. Please try again.`),a(n,`Collection update failed.`)}finally{i===m&&c(n,!1)}},y=async e=>{let t=u(e),r=++m;c(n,!0),s(n,``),o(n,`Loading more products...`);try{let e=i(await _(t,r));if(!n.products||!e.products||!n.paginationWrap||!e.paginationWrap)throw Error(`Collection append targets missing`);let s=document.createDocumentFragment();e.products.querySelectorAll(`[data-js="collection-product-card"]`).forEach(e=>{s.appendChild(e)}),n.products.appendChild(s),n.paginationWrap.replaceWith(e.paginationWrap),n.paginationWrap=e.paginationWrap,window.history.pushState({source:`collection-append`},``,`${t.pathname}${t.search}`),o(n,`More products loaded.`),a(n,`Collection updated.`)}catch(e){if(e instanceof DOMException&&e.name===`AbortError`)return;s(n,`Could not load more products. Please try again.`),o(n,`Load more failed.`)}finally{r===m&&c(n,!1)}},b=e=>{let t=e.target,n=t.closest(`[data-js="collection-quick-buy"]`);if(n){if(e.preventDefault(),n.disabled)return;l(n,n.dataset.productUrl??``);return}let r=t.closest(`[data-js="collection-load-more"]`);if(r){e.preventDefault();let t=r.dataset.nextUrl;if(!t||r.disabled)return;r.disabled=!0,r.setAttribute(`aria-busy`,`true`),y(t).finally(()=>{r.disabled=!1,r.setAttribute(`aria-busy`,`false`)});return}let i=t.closest(`[data-js="collection-clear"]`);if(i){e.preventDefault(),v(u(i.href),!0);return}let a=t.closest(`[data-js="collection-filter-remove"]`);if(a){e.preventDefault(),v(u(a.href),!0);return}let o=t.closest(`[data-js="collection-default-pagination"] a`);o&&(e.preventDefault(),v(u(o.href),!0))},x=e=>{!e.target.closest(`input, select`)||!n.controls||n.controls.requestSubmit()},S=e=>{let t=e.target;t.matches(`[data-js="collection-controls"]`)&&(e.preventDefault(),v(f(t),!0))};document.addEventListener(`click`,e=>{t.contains(e.target)&&b(e)}),document.addEventListener(`change`,e=>{t.contains(e.target)&&x(e)}),document.addEventListener(`submit`,e=>{t.contains(e.target)&&S(e)}),window.addEventListener(`popstate`,()=>{v(u(window.location.href),!1)})}); \ No newline at end of file diff --git a/assets/collection-Bqtxr7aJ.js b/assets/collection-Bqtxr7aJ.js deleted file mode 100644 index 7da665f2a..000000000 --- a/assets/collection-Bqtxr7aJ.js +++ /dev/null @@ -1 +0,0 @@ -import{a as v}from"./cart-Kw4e-iq4.js";import{e as j,a as A}from"./cart-events-C4m0PwR1.js";const L='[data-js="collection-root"]';function b(t){return{root:t,sectionId:t.dataset.sectionId??"",controls:t.querySelector('[data-js="collection-controls"]'),products:t.querySelector('[data-js="collection-products"]'),paginationWrap:t.querySelector('[data-js="collection-pagination-wrap"]'),loadStatus:t.querySelector('[data-js="collection-load-status"]'),status:t.querySelector('[data-js="collection-status"]'),error:t.querySelector('[data-js="collection-error"]')}}function w(t,o){t.status&&(t.status.textContent=o)}function y(t,o){t.loadStatus&&(t.loadStatus.textContent=o)}function C(t,o){t.error&&(t.error.textContent=o)}function S(t,o){t.root.setAttribute("aria-busy",String(o))}async function D(t,o){const e=Number(t.dataset.variantId??"");if(!e){o&&window.location.assign(o);return}const c=t.dataset.labelIdle??"Quick buy",l=t.dataset.labelLoading??"Adding...",f=t.dataset.labelSuccess??"Added",d=t.dataset.labelError??"Try again";t.disabled=!0,t.setAttribute("aria-busy","true"),t.textContent=l;try{await v(e,1),j({itemCountDelta:1}),A(),t.textContent=f,window.setTimeout(()=>{t.textContent=c,t.disabled=!1,t.setAttribute("aria-busy","false")},1200)}catch{t.textContent=d,window.setTimeout(()=>{t.textContent=c,t.disabled=!1,t.setAttribute("aria-busy","false")},1500)}}function u(t){return new URL(t,window.location.origin)}function k(t,o){const e=new URL(t.toString());return e.searchParams.set("section_id",o),`${e.pathname}${e.search}`}function I(t){const o=u(t.action||window.location.href),e=new URLSearchParams;return new FormData(t).forEach((l,f)=>{const d=String(l).trim();d.length>0&&e.append(f,d)}),o.search=e.toString(),o}function U(t){return new DOMParser().parseFromString(t,"text/html").querySelector(L)}document.addEventListener("DOMContentLoaded",()=>{const t=document.querySelector(L);if(!t)return;let o=t,e=b(o);if(!e.sectionId)return;let c=0,l=null;const f=a=>{o=a,e=b(a)},d=async(a,n)=>{l?.abort(),l=new AbortController;const i=await fetch(k(a,e.sectionId),{signal:l.signal,headers:{"X-Requested-With":"XMLHttpRequest"}});if(!i.ok)throw new Error("Collection section request failed");const r=await i.text();if(n!==c)throw new DOMException("Stale collection request","AbortError");const s=U(r);if(!s)throw new Error("Collection section parse failed");return s},p=async(a,n)=>{const i=++c;S(e,!0),C(e,""),w(e,"Loading products...");try{const r=await d(a,i);o.replaceWith(r),f(r),n&&window.history.pushState({source:"collection-replace"},"",`${a.pathname}${a.search}`),w(e,"Products updated."),y(e,"")}catch(r){if(r instanceof DOMException&&r.name==="AbortError")return;C(e,"Could not update collection right now. Please try again."),w(e,"Collection update failed.")}finally{i===c&&S(e,!1)}},q=async a=>{const n=u(a),i=++c;S(e,!0),C(e,""),y(e,"Loading more products...");try{const r=await d(n,i),s=b(r);if(!e.products||!s.products||!e.paginationWrap||!s.paginationWrap)throw new Error("Collection append targets missing");const g=document.createDocumentFragment();s.products.querySelectorAll('[data-js="collection-product-card"]').forEach(m=>{g.appendChild(m)}),e.products.appendChild(g),e.paginationWrap.replaceWith(s.paginationWrap),e.paginationWrap=s.paginationWrap,window.history.pushState({source:"collection-append"},"",`${n.pathname}${n.search}`),y(e,"More products loaded."),w(e,"Collection updated.")}catch(r){if(r instanceof DOMException&&r.name==="AbortError")return;C(e,"Could not load more products. Please try again."),y(e,"Load more failed.")}finally{i===c&&S(e,!1)}},x=a=>{const n=a.target,i=n.closest('[data-js="collection-quick-buy"]');if(i){if(a.preventDefault(),i.disabled)return;const h=i.dataset.productUrl??"";D(i,h);return}const r=n.closest('[data-js="collection-load-more"]');if(r){a.preventDefault();const h=r.dataset.nextUrl;if(!h||r.disabled)return;r.disabled=!0,r.setAttribute("aria-busy","true"),q(h).finally(()=>{r.disabled=!1,r.setAttribute("aria-busy","false")});return}const s=n.closest('[data-js="collection-clear"]');if(s){a.preventDefault(),p(u(s.href),!0);return}const g=n.closest('[data-js="collection-filter-remove"]');if(g){a.preventDefault(),p(u(g.href),!0);return}const m=n.closest('[data-js="collection-default-pagination"] a');m&&(a.preventDefault(),p(u(m.href),!0))},E=a=>{!a.target.closest("input, select")||!e.controls||e.controls.requestSubmit()},R=a=>{const n=a.target;n.matches('[data-js="collection-controls"]')&&(a.preventDefault(),p(I(n),!0))};document.addEventListener("click",a=>{o.contains(a.target)&&x(a)}),document.addEventListener("change",a=>{o.contains(a.target)&&E(a)}),document.addEventListener("submit",a=>{o.contains(a.target)&&R(a)}),window.addEventListener("popstate",()=>{p(u(window.location.href),!1)})}); diff --git a/assets/drawer-BOuNB4a_.js b/assets/drawer-BOuNB4a_.js deleted file mode 100644 index 9957d1efa..000000000 --- a/assets/drawer-BOuNB4a_.js +++ /dev/null @@ -1 +0,0 @@ -const M='a[href], button:not([disabled]), textarea, input, select, [tabindex]:not([tabindex="-1"])';function $(t,n=250){let r=null;return(...a)=>{r!==null&&window.clearTimeout(r),r=window.setTimeout(()=>{t(...a)},n)}}function H(t){const n=document.createElement("li"),r=document.createElement("a");if(r.href=t.url,r.className="flex items-center gap-3 rounded border px-2 py-2 hover:bg-gray-50",t.image?.url){const o=document.createElement("img");o.src=t.image.url,o.alt=t.image.alt??t.title,o.className="h-10 w-10 object-cover",r.appendChild(o)}const a=document.createElement("span");return a.className="text-sm",a.textContent=t.title,r.appendChild(a),n.appendChild(r),n}function U(t){const n=document.createElement("section"),r=document.createElement("h3");r.className="mb-2 text-xs font-semibold uppercase tracking-wide opacity-70",r.textContent=t.title;const a=document.createElement("ul");return a.className="space-y-1",t.items.forEach(o=>{a.appendChild(H(o))}),n.appendChild(r),n.appendChild(a),n}function X(){const t=document.querySelector('[data-js="search-drawer"]');if(!t)return;const n=t.querySelector('[data-js="search-drawer-overlay"]'),r=t.querySelector('[data-js="search-drawer-panel"]'),a=t.querySelector('[data-js="search-close"]'),o=t.querySelector('[data-js="search-drawer-form"]'),A=t.querySelector('[data-js="search-drawer-input"]'),j=t.querySelector('[data-js="search-drawer-status"]'),k=t.querySelector('[data-js="search-drawer-error"]'),f=t.querySelector('[data-js="search-drawer-loader"]'),h=t.querySelector('[data-js="search-drawer-empty"]'),m=t.querySelector('[data-js="search-drawer-groups"]');if(!n||!r||!a||!o||!A||!j||!k||!f||!h||!m)return;const y=t,w=A,S=document.querySelectorAll('[data-js="search-open"]');let d=!1,q=null,i=null,b=0;const p=e=>{j.textContent=e},x=e=>{k.textContent=e},E=e=>{r.setAttribute("aria-busy",String(e)),e?(f.classList.remove("hidden"),f.classList.add("flex")):(f.classList.add("hidden"),f.classList.remove("flex"))},D=e=>{w.setAttribute("aria-expanded",String(e))},C=e=>{m.replaceChildren(),h.textContent=e,h.classList.remove("hidden")},F=()=>Array.from(r.querySelectorAll(M)).filter(e=>!e.hasAttribute("disabled")&&!e.getAttribute("aria-hidden")),N=e=>{if(!d)return;if(e.key==="Escape"){e.preventDefault(),g();return}if(e.key!=="Tab")return;const s=F();if(s.length===0)return;const c=s[0],l=s[s.length-1];if(e.shiftKey&&document.activeElement===c){e.preventDefault(),l.focus();return}!e.shiftKey&&document.activeElement===l&&(e.preventDefault(),c.focus())};function I(e){d||(d=!0,q=e??(document.activeElement instanceof HTMLElement?document.activeElement:null),y.hidden=!1,y.setAttribute("aria-hidden","false"),S.forEach(s=>s.setAttribute("aria-expanded","true")),document.body.style.overflow="hidden",D(!0),document.addEventListener("keydown",N),w.focus())}function g(){d&&(d=!1,i?.abort(),i=null,y.hidden=!0,y.setAttribute("aria-hidden","true"),S.forEach(e=>e.setAttribute("aria-expanded","false")),document.body.style.overflow="",D(!1),E(!1),document.removeEventListener("keydown",N),q&&q.focus())}const O=e=>{if(m.replaceChildren(),e.length===0){C("No quick matches. Press Enter for full results."),p("No quick matches.");return}const s=document.createDocumentFragment();e.forEach(l=>{s.appendChild(U(l))}),m.appendChild(s),h.classList.add("hidden");const c=e.reduce((l,u)=>l+u.items.length,0);p(`${c} quick matches`)},B=async e=>{if(!d)return;if(e.length<2){i?.abort(),i=null,x(""),p("Type at least 2 characters."),E(!1),C("Start typing to see quick results.");return}i?.abort(),i=new AbortController;const s=++b;E(!0),x(""),p("Searching...");const c=new URLSearchParams;c.set("q",e),c.set("resources[type]","product,page,article"),c.set("resources[limit]","6"),c.set("resources[options][unavailable_products]","last");const l=`${window.Shopify.routes.root}search/suggest.json?${c.toString()}`;try{const u=await fetch(l,{signal:i.signal,headers:{Accept:"application/json","X-Requested-With":"XMLHttpRequest"}});if(!u.ok)throw new Error("Predictive endpoint unavailable");const L=await u.json();if(s!==b)return;const v=[],P=L.resources?.results?.products??[],R=L.resources?.results?.pages??[],T=L.resources?.results?.articles??[];P.length>0&&v.push({title:"Products",items:P.slice(0,6)}),R.length>0&&v.push({title:"Pages",items:R.slice(0,6)}),T.length>0&&v.push({title:"Articles",items:T.slice(0,6)}),O(v)}catch(u){if(u instanceof DOMException&&u.name==="AbortError"||s!==b)return;C("Quick results unavailable. Press Enter for full results."),x("Predictive search is unavailable. Full search still works."),p("Predictive search unavailable.")}finally{s===b&&E(!1)}},K=$(e=>{B(e.trim())});S.forEach(e=>{e.setAttribute("aria-expanded","false"),e.addEventListener("click",s=>{s.preventDefault(),I(e)})}),a.addEventListener("click",g),n.addEventListener("click",g),w.addEventListener("input",()=>{K(w.value)}),o.addEventListener("submit",()=>{g()})}export{X as initSearchDrawer}; diff --git a/assets/drawer-BY7bJn32.js b/assets/drawer-BY7bJn32.js new file mode 100644 index 000000000..8699353b7 --- /dev/null +++ b/assets/drawer-BY7bJn32.js @@ -0,0 +1 @@ +import{changeCartLine as e}from"./cart-DFJfubsI.js";import{applySectionReplace as t,fetchSingleSectionHtml as n,normalizeSectionsUrl as r}from"./section-rendering-COdqImLj.js";import{CART_OPEN_EVENT as i,CART_UPDATED_EVENT as a}from"./cart-events-DFMQ7aoZ.js";var o=`a[href], button:not([disabled]), textarea, input, select, [tabindex]:not([tabindex="-1"])`;function s(){let s=document.querySelector(`[data-js="cart-drawer"]`);if(!s)return;let c=s.querySelector(`[data-js="cart-drawer-overlay"]`),l=s.querySelector(`[data-js="cart-drawer-panel"]`),u=s.querySelector(`[data-js="cart-close"]`),d=s.querySelector(`[data-js="cart-items"]`),f=s.querySelector(`[data-js="cart-empty"]`),p=s.querySelector(`[data-js="cart-subtotal"]`),m=s.querySelector(`[data-js="cart-drawer-status"]`),h=s.querySelector(`[data-js="cart-drawer-error"]`),g=document.querySelectorAll(`[data-js="cart-count"]`);if(!c||!l||!u||!d||!f||!p||!m||!h)return;let _=s,v=c,y=l,b=u,x=m,S=h,C=r(window.location.pathname),w=document.querySelectorAll(`[data-js="cart-open"]`),T=d,E=f,D=p,O=!1,k=!1,A=0,j=null,M=null,N=e=>{x.textContent=e},P=e=>{S.textContent=e},F=e=>{y.setAttribute(`aria-busy`,String(e)),_.querySelectorAll(`button`).forEach(t=>{t.dataset.js!==`cart-close`&&(t.disabled=e)})},I=e=>{g.forEach(t=>{t.textContent=String(e),t.hidden=e<1})},L=()=>{let e=g[0];return e&&Number(e.textContent??`0`)||0},R=()=>{let e=T.querySelectorAll(`[data-js="cart-qty-value"]`);return Array.from(e).reduce((e,t)=>{let n=Number(t.textContent??`0`);return e+(Number.isFinite(n)?n:0)},0)},z=(e,t)=>{let n=T.querySelector(`[data-line="${e}"]`);if(!n)return;n.classList.toggle(`animate-pulse`,t);let r=n.querySelector(`[data-js="cart-drawer-line-overlay"]`);r&&(t?(r.classList.remove(`hidden`),r.classList.add(`flex`)):(r.classList.add(`hidden`),r.classList.remove(`flex`)))},B=e=>{let n=t(e,`[data-js="cart-drawer"]`,[{key:`items`,current:T,selector:`[data-js="cart-items"]`},{key:`empty`,current:E,selector:`[data-js="cart-empty"]`},{key:`subtotal`,current:D,selector:`[data-js="cart-subtotal"]`}]);if(!n.ok)return!1;let r=n.nodes.items,i=n.nodes.empty,a=n.nodes.subtotal;return!r||!i||!a?!1:(T=r,E=i,D=a,I(R()),!0)},V=e=>B(e),H=async()=>{P(``);try{if(!B(await n(`cart-drawer`,C)))throw Error(`Drawer section rendering failed`)}catch{P(`Could not load your cart right now. Please try again.`),N(`Cart load failed.`)}},U=async(t,n)=>{let r=++A;k=!0,P(``),F(!0),z(t,!0);try{let i=await e(t,n,{sections:[`cart-drawer`],sectionsUrl:C});if(!V(i.sections?.[`cart-drawer`])||r!==A)throw Error(`Drawer section rendering failed`);I(i.item_count),N(n===0?`Item removed.`:`Cart updated.`)}catch{P(`Could not refresh cart UI. Please try again.`),N(`Cart update failed.`)}finally{r===A&&(k=!1,F(!1),z(t,!1),j&&W())}},W=async()=>{if(!k)for(;j;){let e=j;j=null,await U(e.line,e.quantity)}},G=(e,t)=>{j={line:e,quantity:t},W()},K=()=>Array.from(y.querySelectorAll(o)).filter(e=>!e.hasAttribute(`disabled`)&&!e.getAttribute(`aria-hidden`)),q=e=>{if(!O)return;if(e.key===`Escape`){e.preventDefault(),Y();return}if(e.key!==`Tab`)return;let t=K();if(t.length===0)return;let n=t[0],r=t[t.length-1];if(e.shiftKey&&document.activeElement===n){e.preventDefault(),r.focus();return}!e.shiftKey&&document.activeElement===r&&(e.preventDefault(),n.focus())};function J(e){O||(O=!0,M=e??(document.activeElement instanceof HTMLElement?document.activeElement:null),_.hidden=!1,_.setAttribute(`aria-hidden`,`false`),w.forEach(e=>e.setAttribute(`aria-expanded`,`true`)),document.body.style.overflow=`hidden`,document.addEventListener(`keydown`,q),b.focus(),H())}function Y(){O&&(O=!1,_.hidden=!0,_.setAttribute(`aria-hidden`,`true`),w.forEach(e=>e.setAttribute(`aria-expanded`,`false`)),document.body.style.overflow=``,document.removeEventListener(`keydown`,q),M&&M.focus())}w.forEach(e=>{e.addEventListener(`click`,t=>{t.preventDefault(),J(e)}),e.setAttribute(`aria-expanded`,`false`)}),b.addEventListener(`click`,Y),v.addEventListener(`click`,Y),window.addEventListener(i,()=>{J()}),window.addEventListener(a,e=>{let t=e.detail;typeof t?.itemCount==`number`?I(t.itemCount):typeof t?.itemCountDelta==`number`&&I(Math.max(0,L()+t.itemCountDelta)),t?.openDrawer&&J()}),_.addEventListener(`click`,e=>{let t=e.target.closest(`[data-js="cart-qty-dec"], [data-js="cart-qty-inc"], [data-js="cart-remove"]`);if(!t)return;let n=Number(t.dataset.line);if(!n)return;let r=T.querySelector(`[data-line="${n}"]`)?.querySelector(`[data-js="cart-qty-value"]`),i=Number(r?.textContent??`1`);if(t.dataset.js===`cart-remove`){G(n,0);return}if(t.dataset.js===`cart-qty-inc`){G(n,i+1);return}t.dataset.js===`cart-qty-dec`&&G(n,Math.max(0,i-1))})}export{s as initCartDrawer}; \ No newline at end of file diff --git a/assets/drawer-CJwC366z.js b/assets/drawer-CJwC366z.js deleted file mode 100644 index b05e3427a..000000000 --- a/assets/drawer-CJwC366z.js +++ /dev/null @@ -1 +0,0 @@ -import{c as W}from"./cart-Kw4e-iq4.js";import{C as X,b as Y}from"./cart-events-C4m0PwR1.js";import{n as Z,f as tt,a as et}from"./section-rendering-vVqNxfcB.js";const rt='a[href], button:not([disabled]), textarea, input, select, [tabindex]:not([tabindex="-1"])';function st(){const n=document.querySelector('[data-js="cart-drawer"]');if(!n)return;const b=n.querySelector('[data-js="cart-drawer-overlay"]'),C=n.querySelector('[data-js="cart-drawer-panel"]'),v=n.querySelector('[data-js="cart-close"]'),q=n.querySelector('[data-js="cart-items"]'),j=n.querySelector('[data-js="cart-empty"]'),x=n.querySelector('[data-js="cart-subtotal"]'),L=n.querySelector('[data-js="cart-drawer-status"]'),A=n.querySelector('[data-js="cart-drawer-error"]'),g=document.querySelectorAll('[data-js="cart-count"]');if(!b||!C||!v||!q||!j||!x||!L||!A)return;const o=n,O=b,D=C,U=v,P=L,H=A,k=Z(window.location.pathname),f=document.querySelectorAll('[data-js="cart-open"]');let s=q,R=j,N=x,c=!1,m=!1,y=0,i=null,p=null;const w=t=>{P.textContent=t},d=t=>{H.textContent=t},T=t=>{D.setAttribute("aria-busy",String(t)),o.querySelectorAll("button").forEach(e=>{e.dataset.js==="cart-close"||(e.disabled=t)})},l=t=>{g.forEach(e=>{e.textContent=String(t),e.hidden=t<1})},K=()=>{const t=g[0];return t&&Number(t.textContent??"0")||0},Q=()=>{const t=s.querySelectorAll('[data-js="cart-qty-value"]');return Array.from(t).reduce((e,r)=>{const a=Number(r.textContent??"0");return e+(Number.isFinite(a)?a:0)},0)},B=(t,e)=>{const r=s.querySelector(`[data-line="${t}"]`);if(!r)return;r.classList.toggle("animate-pulse",e);const a=r.querySelector('[data-js="cart-drawer-line-overlay"]');a&&(e?(a.classList.remove("hidden"),a.classList.add("flex")):(a.classList.add("hidden"),a.classList.remove("flex")))},I=t=>{const e=et(t,'[data-js="cart-drawer"]',[{key:"items",current:s,selector:'[data-js="cart-items"]'},{key:"empty",current:R,selector:'[data-js="cart-empty"]'},{key:"subtotal",current:N,selector:'[data-js="cart-subtotal"]'}]);if(!e.ok)return!1;const r=e.nodes.items,a=e.nodes.empty,u=e.nodes.subtotal;return!r||!a||!u?!1:(s=r,R=a,N=u,l(Q()),!0)},V=t=>I(t),$=async()=>{d("");try{const t=await tt("cart-drawer",k);if(!I(t))throw new Error("Drawer section rendering failed")}catch{d("Could not load your cart right now. Please try again."),w("Cart load failed.")}},z=async(t,e)=>{const r=++y;m=!0,d(""),T(!0),B(t,!0);try{const a=await W(t,e,{sections:["cart-drawer"],sectionsUrl:k});if(!V(a.sections?.["cart-drawer"])||r!==y)throw new Error("Drawer section rendering failed");l(a.item_count),w(e===0?"Item removed.":"Cart updated.")}catch{d("Could not refresh cart UI. Please try again."),w("Cart update failed.")}finally{r===y&&(m=!1,T(!1),B(t,!1),i&&M())}},M=async()=>{if(!m)for(;i;){const t=i;i=null,await z(t.line,t.quantity)}},h=(t,e)=>{i={line:t,quantity:e},M()},G=()=>Array.from(D.querySelectorAll(rt)).filter(t=>!t.hasAttribute("disabled")&&!t.getAttribute("aria-hidden")),_=t=>{if(!c)return;if(t.key==="Escape"){t.preventDefault(),S();return}if(t.key!=="Tab")return;const e=G();if(e.length===0)return;const r=e[0],a=e[e.length-1];if(t.shiftKey&&document.activeElement===r){t.preventDefault(),a.focus();return}!t.shiftKey&&document.activeElement===a&&(t.preventDefault(),r.focus())};function E(t){c||(c=!0,p=t??(document.activeElement instanceof HTMLElement?document.activeElement:null),o.hidden=!1,o.setAttribute("aria-hidden","false"),f.forEach(e=>e.setAttribute("aria-expanded","true")),document.body.style.overflow="hidden",document.addEventListener("keydown",_),U.focus(),$())}function S(){c&&(c=!1,o.hidden=!0,o.setAttribute("aria-hidden","true"),f.forEach(t=>t.setAttribute("aria-expanded","false")),document.body.style.overflow="",document.removeEventListener("keydown",_),p&&p.focus())}f.forEach(t=>{t.addEventListener("click",e=>{e.preventDefault(),E(t)}),t.setAttribute("aria-expanded","false")}),U.addEventListener("click",S),O.addEventListener("click",S),window.addEventListener(X,()=>{E()}),window.addEventListener(Y,t=>{const r=t.detail;typeof r?.itemCount=="number"?l(r.itemCount):typeof r?.itemCountDelta=="number"&&l(Math.max(0,K()+r.itemCountDelta)),r?.openDrawer&&E()}),o.addEventListener("click",t=>{const r=t.target.closest('[data-js="cart-qty-dec"], [data-js="cart-qty-inc"], [data-js="cart-remove"]');if(!r)return;const a=Number(r.dataset.line);if(!a)return;const J=s.querySelector(`[data-line="${a}"]`)?.querySelector('[data-js="cart-qty-value"]'),F=Number(J?.textContent??"1");if(r.dataset.js==="cart-remove"){h(a,0);return}if(r.dataset.js==="cart-qty-inc"){h(a,F+1);return}r.dataset.js==="cart-qty-dec"&&h(a,Math.max(0,F-1))})}export{st as initCartDrawer}; diff --git a/assets/drawer-DwueD-Lo.js b/assets/drawer-DwueD-Lo.js new file mode 100644 index 000000000..a8f72928a --- /dev/null +++ b/assets/drawer-DwueD-Lo.js @@ -0,0 +1 @@ +var e=`a[href], button:not([disabled]), textarea, input, select, [tabindex]:not([tabindex="-1"])`;function t(e,t=250){let n=null;return(...r)=>{n!==null&&window.clearTimeout(n),n=window.setTimeout(()=>{e(...r)},t)}}function n(e){let t=document.createElement(`li`),n=document.createElement(`a`);if(n.href=e.url,n.className=`flex items-center gap-3 rounded border px-2 py-2 hover:bg-gray-50`,e.image?.url){let t=document.createElement(`img`);t.src=e.image.url,t.alt=e.image.alt??e.title,t.className=`h-10 w-10 object-cover`,n.appendChild(t)}let r=document.createElement(`span`);return r.className=`text-sm`,r.textContent=e.title,n.appendChild(r),t.appendChild(n),t}function r(e){let t=document.createElement(`section`),r=document.createElement(`h3`);r.className=`mb-2 text-xs font-semibold uppercase tracking-wide opacity-70`,r.textContent=e.title;let i=document.createElement(`ul`);return i.className=`space-y-1`,e.items.forEach(e=>{i.appendChild(n(e))}),t.appendChild(r),t.appendChild(i),t}function i(){let n=document.querySelector(`[data-js="search-drawer"]`);if(!n)return;let i=n.querySelector(`[data-js="search-drawer-overlay"]`),a=n.querySelector(`[data-js="search-drawer-panel"]`),o=n.querySelector(`[data-js="search-close"]`),s=n.querySelector(`[data-js="search-drawer-form"]`),c=n.querySelector(`[data-js="search-drawer-input"]`),l=n.querySelector(`[data-js="search-drawer-status"]`),u=n.querySelector(`[data-js="search-drawer-error"]`),d=n.querySelector(`[data-js="search-drawer-loader"]`),f=n.querySelector(`[data-js="search-drawer-empty"]`),p=n.querySelector(`[data-js="search-drawer-groups"]`);if(!i||!a||!o||!s||!c||!l||!u||!d||!f||!p)return;let m=n,h=c,g=document.querySelectorAll(`[data-js="search-open"]`),_=!1,v=null,y=null,b=0,x=e=>{l.textContent=e},S=e=>{u.textContent=e},C=e=>{a.setAttribute(`aria-busy`,String(e)),e?(d.classList.remove(`hidden`),d.classList.add(`flex`)):(d.classList.add(`hidden`),d.classList.remove(`flex`))},w=e=>{h.setAttribute(`aria-expanded`,String(e))},T=e=>{p.replaceChildren(),f.textContent=e,f.classList.remove(`hidden`)},E=()=>Array.from(a.querySelectorAll(e)).filter(e=>!e.hasAttribute(`disabled`)&&!e.getAttribute(`aria-hidden`)),D=e=>{if(!_)return;if(e.key===`Escape`){e.preventDefault(),k();return}if(e.key!==`Tab`)return;let t=E();if(t.length===0)return;let n=t[0],r=t[t.length-1];if(e.shiftKey&&document.activeElement===n){e.preventDefault(),r.focus();return}!e.shiftKey&&document.activeElement===r&&(e.preventDefault(),n.focus())};function O(e){_||(_=!0,v=e??(document.activeElement instanceof HTMLElement?document.activeElement:null),m.hidden=!1,m.setAttribute(`aria-hidden`,`false`),g.forEach(e=>e.setAttribute(`aria-expanded`,`true`)),document.body.style.overflow=`hidden`,w(!0),document.addEventListener(`keydown`,D),h.focus())}function k(){_&&(_=!1,y?.abort(),y=null,m.hidden=!0,m.setAttribute(`aria-hidden`,`true`),g.forEach(e=>e.setAttribute(`aria-expanded`,`false`)),document.body.style.overflow=``,w(!1),C(!1),document.removeEventListener(`keydown`,D),v&&v.focus())}let A=e=>{if(p.replaceChildren(),e.length===0){T(`No quick matches. Press Enter for full results.`),x(`No quick matches.`);return}let t=document.createDocumentFragment();e.forEach(e=>{t.appendChild(r(e))}),p.appendChild(t),f.classList.add(`hidden`),x(`${e.reduce((e,t)=>e+t.items.length,0)} quick matches`)},j=async e=>{if(!_)return;if(e.length<2){y?.abort(),y=null,S(``),x(`Type at least 2 characters.`),C(!1),T(`Start typing to see quick results.`);return}y?.abort(),y=new AbortController;let t=++b;C(!0),S(``),x(`Searching...`);let n=new URLSearchParams;n.set(`q`,e),n.set(`resources[type]`,`product,page,article`),n.set(`resources[limit]`,`6`),n.set(`resources[options][unavailable_products]`,`last`);let r=`${window.Shopify.routes.root}search/suggest.json?${n.toString()}`;try{let e=await fetch(r,{signal:y.signal,headers:{Accept:`application/json`,"X-Requested-With":`XMLHttpRequest`}});if(!e.ok)throw Error(`Predictive endpoint unavailable`);let n=await e.json();if(t!==b)return;let i=[],a=n.resources?.results?.products??[],o=n.resources?.results?.pages??[],s=n.resources?.results?.articles??[];a.length>0&&i.push({title:`Products`,items:a.slice(0,6)}),o.length>0&&i.push({title:`Pages`,items:o.slice(0,6)}),s.length>0&&i.push({title:`Articles`,items:s.slice(0,6)}),A(i)}catch(e){if(e instanceof DOMException&&e.name===`AbortError`||t!==b)return;T(`Quick results unavailable. Press Enter for full results.`),S(`Predictive search is unavailable. Full search still works.`),x(`Predictive search unavailable.`)}finally{t===b&&C(!1)}},M=t(e=>{j(e.trim())});g.forEach(e=>{e.setAttribute(`aria-expanded`,`false`),e.addEventListener(`click`,t=>{t.preventDefault(),O(e)})}),o.addEventListener(`click`,k),i.addEventListener(`click`,k),h.addEventListener(`input`,()=>{M(h.value)}),s.addEventListener(`submit`,()=>{k()})}export{i as initSearchDrawer}; \ No newline at end of file diff --git a/assets/gift-card-Ciee4aDH.js b/assets/gift-card-Ciee4aDH.js new file mode 100644 index 000000000..e69de29bb diff --git a/assets/gift-card-l0sNRNKZ.js b/assets/gift-card-l0sNRNKZ.js deleted file mode 100644 index 8b1378917..000000000 --- a/assets/gift-card-l0sNRNKZ.js +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/handlers-B949bjPl.js b/assets/handlers-B949bjPl.js deleted file mode 100644 index 719716893..000000000 --- a/assets/handlers-B949bjPl.js +++ /dev/null @@ -1 +0,0 @@ -import{f as u}from"./variant-picker-DRrQ71gT.js";import{a as m}from"./cart-Kw4e-iq4.js";import{e as l,a as p}from"./cart-events-C4m0PwR1.js";import{s as t}from"./state-Bwp_aMHz.js";import{s as i}from"./sync-BRYiu7LQ.js";function V(n){const e=n.target.closest('[data-js="option-value"]');if(!e||e.disabled||e.getAttribute("aria-disabled")==="true")return;const a=Number(e.dataset.optionPosition)-1,r=e.dataset.optionValue??"";if(a<0||!r)return;t.selectedOptions[a]=r;const o=u(t.productData.variants,t.selectedOptions);if(!o){t.selectedOptions[a]=t.currentVariant.options[a]??t.selectedOptions[a];return}const d=t.currentMediaId,c=t.mediaContextVariantId;t.currentVariant=o,o.featured_media?(t.currentMediaId=o.featured_media.id,t.mediaContextVariantId=o.id):(t.currentMediaId=d,t.mediaContextVariantId=c);const s=new URL(window.location.href);s.searchParams.set("variant",String(o.id)),history.replaceState({},"",`${s.pathname}${s.search}${s.hash}`),i()}function v(n){const e=n.target.closest('[data-js="thumbnail"]');if(!e)return;const a=Number(e.dataset.thumbnail);if(!a)return;t.currentMediaId=a;const r=Number((e.dataset.variantMedia??"").split(",")[0]);r&&(t.mediaContextVariantId=r),i()}async function y(n){if(n.preventDefault(),t.cartState==="loading")return;const a=n.target.querySelector('input[name="quantity"]'),r=a?Math.max(1,Number(a.value)||1):1;t.cartState="loading",i();try{await m(t.currentVariant.id,r),l({itemCountDelta:r}),p(),t.cartState="success",i(),setTimeout(()=>{t.cartState="idle",i()},2e3)}catch{t.cartState="error",i(),setTimeout(()=>{t.cartState="idle",i()},3e3)}}export{y as a,v as b,V as o}; diff --git a/assets/handlers-CMuQJ9_L.js b/assets/handlers-CMuQJ9_L.js new file mode 100644 index 000000000..f9abdda16 --- /dev/null +++ b/assets/handlers-CMuQJ9_L.js @@ -0,0 +1 @@ +import{addToCart as e}from"./cart-DFJfubsI.js";import{emitCartOpen as t,emitCartUpdated as n}from"./cart-events-DFMQ7aoZ.js";import{state as r}from"./state-yJSoUqNP.js";import{findVariantByOptions as i}from"./variant-picker-BcihOqVL.js";import{syncDOM as a}from"./sync-CuhjKoMp.js";function o(e){let t=e.target.closest(`[data-js="option-value"]`);if(!t||t.disabled||t.getAttribute(`aria-disabled`)===`true`)return;let n=Number(t.dataset.optionPosition)-1,o=t.dataset.optionValue??``;if(n<0||!o)return;r.selectedOptions[n]=o;let s=i(r.productData.variants,r.selectedOptions);if(!s){r.selectedOptions[n]=r.currentVariant.options[n]??r.selectedOptions[n];return}let c=r.currentMediaId,l=r.mediaContextVariantId;r.currentVariant=s,s.featured_media?(r.currentMediaId=s.featured_media.id,r.mediaContextVariantId=s.id):(r.currentMediaId=c,r.mediaContextVariantId=l);let u=new URL(window.location.href);u.searchParams.set(`variant`,String(s.id)),history.replaceState({},``,`${u.pathname}${u.search}${u.hash}`),a()}function s(e){let t=e.target.closest(`[data-js="thumbnail"]`);if(!t)return;let n=Number(t.dataset.thumbnail);if(!n)return;r.currentMediaId=n;let i=Number((t.dataset.variantMedia??``).split(`,`)[0]);i&&(r.mediaContextVariantId=i),a()}async function c(i){if(i.preventDefault(),r.cartState===`loading`)return;let o=i.target.querySelector(`input[name="quantity"]`),s=o?Math.max(1,Number(o.value)||1):1;r.cartState=`loading`,a();try{await e(r.currentVariant.id,s),n({itemCountDelta:s}),t(),r.cartState=`success`,a(),setTimeout(()=>{r.cartState=`idle`,a()},2e3)}catch{r.cartState=`error`,a(),setTimeout(()=>{r.cartState=`idle`,a()},3e3)}}export{c as onAddToCart,o as onOptionClick,s as onThumbnailClick}; \ No newline at end of file diff --git a/assets/index-D_EjmhRG.js b/assets/index-D_EjmhRG.js new file mode 100644 index 000000000..e69de29bb diff --git a/assets/index-l0sNRNKZ.js b/assets/index-l0sNRNKZ.js deleted file mode 100644 index 8b1378917..000000000 --- a/assets/index-l0sNRNKZ.js +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/list-collections-87bPu3lg.js b/assets/list-collections-87bPu3lg.js new file mode 100644 index 000000000..e69de29bb diff --git a/assets/list-collections-l0sNRNKZ.js b/assets/list-collections-l0sNRNKZ.js deleted file mode 100644 index 8b1378917..000000000 --- a/assets/list-collections-l0sNRNKZ.js +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/main-DNSsntQO.css b/assets/main-DNSsntQO.css deleted file mode 100644 index fb3883c25..000000000 --- a/assets/main-DNSsntQO.css +++ /dev/null @@ -1 +0,0 @@ -@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-border-style:solid;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-duration:initial;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-500:oklch(63.7% .237 25.331);--color-red-700:oklch(50.5% .213 27.518);--color-yellow-500:oklch(79.5% .184 86.047);--color-green-500:oklch(72.3% .219 149.579);--color-blue-50:oklch(97% .014 254.604);--color-blue-400:oklch(70.7% .165 254.624);--color-blue-500:oklch(62.3% .214 259.815);--color-blue-600:oklch(54.6% .245 262.881);--color-blue-700:oklch(48.8% .243 264.376);--color-gray-50:oklch(98.5% .002 247.839);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-300:oklch(87.2% .01 258.338);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-700:oklch(37.3% .034 259.733);--color-gray-800:oklch(27.8% .033 256.848);--color-gray-900:oklch(21% .034 264.665);--color-neutral-100:oklch(97% 0 0);--color-neutral-900:oklch(20.5% 0 0);--color-black:#000;--color-white:#fff;--spacing:.25rem;--breakpoint-sm:40rem;--breakpoint-md:48rem;--breakpoint-lg:64rem;--breakpoint-xl:80rem;--container-md:28rem;--container-lg:32rem;--container-xl:36rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height: 1.5 ;--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-3xl:1.875rem;--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5 / 2.25);--font-weight-thin:100;--font-weight-extralight:200;--font-weight-light:300;--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--font-weight-extrabold:800;--font-weight-black:900;--tracking-wide:.025em;--radius-sm:.25rem;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--shadow-sm:0 1px 3px 0 #0000001a, 0 1px 2px -1px #0000001a;--shadow-md:0 4px 6px -1px #0000001a, 0 2px 4px -2px #0000001a;--animate-spin:spin 1s linear infinite;--animate-pulse:pulse 2s cubic-bezier(.4, 0, .6, 1) infinite;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.pointer-events-none{pointer-events:none}.invisible{visibility:hidden}.visible{visibility:visible}.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.inset-0{inset:calc(var(--spacing) * 0)}.end{inset-inline-end:var(--spacing)}.top-0{top:calc(var(--spacing) * 0)}.top-\[117px\]{top:117px}.right-0{right:calc(var(--spacing) * 0)}.bottom-0{bottom:calc(var(--spacing) * 0)}.left-0{left:calc(var(--spacing) * 0)}.z-10{z-index:10}.z-20{z-index:20}.z-50{z-index:50}.container{width:100%}@media(min-width:40rem){.container{max-width:40rem}}@media(min-width:48rem){.container{max-width:48rem}}@media(min-width:64rem){.container{max-width:64rem}}@media(min-width:80rem){.container{max-width:80rem}}@media(min-width:96rem){.container{max-width:96rem}}.mx-2{margin-inline:calc(var(--spacing) * 2)}.mx-5{margin-inline:calc(var(--spacing) * 5)}.mt-1{margin-top:calc(var(--spacing) * 1)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-8{margin-bottom:calc(var(--spacing) * 8)}.ml-auto{margin-left:auto}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.scrollbar-hidden::-webkit-scrollbar{display:none}.aspect-square{aspect-ratio:1}.h-4{height:calc(var(--spacing) * 4)}.h-5{height:calc(var(--spacing) * 5)}.h-8{height:calc(var(--spacing) * 8)}.h-9{height:calc(var(--spacing) * 9)}.h-10{height:calc(var(--spacing) * 10)}.h-16{height:calc(var(--spacing) * 16)}.h-20{height:calc(var(--spacing) * 20)}.h-full{height:100%}.max-h-\[calc\(100vh-240px\)\]{max-height:calc(100vh - 240px)}.min-h-5{min-height:calc(var(--spacing) * 5)}.min-h-\[240px\]{min-height:240px}.min-h-svh{min-height:100svh}.w-4{width:calc(var(--spacing) * 4)}.w-5{width:calc(var(--spacing) * 5)}.w-8{width:calc(var(--spacing) * 8)}.w-9{width:calc(var(--spacing) * 9)}.w-10{width:calc(var(--spacing) * 10)}.w-16{width:calc(var(--spacing) * 16)}.w-20{width:calc(var(--spacing) * 20)}.w-\[calc\(100\%-2rem\)\]{width:calc(100% - 2rem)}.w-full{width:100%}.max-w-lg{max-width:var(--container-lg)}.max-w-md{max-width:var(--container-md)}.max-w-xl{max-width:var(--container-xl)}.min-w-6{min-width:calc(var(--spacing) * 6)}.flex-1{flex:1}.shrink-0{flex-shrink:0}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.animate-pulse{animation:var(--animate-pulse)}.animate-spin{animation:var(--animate-spin)}.cursor-pointer{cursor:pointer}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-\[64px_1fr\]{grid-template-columns:64px 1fr}.grid-cols-\[80px_1fr\]{grid-template-columns:80px 1fr}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-end{align-items:flex-end}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-1{gap:calc(var(--spacing) * 1)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 5) * calc(1 - var(--tw-space-y-reverse)))}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-xl{border-radius:var(--radius-xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-black\/20{border-color:#0003}@supports (color:color-mix(in lab,red,red)){.border-black\/20{border-color:color-mix(in oklab,var(--color-black) 20%,transparent)}}.border-gray-300{border-color:var(--color-gray-300)}.border-t-black{border-top-color:var(--color-black)}.bg-\[\#FF5A00\]{background-color:#ff5a00}.bg-black\/40{background-color:#0006}@supports (color:color-mix(in lab,red,red)){.bg-black\/40{background-color:color-mix(in oklab,var(--color-black) 40%,transparent)}}.bg-blue-400{background-color:var(--color-blue-400)}.bg-blue-500{background-color:var(--color-blue-500)}.bg-blue-600{background-color:var(--color-blue-600)}.bg-gray-800{background-color:var(--color-gray-800)}.bg-white{background-color:var(--color-white)}.bg-white\/60{background-color:#fff9}@supports (color:color-mix(in lab,red,red)){.bg-white\/60{background-color:color-mix(in oklab,var(--color-white) 60%,transparent)}}.\[mask-type\:luminance\]{mask-type:luminance}.fill-\(--brand-color\){fill:var(--brand-color)}.object-cover{object-fit:cover}.p-2{padding:calc(var(--spacing) * 2)}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.p-6{padding:calc(var(--spacing) * 6)}.p-8{padding:calc(var(--spacing) * 8)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.py-1{padding-block:calc(var(--spacing) * 1)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-8{padding-block:calc(var(--spacing) * 8)}.pb-3{padding-bottom:calc(var(--spacing) * 3)}.pb-4{padding-bottom:calc(var(--spacing) * 4)}.text-center{text-align:center}.text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.text-\(color\:--brand-text\){color:var(--brand-text)}.text-blue-500{color:var(--color-blue-500)}.text-gray-300{color:var(--color-gray-300)}.text-gray-500{color:var(--color-gray-500)}.text-gray-700{color:var(--color-gray-700)}.text-gray-800{color:var(--color-gray-800)}.text-gray-900{color:var(--color-gray-900)}.text-green-500{color:var(--color-green-500)}.text-red-500{color:var(--color-red-500)}.text-red-700{color:var(--color-red-700)}.text-white{color:var(--color-white)}.text-yellow-500{color:var(--color-yellow-500)}.capitalize{text-transform:capitalize}.uppercase{text-transform:uppercase}.underline{text-decoration-line:underline}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.opacity-100{opacity:1}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-2xl{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a), 0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a), 0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-300{--tw-duration:.3s;transition-duration:.3s}.forced-color-adjust-auto{forced-color-adjust:auto}.forced-color-adjust-none{forced-color-adjust:none}@media(hover:hover){.group-hover\:text-blue-600:is(:where(.group):hover *){color:var(--color-blue-600)}.group-hover\:text-gray-700:is(:where(.group):hover *){color:var(--color-gray-700)}}.peer-checked\:block:is(:where(.peer):checked~*){display:block}.first\:pt-0:first-child{padding-top:calc(var(--spacing) * 0)}.last\:pb-0:last-child{padding-bottom:calc(var(--spacing) * 0)}.odd\:bg-gray-50:nth-child(odd){background-color:var(--color-gray-50)}.even\:bg-white:nth-child(2n){background-color:var(--color-white)}.focus-within\:ring-2:focus-within{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}@media(hover:hover){.hover\:-translate-y-1:hover{--tw-translate-y:calc(var(--spacing) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.hover\:bg-blue-50:hover{background-color:var(--color-blue-50)}.hover\:bg-blue-600:hover{background-color:var(--color-blue-600)}.hover\:bg-blue-700:hover{background-color:var(--color-blue-700)}.hover\:bg-gray-50:hover{background-color:var(--color-gray-50)}.hover\:shadow-lg:hover{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}}.focus\:not-sr-only:focus{clip-path:none;white-space:normal;width:auto;height:auto;margin:0;padding:0;position:static;overflow:visible}.focus\:absolute:focus{position:absolute}.focus\:top-4:focus{top:calc(var(--spacing) * 4)}.focus\:left-4:focus{left:calc(var(--spacing) * 4)}.focus\:z-50:focus{z-index:50}.focus\:rounded:focus{border-radius:.25rem}.focus\:border-blue-500:focus{border-color:var(--color-blue-500)}.focus\:bg-white:focus{background-color:var(--color-white)}.focus\:px-4:focus{padding-inline:calc(var(--spacing) * 4)}.focus\:py-2:focus{padding-block:calc(var(--spacing) * 2)}.focus\:ring-1:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-blue-500:focus{--tw-ring-color:var(--color-blue-500)}.focus\:ring-offset-2:focus{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.focus-visible\:ring-2:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus-visible\:ring-blue-500:focus-visible{--tw-ring-color:var(--color-blue-500)}.focus-visible\:ring-offset-2:focus-visible{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.active\:bg-blue-700:active{background-color:var(--color-blue-700)}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}@media(prefers-reduced-motion:reduce){.motion-reduce\:transform-none{transform:none}.motion-reduce\:transition-none{transition-property:none}}@media(min-width:40rem){.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(min-width:48rem){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:p-8{padding:calc(var(--spacing) * 8)}.md\:px-8{padding-inline:calc(var(--spacing) * 8)}@media(hover:hover){.md\:hover\:px-10:hover{padding-inline:calc(var(--spacing) * 10)}}}@media(min-width:64rem){.lg\:w-16{width:calc(var(--spacing) * 16)}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:flex-col{flex-direction:column}.lg\:flex-row-reverse{flex-direction:row-reverse}.lg\:p-12{padding:calc(var(--spacing) * 12)}}@media(min-width:80rem){.xl\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}}@media(prefers-color-scheme:dark){.dark\:bg-blue-400{background-color:var(--color-blue-400)}.dark\:bg-blue-500{background-color:var(--color-blue-500)}.dark\:bg-gray-900{background-color:var(--color-gray-900)}.dark\:text-gray-100{color:var(--color-gray-100)}.dark\:opacity-90{opacity:.9}.dark\:shadow-none{--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark\:ring-1{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark\:ring-white\/10{--tw-ring-color:#ffffff1a}@supports (color:color-mix(in lab,red,red)){.dark\:ring-white\/10{--tw-ring-color:color-mix(in oklab, var(--color-white) 10%, transparent)}}@media(hover:hover){.dark\:hover\:bg-blue-400:hover{background-color:var(--color-blue-400)}.dark\:hover\:bg-blue-500:hover{background-color:var(--color-blue-500)}}}}html{overscroll-behavior:none}@media(min-width:768px){html{scrollbar-gutter:stable;-webkit-scroll-gutter:stable}}body{font-smooth:antialiased;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;overscroll-behavior:none}img,svg,video{pointer-events:none;-webkit-user-select:none;user-select:none}dialog{max-width:none;max-height:none}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@keyframes spin{to{transform:rotate(360deg)}}@keyframes pulse{50%{opacity:.5}} diff --git a/assets/main-JmcnsjPw.css b/assets/main-JmcnsjPw.css new file mode 100644 index 000000000..6ca57b4a2 --- /dev/null +++ b/assets/main-JmcnsjPw.css @@ -0,0 +1,2 @@ +/*! tailwindcss v4.2.2 | MIT License | https://tailwindcss.com */ +@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-border-style:solid;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-duration:initial;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-500:oklch(63.7% .237 25.331);--color-red-700:oklch(50.5% .213 27.518);--color-yellow-500:oklch(79.5% .184 86.047);--color-green-500:oklch(72.3% .219 149.579);--color-blue-50:oklch(97% .014 254.604);--color-blue-400:oklch(70.7% .165 254.624);--color-blue-500:oklch(62.3% .214 259.815);--color-blue-600:oklch(54.6% .245 262.881);--color-blue-700:oklch(48.8% .243 264.376);--color-gray-50:oklch(98.5% .002 247.839);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-300:oklch(87.2% .01 258.338);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-700:oklch(37.3% .034 259.733);--color-gray-800:oklch(27.8% .033 256.848);--color-gray-900:oklch(21% .034 264.665);--color-neutral-100:oklch(97% 0 0);--color-neutral-900:oklch(20.5% 0 0);--color-black:#000;--color-white:#fff;--spacing:.25rem;--breakpoint-sm:40rem;--breakpoint-md:48rem;--breakpoint-lg:64rem;--breakpoint-xl:80rem;--container-md:28rem;--container-lg:32rem;--container-xl:36rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-3xl:1.875rem;--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5 / 2.25);--font-weight-thin:100;--font-weight-extralight:200;--font-weight-light:300;--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--font-weight-extrabold:800;--font-weight-black:900;--tracking-wide:.025em;--radius-sm:.25rem;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--shadow-sm:0 1px 3px 0 #0000001a, 0 1px 2px -1px #0000001a;--shadow-md:0 4px 6px -1px #0000001a, 0 2px 4px -2px #0000001a;--animate-spin:spin 1s linear infinite;--animate-pulse:pulse 2s cubic-bezier(.4, 0, .6, 1) infinite;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.pointer-events-none{pointer-events:none}.invisible{visibility:hidden}.visible{visibility:visible}.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.inset-0{inset:calc(var(--spacing) * 0)}.end{inset-inline-end:var(--spacing)}.top-0{top:calc(var(--spacing) * 0)}.top-\[117px\]{top:117px}.right-0{right:calc(var(--spacing) * 0)}.bottom-0{bottom:calc(var(--spacing) * 0)}.left-0{left:calc(var(--spacing) * 0)}.z-10{z-index:10}.z-20{z-index:20}.z-50{z-index:50}.container{width:100%}@media (width>=40rem){.container{max-width:40rem}}@media (width>=48rem){.container{max-width:48rem}}@media (width>=64rem){.container{max-width:64rem}}@media (width>=80rem){.container{max-width:80rem}}@media (width>=96rem){.container{max-width:96rem}}.mx-2{margin-inline:calc(var(--spacing) * 2)}.mx-5{margin-inline:calc(var(--spacing) * 5)}.mt-1{margin-top:calc(var(--spacing) * 1)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-8{margin-bottom:calc(var(--spacing) * 8)}.ml-auto{margin-left:auto}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.scrollbar-hidden::-webkit-scrollbar{display:none}.aspect-square{aspect-ratio:1}.h-4{height:calc(var(--spacing) * 4)}.h-5{height:calc(var(--spacing) * 5)}.h-8{height:calc(var(--spacing) * 8)}.h-9{height:calc(var(--spacing) * 9)}.h-10{height:calc(var(--spacing) * 10)}.h-16{height:calc(var(--spacing) * 16)}.h-20{height:calc(var(--spacing) * 20)}.h-full{height:100%}.max-h-\[calc\(100vh-240px\)\]{max-height:calc(100vh - 240px)}.min-h-5{min-height:calc(var(--spacing) * 5)}.min-h-\[240px\]{min-height:240px}.min-h-svh{min-height:100svh}.w-4{width:calc(var(--spacing) * 4)}.w-5{width:calc(var(--spacing) * 5)}.w-8{width:calc(var(--spacing) * 8)}.w-9{width:calc(var(--spacing) * 9)}.w-10{width:calc(var(--spacing) * 10)}.w-16{width:calc(var(--spacing) * 16)}.w-20{width:calc(var(--spacing) * 20)}.w-\[calc\(100\%-2rem\)\]{width:calc(100% - 2rem)}.w-full{width:100%}.max-w-lg{max-width:var(--container-lg)}.max-w-md{max-width:var(--container-md)}.max-w-xl{max-width:var(--container-xl)}.min-w-6{min-width:calc(var(--spacing) * 6)}.flex-1{flex:1}.shrink-0{flex-shrink:0}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.animate-pulse{animation:var(--animate-pulse)}.animate-spin{animation:var(--animate-spin)}.cursor-pointer{cursor:pointer}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-\[64px_1fr\]{grid-template-columns:64px 1fr}.grid-cols-\[80px_1fr\]{grid-template-columns:80px 1fr}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-end{align-items:flex-end}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-1{gap:calc(var(--spacing) * 1)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 5) * calc(1 - var(--tw-space-y-reverse)))}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-xl{border-radius:var(--radius-xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-black\/20{border-color:#0003}@supports (color:color-mix(in lab, red, red)){.border-black\/20{border-color:color-mix(in oklab, var(--color-black) 20%, transparent)}}.border-gray-300{border-color:var(--color-gray-300)}.border-t-black{border-top-color:var(--color-black)}.bg-\[\#FF5A00\]{background-color:#ff5a00}.bg-black\/40{background-color:#0006}@supports (color:color-mix(in lab, red, red)){.bg-black\/40{background-color:color-mix(in oklab, var(--color-black) 40%, transparent)}}.bg-blue-400{background-color:var(--color-blue-400)}.bg-blue-500{background-color:var(--color-blue-500)}.bg-blue-600{background-color:var(--color-blue-600)}.bg-gray-800{background-color:var(--color-gray-800)}.bg-white{background-color:var(--color-white)}.bg-white\/60{background-color:#fff9}@supports (color:color-mix(in lab, red, red)){.bg-white\/60{background-color:color-mix(in oklab, var(--color-white) 60%, transparent)}}.\[mask-type\:luminance\]{mask-type:luminance}.fill-\(--brand-color\){fill:var(--brand-color)}.object-cover{object-fit:cover}.p-2{padding:calc(var(--spacing) * 2)}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.p-6{padding:calc(var(--spacing) * 6)}.p-8{padding:calc(var(--spacing) * 8)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.py-1{padding-block:calc(var(--spacing) * 1)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-8{padding-block:calc(var(--spacing) * 8)}.pb-3{padding-bottom:calc(var(--spacing) * 3)}.pb-4{padding-bottom:calc(var(--spacing) * 4)}.text-center{text-align:center}.text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.text-\(color\:--brand-text\){color:var(--brand-text)}.text-blue-500{color:var(--color-blue-500)}.text-gray-300{color:var(--color-gray-300)}.text-gray-500{color:var(--color-gray-500)}.text-gray-700{color:var(--color-gray-700)}.text-gray-800{color:var(--color-gray-800)}.text-gray-900{color:var(--color-gray-900)}.text-green-500{color:var(--color-green-500)}.text-red-500{color:var(--color-red-500)}.text-red-700{color:var(--color-red-700)}.text-white{color:var(--color-white)}.text-yellow-500{color:var(--color-yellow-500)}.capitalize{text-transform:capitalize}.uppercase{text-transform:uppercase}.underline{text-decoration-line:underline}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.opacity-100{opacity:1}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-2xl{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a), 0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a), 0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-300{--tw-duration:.3s;transition-duration:.3s}.forced-color-adjust-auto{forced-color-adjust:auto}.forced-color-adjust-none{forced-color-adjust:none}@media (hover:hover){.group-hover\:text-blue-600:is(:where(.group):hover *){color:var(--color-blue-600)}.group-hover\:text-gray-700:is(:where(.group):hover *){color:var(--color-gray-700)}}.peer-checked\:block:is(:where(.peer):checked~*){display:block}.first\:pt-0:first-child{padding-top:calc(var(--spacing) * 0)}.last\:pb-0:last-child{padding-bottom:calc(var(--spacing) * 0)}.odd\:bg-gray-50:nth-child(odd){background-color:var(--color-gray-50)}.even\:bg-white:nth-child(2n){background-color:var(--color-white)}.focus-within\:ring-2:focus-within{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}@media (hover:hover){.hover\:-translate-y-1:hover{--tw-translate-y:calc(var(--spacing) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.hover\:bg-blue-50:hover{background-color:var(--color-blue-50)}.hover\:bg-blue-600:hover{background-color:var(--color-blue-600)}.hover\:bg-blue-700:hover{background-color:var(--color-blue-700)}.hover\:bg-gray-50:hover{background-color:var(--color-gray-50)}.hover\:shadow-lg:hover{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}}.focus\:not-sr-only:focus{clip-path:none;white-space:normal;width:auto;height:auto;margin:0;padding:0;position:static;overflow:visible}.focus\:absolute:focus{position:absolute}.focus\:top-4:focus{top:calc(var(--spacing) * 4)}.focus\:left-4:focus{left:calc(var(--spacing) * 4)}.focus\:z-50:focus{z-index:50}.focus\:rounded:focus{border-radius:.25rem}.focus\:border-blue-500:focus{border-color:var(--color-blue-500)}.focus\:bg-white:focus{background-color:var(--color-white)}.focus\:px-4:focus{padding-inline:calc(var(--spacing) * 4)}.focus\:py-2:focus{padding-block:calc(var(--spacing) * 2)}.focus\:ring-1:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus\:ring-blue-500:focus{--tw-ring-color:var(--color-blue-500)}.focus\:ring-offset-2:focus{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.focus-visible\:ring-2:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus-visible\:ring-blue-500:focus-visible{--tw-ring-color:var(--color-blue-500)}.focus-visible\:ring-offset-2:focus-visible{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.active\:bg-blue-700:active{background-color:var(--color-blue-700)}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}@media (prefers-reduced-motion:reduce){.motion-reduce\:transform-none{transform:none}.motion-reduce\:transition-none{transition-property:none}}@media (width>=40rem){.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (width>=48rem){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:p-8{padding:calc(var(--spacing) * 8)}.md\:px-8{padding-inline:calc(var(--spacing) * 8)}@media (hover:hover){.md\:hover\:px-10:hover{padding-inline:calc(var(--spacing) * 10)}}}@media (width>=64rem){.lg\:w-16{width:calc(var(--spacing) * 16)}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:flex-col{flex-direction:column}.lg\:flex-row-reverse{flex-direction:row-reverse}.lg\:p-12{padding:calc(var(--spacing) * 12)}}@media (width>=80rem){.xl\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}}@media (prefers-color-scheme:dark){.dark\:bg-blue-400{background-color:var(--color-blue-400)}.dark\:bg-blue-500{background-color:var(--color-blue-500)}.dark\:bg-gray-900{background-color:var(--color-gray-900)}.dark\:text-gray-100{color:var(--color-gray-100)}.dark\:opacity-90{opacity:.9}.dark\:shadow-none{--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.dark\:ring-1{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.dark\:ring-white\/10{--tw-ring-color:#ffffff1a}@supports (color:color-mix(in lab, red, red)){.dark\:ring-white\/10{--tw-ring-color:color-mix(in oklab, var(--color-white) 10%, transparent)}}@media (hover:hover){.dark\:hover\:bg-blue-400:hover{background-color:var(--color-blue-400)}.dark\:hover\:bg-blue-500:hover{background-color:var(--color-blue-500)}}}}html{overscroll-behavior:none}@media (width>=768px){html{scrollbar-gutter:stable;-webkit-scroll-gutter:stable}}body{font-smooth:antialiased;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;overscroll-behavior:none}img,svg,video{pointer-events:none;-webkit-user-select:none;user-select:none}dialog{max-width:none;max-height:none}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@keyframes spin{to{transform:rotate(360deg)}}@keyframes pulse{50%{opacity:.5}} diff --git a/assets/page-Bwhrow8I.js b/assets/page-Bwhrow8I.js deleted file mode 100644 index e65331cef..000000000 --- a/assets/page-Bwhrow8I.js +++ /dev/null @@ -1 +0,0 @@ -import{c as B}from"./cart-Kw4e-iq4.js";import{n as M,a as F}from"./section-rendering-vVqNxfcB.js";function R(){const o=document.querySelector('[data-js="cart-page"]');if(!o)return;const i=o.dataset.sectionId,v=M(window.location.pathname),f=o.querySelector('[data-js="cart-page-items"]'),y=o.querySelector('[data-js="cart-page-empty"]'),m=o.querySelector('[data-js="cart-page-footer"]'),w=o.querySelector('[data-js="cart-page-subtotal"]'),g=o.querySelector('[data-js="cart-page-status"]'),j=o.querySelector('[data-js="cart-page-error"]'),N=document.querySelectorAll('[data-js="cart-count"]');if(!f||!y||!m||!w||!g||!j||!i)return;let n=f,S=y,d=m,l=!1,u=0,s=null;const q=t=>{g.textContent=t},h=t=>{j.textContent=t},x=t=>{o.setAttribute("aria-busy",String(t)),o.querySelectorAll("button").forEach(e=>{e.type!=="submit"&&(e.disabled=t)})},b=t=>{N.forEach(e=>{e.textContent=String(t),e.hidden=t<1})},C=(t,e)=>{const r=n.querySelector(`[data-line="${t}"]`);if(!r)return;r.classList.toggle("animate-pulse",e);const a=r.querySelector('[data-js="cart-page-line-overlay"]');a&&(e?(a.classList.remove("hidden"),a.classList.add("flex")):(a.classList.add("hidden"),a.classList.remove("flex")))},I=t=>{const e=F(t,'[data-js="cart-page"]',[{key:"items",current:n,selector:'[data-js="cart-page-items"]'},{key:"empty",current:S,selector:'[data-js="cart-page-empty"]'},{key:"footer",current:d,selector:'[data-js="cart-page-footer"]'}]);if(!e.ok)return!1;const r=e.nodes.items,a=e.nodes.empty,c=e.nodes.footer;return!r||!a||!c?!1:(n=r,S=a,d=c,!!d.querySelector('[data-js="cart-page-subtotal"]'))},k=t=>I(t),E=async(t,e)=>{const r=++u;l=!0,x(!0),C(t,!0),h("");try{const a=await B(t,e,{sections:[i],sectionsUrl:v});if(!k(a.sections?.[i])||r!==u)throw new Error("Cart section rendering failed");b(a.item_count),q(e===0?"Item removed.":"Cart updated.")}catch{h("Could not refresh cart UI. Please try again."),q("Cart update failed.")}finally{r===u&&(l=!1,x(!1),C(t,!1),s&&L())}},L=async()=>{if(!l)for(;s;){const t=s;s=null,await E(t.line,t.quantity)}},p=(t,e)=>{s={line:t,quantity:e},L()};o.addEventListener("click",t=>{const r=t.target.closest('[data-js="cart-page-dec"], [data-js="cart-page-inc"], [data-js="cart-page-remove"]');if(!r)return;const a=Number(r.dataset.line);if(!a)return;const A=n.querySelector(`[data-line="${a}"]`)?.querySelector('[data-js="cart-page-qty"]'),U=Number(A?.textContent??"1");if(r.dataset.js==="cart-page-remove"){p(a,0);return}if(r.dataset.js==="cart-page-inc"){p(a,U+1);return}r.dataset.js==="cart-page-dec"&&p(a,Math.max(0,U-1))})}export{R as i}; diff --git a/assets/page-CKX_Lx8T.js b/assets/page-CKX_Lx8T.js new file mode 100644 index 000000000..6ed5b494a --- /dev/null +++ b/assets/page-CKX_Lx8T.js @@ -0,0 +1 @@ +import{changeCartLine as e}from"./cart-DFJfubsI.js";import{applySectionReplace as t,normalizeSectionsUrl as n}from"./section-rendering-COdqImLj.js";function r(){let r=document.querySelector(`[data-js="cart-page"]`);if(!r)return;let i=r.dataset.sectionId,a=n(window.location.pathname),o=r.querySelector(`[data-js="cart-page-items"]`),s=r.querySelector(`[data-js="cart-page-empty"]`),c=r.querySelector(`[data-js="cart-page-footer"]`),l=r.querySelector(`[data-js="cart-page-subtotal"]`),u=r.querySelector(`[data-js="cart-page-status"]`),d=r.querySelector(`[data-js="cart-page-error"]`),f=document.querySelectorAll(`[data-js="cart-count"]`);if(!o||!s||!c||!l||!u||!d||!i)return;let p=o,m=s,h=c,g=!1,_=0,v=null,y=e=>{u.textContent=e},b=e=>{d.textContent=e},x=e=>{r.setAttribute(`aria-busy`,String(e)),r.querySelectorAll(`button`).forEach(t=>{t.type!==`submit`&&(t.disabled=e)})},S=e=>{f.forEach(t=>{t.textContent=String(e),t.hidden=e<1})},C=(e,t)=>{let n=p.querySelector(`[data-line="${e}"]`);if(!n)return;n.classList.toggle(`animate-pulse`,t);let r=n.querySelector(`[data-js="cart-page-line-overlay"]`);r&&(t?(r.classList.remove(`hidden`),r.classList.add(`flex`)):(r.classList.add(`hidden`),r.classList.remove(`flex`)))},w=e=>{let n=t(e,`[data-js="cart-page"]`,[{key:`items`,current:p,selector:`[data-js="cart-page-items"]`},{key:`empty`,current:m,selector:`[data-js="cart-page-empty"]`},{key:`footer`,current:h,selector:`[data-js="cart-page-footer"]`}]);if(!n.ok)return!1;let r=n.nodes.items,i=n.nodes.empty,a=n.nodes.footer;return!r||!i||!a?!1:(p=r,m=i,h=a,!!h.querySelector(`[data-js="cart-page-subtotal"]`))},T=e=>w(e),E=async(t,n)=>{let r=++_;g=!0,x(!0),C(t,!0),b(``);try{let o=await e(t,n,{sections:[i],sectionsUrl:a});if(!T(o.sections?.[i])||r!==_)throw Error(`Cart section rendering failed`);S(o.item_count),y(n===0?`Item removed.`:`Cart updated.`)}catch{b(`Could not refresh cart UI. Please try again.`),y(`Cart update failed.`)}finally{r===_&&(g=!1,x(!1),C(t,!1),v&&D())}},D=async()=>{if(!g)for(;v;){let e=v;v=null,await E(e.line,e.quantity)}},O=(e,t)=>{v={line:e,quantity:t},D()};r.addEventListener(`click`,e=>{let t=e.target.closest(`[data-js="cart-page-dec"], [data-js="cart-page-inc"], [data-js="cart-page-remove"]`);if(!t)return;let n=Number(t.dataset.line);if(!n)return;let r=p.querySelector(`[data-line="${n}"]`)?.querySelector(`[data-js="cart-page-qty"]`),i=Number(r?.textContent??`1`);if(t.dataset.js===`cart-page-remove`){O(n,0);return}if(t.dataset.js===`cart-page-inc`){O(n,i+1);return}t.dataset.js===`cart-page-dec`&&O(n,Math.max(0,i-1))})}export{r as initCartPage}; \ No newline at end of file diff --git a/assets/page-EITKwFv5.js b/assets/page-EITKwFv5.js new file mode 100644 index 000000000..e69de29bb diff --git a/assets/page-l0sNRNKZ.js b/assets/page-l0sNRNKZ.js deleted file mode 100644 index 8b1378917..000000000 --- a/assets/page-l0sNRNKZ.js +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/password-BArd4X24.js b/assets/password-BArd4X24.js new file mode 100644 index 000000000..e69de29bb diff --git a/assets/password-l0sNRNKZ.js b/assets/password-l0sNRNKZ.js deleted file mode 100644 index 8b1378917..000000000 --- a/assets/password-l0sNRNKZ.js +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/product-B6uO69WW.js b/assets/product-B6uO69WW.js new file mode 100644 index 000000000..c719fbc1f --- /dev/null +++ b/assets/product-B6uO69WW.js @@ -0,0 +1 @@ +import{state as e}from"./state-yJSoUqNP.js";import{syncDOM as t}from"./sync-CuhjKoMp.js";import{onAddToCart as n,onOptionClick as r,onThumbnailClick as i}from"./handlers-CMuQJ9_L.js";import{loadRecommendations as a}from"./recommendations-CnpPRP46.js";function o(e){let t=new URLSearchParams(window.location.search),n=Number(t.get(`variant`));return e.variants.find(e=>e.id===n)??e.variants.find(e=>e.available)??e.variants[0]}function s(t){if(e.currentVariant=t,e.selectedOptions=[...t.options],t.featured_media){e.currentMediaId=t.featured_media.id,e.mediaContextVariantId=t.id;return}if(e.currentMediaId===null){let t=document.querySelector(`[data-js="media-item"]`),n=Number(t?.dataset.mediaId??``),r=Number((t?.dataset.variantMedia??``).split(`,`)[0]);e.currentMediaId=n||null,e.mediaContextVariantId=r||e.mediaContextVariantId}}function c(e){return e instanceof HTMLElement}function l(){let l=document.querySelector(`[data-js="product-data"]`);if(!l?.textContent)return;e.productData=JSON.parse(l.textContent);let u=document.querySelector(`[data-js="variant-prices"]`);e.variantPrices=u?.textContent?JSON.parse(u.textContent):{},s(o(e.productData));let d=!1,f=()=>{d||(d=!0,a())},p=e=>{c(e.target)&&e.target.closest(`[data-js="product-form"], [data-product-media], [data-product-thumbnails]`)&&(f(),document.removeEventListener(`pointerdown`,p,!0),document.removeEventListener(`keydown`,m,!0))},m=e=>{e.key!==`Enter`&&e.key!==` `||c(e.target)&&e.target.closest(`[data-js="product-form"], [data-product-media], [data-product-thumbnails]`)&&(f(),document.removeEventListener(`pointerdown`,p,!0),document.removeEventListener(`keydown`,m,!0))};document.addEventListener(`pointerdown`,p,!0),document.addEventListener(`keydown`,m,!0);let h=document.querySelector(`[data-js="product-form"]`);h?.addEventListener(`click`,e=>{r(e)}),h?.addEventListener(`submit`,e=>{f(),n(e)}),document.addEventListener(`click`,e=>{i(e)}),window.addEventListener(`popstate`,()=>{s(o(e.productData)),e.cartState=`idle`,t()}),t()}document.addEventListener(`DOMContentLoaded`,l); \ No newline at end of file diff --git a/assets/product-_rxK6zOl.js b/assets/product-_rxK6zOl.js deleted file mode 100644 index ba2f35c17..000000000 --- a/assets/product-_rxK6zOl.js +++ /dev/null @@ -1 +0,0 @@ -import{s as n}from"./state-Bwp_aMHz.js";import{s as c}from"./sync-BRYiu7LQ.js";import{o as f,a as v,b as L}from"./handlers-B949bjPl.js";import{l as E}from"./recommendations-H0stnrna.js";import"./variant-picker-DRrQ71gT.js";import"./cart-Kw4e-iq4.js";import"./cart-events-C4m0PwR1.js";function u(t){const a=new URLSearchParams(window.location.search),o=Number(a.get("variant"));return t.variants.find(r=>r.id===o)??t.variants.find(r=>r.available)??t.variants[0]}function m(t){if(n.currentVariant=t,n.selectedOptions=[...t.options],t.featured_media){n.currentMediaId=t.featured_media.id,n.mediaContextVariantId=t.id;return}if(n.currentMediaId===null){const a=document.querySelector('[data-js="media-item"]'),o=Number(a?.dataset.mediaId??""),r=Number((a?.dataset.variantMedia??"").split(",")[0]);n.currentMediaId=o||null,n.mediaContextVariantId=r||n.mediaContextVariantId}}function l(t){return t instanceof HTMLElement}function I(){const t=document.querySelector('[data-js="product-data"]');if(!t?.textContent)return;n.productData=JSON.parse(t.textContent);const a=document.querySelector('[data-js="variant-prices"]');n.variantPrices=a?.textContent?JSON.parse(a.textContent):{},m(u(n.productData));let o=!1;const r=()=>{o||(o=!0,E())},i=e=>{!l(e.target)||!e.target.closest('[data-js="product-form"], [data-product-media], [data-product-thumbnails]')||(r(),document.removeEventListener("pointerdown",i,!0),document.removeEventListener("keydown",d,!0))},d=e=>{e.key!=="Enter"&&e.key!==" "||!l(e.target)||!e.target.closest('[data-js="product-form"], [data-product-media], [data-product-thumbnails]')||(r(),document.removeEventListener("pointerdown",i,!0),document.removeEventListener("keydown",d,!0))};document.addEventListener("pointerdown",i,!0),document.addEventListener("keydown",d,!0);const s=document.querySelector('[data-js="product-form"]');s?.addEventListener("click",e=>{f(e)}),s?.addEventListener("submit",e=>{r(),v(e)}),document.addEventListener("click",e=>{L(e)}),window.addEventListener("popstate",()=>{m(u(n.productData)),n.cartState="idle",c()}),c()}document.addEventListener("DOMContentLoaded",I); diff --git a/assets/recommendations-CnpPRP46.js b/assets/recommendations-CnpPRP46.js new file mode 100644 index 000000000..089318a1a --- /dev/null +++ b/assets/recommendations-CnpPRP46.js @@ -0,0 +1 @@ +function e(){let e=document.querySelector(`[data-js="recommendations"]`);if(!e)return;let t=e.dataset.productId,n=e.dataset.sectionId;if(!t||!n)return;let r=`${window.Shopify.routes.root}recommendations/products?section_id=${n}&product_id=${t}&intent=related`;fetch(r).then(e=>e.text()).then(t=>{let n=new DOMParser().parseFromString(t,`text/html`).querySelector(`[data-js="product-recommendations-root"]`);n&&(e.innerHTML=n.innerHTML)}).catch(()=>{})}export{e as loadRecommendations}; \ No newline at end of file diff --git a/assets/recommendations-H0stnrna.js b/assets/recommendations-H0stnrna.js deleted file mode 100644 index 387451d8f..000000000 --- a/assets/recommendations-H0stnrna.js +++ /dev/null @@ -1 +0,0 @@ -function s(){const t=document.querySelector('[data-js="recommendations"]');if(!t)return;const n=t.dataset.productId,o=t.dataset.sectionId;if(!n||!o)return;const c=`${window.Shopify.routes.root}recommendations/products?section_id=${o}&product_id=${n}&intent=related`;fetch(c).then(e=>e.text()).then(e=>{const r=new DOMParser().parseFromString(e,"text/html").querySelector('[data-js="product-recommendations-root"]');r&&(t.innerHTML=r.innerHTML)}).catch(()=>{})}export{s as l}; diff --git a/assets/search-EaqtnZZe.js b/assets/search-EaqtnZZe.js new file mode 100644 index 000000000..6dc2fd2a4 --- /dev/null +++ b/assets/search-EaqtnZZe.js @@ -0,0 +1 @@ +function e(e,t=250){let n=null;return(...r)=>{n!==null&&window.clearTimeout(n),n=window.setTimeout(()=>{e(...r)},t)}}document.addEventListener(`DOMContentLoaded`,()=>{let t=document.querySelector(`[data-js="search-root"]`);if(!t)return;let n=t.querySelector(`[data-js="search-input"]`),r=t.querySelector(`[data-js="predictive-results"]`),i=t.querySelector(`[data-js="predictive-list"]`),a=t.querySelector(`[data-js="predictive-status"]`),o=t.querySelector(`[data-js="search-status"]`);if(!n||!r||!i||!a||!o)return;let s=null,c=e=>{o.textContent=e},l=e=>{a.textContent=e},u=()=>{r.hidden=!0,n.setAttribute(`aria-expanded`,`false`)},d=()=>{r.hidden=!1,n.setAttribute(`aria-expanded`,`true`)},f=e=>{let t=document.createElement(`li`),n=document.createElement(`a`);if(n.href=e.url,n.className=`flex items-center gap-3 rounded border px-2 py-2 hover:bg-gray-50`,e.image?.url){let t=document.createElement(`img`);t.src=e.image.url,t.alt=e.image.alt??e.title,t.className=`h-10 w-10 object-cover`,n.appendChild(t)}let r=document.createElement(`span`);return r.className=`text-sm`,r.textContent=e.title,n.appendChild(r),t.appendChild(n),t},p=e=>{if(i.replaceChildren(),e.length===0){l(`No quick matches. Press Enter for full results.`),d();return}let t=document.createDocumentFragment();e.forEach(e=>{t.appendChild(f(e))}),i.appendChild(t),l(`${e.length} quick matches`),d()},m=async e=>{if(e.length<2){u(),l(``);return}s?.abort(),s=new AbortController,l(`Searching...`),c(`Predictive search active`),d();let t=`${window.Shopify.routes.root}search/suggest.json?q=${encodeURIComponent(e)}&resources[type]=product,page,article&resources[limit]=6&resources[options][unavailable_products]=last`;try{let e=await fetch(t,{signal:s.signal,headers:{Accept:`application/json`,"X-Requested-With":`XMLHttpRequest`}});if(!e.ok)throw Error(`Predictive endpoint unavailable`);let n=await e.json(),r=n.resources?.results?.products??[],i=n.resources?.results?.pages??[],a=n.resources?.results?.articles??[];p([...r,...i,...a].slice(0,6))}catch(e){if(e instanceof DOMException&&e.name===`AbortError`)return;u(),l(``),c(`Predictive search unavailable. Full search still works.`)}},h=e(e=>{m(e.trim())});n.addEventListener(`input`,()=>{h(n.value)}),n.addEventListener(`focus`,()=>{i.children.length>0&&d()}),t.addEventListener(`focusout`,()=>{window.setTimeout(()=>{let e=document.activeElement;e&&t.contains(e)||u()},100)})}); \ No newline at end of file diff --git a/assets/search-bqQT_R15.js b/assets/search-bqQT_R15.js deleted file mode 100644 index 7da4c1115..000000000 --- a/assets/search-bqQT_R15.js +++ /dev/null @@ -1 +0,0 @@ -function S(r,n=250){let s=null;return(...a)=>{s!==null&&window.clearTimeout(s),s=window.setTimeout(()=>{r(...a)},n)}}document.addEventListener("DOMContentLoaded",()=>{const r=document.querySelector('[data-js="search-root"]');if(!r)return;const n=r.querySelector('[data-js="search-input"]'),s=r.querySelector('[data-js="predictive-results"]'),a=r.querySelector('[data-js="predictive-list"]'),h=r.querySelector('[data-js="predictive-status"]'),m=r.querySelector('[data-js="search-status"]');if(!n||!s||!a||!h||!m)return;let d=null;const f=e=>{m.textContent=e},l=e=>{h.textContent=e},p=()=>{s.hidden=!0,n.setAttribute("aria-expanded","false")},u=()=>{s.hidden=!1,n.setAttribute("aria-expanded","true")},g=e=>{const o=document.createElement("li"),t=document.createElement("a");if(t.href=e.url,t.className="flex items-center gap-3 rounded border px-2 py-2 hover:bg-gray-50",e.image?.url){const i=document.createElement("img");i.src=e.image.url,i.alt=e.image.alt??e.title,i.className="h-10 w-10 object-cover",t.appendChild(i)}const c=document.createElement("span");return c.className="text-sm",c.textContent=e.title,t.appendChild(c),o.appendChild(t),o},v=e=>{if(a.replaceChildren(),e.length===0){l("No quick matches. Press Enter for full results."),u();return}const o=document.createDocumentFragment();e.forEach(t=>{o.appendChild(g(t))}),a.appendChild(o),l(`${e.length} quick matches`),u()},w=async e=>{if(e.length<2){p(),l("");return}d?.abort(),d=new AbortController,l("Searching..."),f("Predictive search active"),u();const o=`${window.Shopify.routes.root}search/suggest.json?q=${encodeURIComponent(e)}&resources[type]=product,page,article&resources[limit]=6&resources[options][unavailable_products]=last`;try{const t=await fetch(o,{signal:d.signal,headers:{Accept:"application/json","X-Requested-With":"XMLHttpRequest"}});if(!t.ok)throw new Error("Predictive endpoint unavailable");const c=await t.json(),i=c.resources?.results?.products??[],y=c.resources?.results?.pages??[],C=c.resources?.results?.articles??[];v([...i,...y,...C].slice(0,6))}catch(t){if(t instanceof DOMException&&t.name==="AbortError")return;p(),l(""),f("Predictive search unavailable. Full search still works.")}},E=S(e=>{w(e.trim())});n.addEventListener("input",()=>{E(n.value)}),n.addEventListener("focus",()=>{a.children.length>0&&u()}),r.addEventListener("focusout",()=>{window.setTimeout(()=>{const e=document.activeElement;e&&r.contains(e)||p()},100)})}); diff --git a/assets/section-rendering-COdqImLj.js b/assets/section-rendering-COdqImLj.js new file mode 100644 index 000000000..5abe82bdd --- /dev/null +++ b/assets/section-rendering-COdqImLj.js @@ -0,0 +1 @@ +function e(e,t,n){if(!e)return{ok:!1,nodes:{}};let r=new DOMParser().parseFromString(e,`text/html`).querySelector(t);if(!r)return{ok:!1,nodes:{}};let i={};for(let e of n){let t=r.querySelector(e.selector);if(!t&&e.required!==!1)return{ok:!1,nodes:{}};t&&(i[e.key]=t)}for(let e of n){let t=i[e.key];t&&e.current.replaceWith(t)}return{ok:!0,nodes:i}}function t(e){return e?e.startsWith(`/`)?e:`/${e}`:`/`}async function n(e,n){let r=t(n),i=new URL(r,window.location.origin);i.searchParams.set(`section_id`,e);let a=await fetch(i.pathname+i.search,{headers:{"X-Requested-With":`XMLHttpRequest`}});if(!a.ok)throw Error(`Section rendering request failed`);return a.text()}export{e as applySectionReplace,n as fetchSingleSectionHtml,t as normalizeSectionsUrl}; \ No newline at end of file diff --git a/assets/section-rendering-vVqNxfcB.js b/assets/section-rendering-vVqNxfcB.js deleted file mode 100644 index 7a47c73b8..000000000 --- a/assets/section-rendering-vVqNxfcB.js +++ /dev/null @@ -1 +0,0 @@ -function f(e,a,o){if(!e)return{ok:!1,nodes:{}};const r=new DOMParser().parseFromString(e,"text/html").querySelector(a);if(!r)return{ok:!1,nodes:{}};const i={};for(const t of o){const n=r.querySelector(t.selector);if(!n&&t.required!==!1)return{ok:!1,nodes:{}};n&&(i[t.key]=n)}for(const t of o){const n=i[t.key];n&&t.current.replaceWith(n)}return{ok:!0,nodes:i}}function c(e){return e?e.startsWith("/")?e:`/${e}`:"/"}async function d(e,a){const o=c(a),s=new URL(o,window.location.origin);s.searchParams.set("section_id",e);const r=await fetch(s.pathname+s.search,{headers:{"X-Requested-With":"XMLHttpRequest"}});if(!r.ok)throw new Error("Section rendering request failed");return r.text()}export{f as a,d as f,c as n}; diff --git a/assets/state-Bwp_aMHz.js b/assets/state-Bwp_aMHz.js deleted file mode 100644 index c202ed658..000000000 --- a/assets/state-Bwp_aMHz.js +++ /dev/null @@ -1 +0,0 @@ -const t={productData:null,currentVariant:null,selectedOptions:[],variantPrices:{},cartState:"idle",currentMediaId:null,mediaContextVariantId:null};export{t as s}; diff --git a/assets/state-yJSoUqNP.js b/assets/state-yJSoUqNP.js new file mode 100644 index 000000000..c7c8e1d85 --- /dev/null +++ b/assets/state-yJSoUqNP.js @@ -0,0 +1 @@ +var e={productData:null,currentVariant:null,selectedOptions:[],variantPrices:{},cartState:`idle`,currentMediaId:null,mediaContextVariantId:null};export{e as state}; \ No newline at end of file diff --git a/assets/sync-BRYiu7LQ.js b/assets/sync-BRYiu7LQ.js deleted file mode 100644 index be04a28c6..000000000 --- a/assets/sync-BRYiu7LQ.js +++ /dev/null @@ -1 +0,0 @@ -import{g as p}from"./variant-picker-DRrQ71gT.js";import{s as a}from"./state-Bwp_aMHz.js";function b(t,e){return t?t.split(",").some(r=>r.trim()===e):!1}function y(t){if(!t)return;const e=document.querySelector("[data-product-media]"),r=document.querySelector("[data-product-thumbnails]");if(e){const n=e.querySelector(`[data-js="media-item"][data-media-id="${t}"]`);n&&e.firstElementChild!==n&&e.prepend(n)}if(r){const n=r.querySelector(`[data-js="thumbnail"][data-thumbnail="${t}"]`);n&&r.firstElementChild!==n&&r.prepend(n)}}function S(t,e){return t.find(n=>b(n.dataset.variantMedia,e))?.dataset.mediaId??null}function E(){v(),h(),V(),A(),g(),I(),M()}function v(){const t=document.querySelector('[data-js="product-price"]');if(!t)return;const e=a.variantPrices[String(a.currentVariant.id)];e&&(t.textContent=e)}function h(){const t=document.querySelector('[data-js="product-availability"]');t&&(t.textContent=a.currentVariant.available?"":"Sold out")}function V(){const t=Array.from(document.querySelectorAll('[data-js="media-item"]'));if(t.length===0)return;const e=a.mediaContextVariantId!==null?String(a.mediaContextVariantId):String(a.currentVariant.id),r=S(t,e),n=a.currentMediaId!==null?String(a.currentMediaId):null,o=n?t.find(i=>i.dataset.mediaId===n):null,d=o?!o.dataset.variantMedia:!1,s=t[0]?.dataset.mediaId??null;let c=!1;t.forEach(i=>{const l=!i.dataset.variantMedia,u=r!==null&&i.dataset.mediaId===r;l||u?(i.removeAttribute("hidden"),c=!0):(i.setAttribute("hidden",""),i.querySelector("video")?.pause())}),!c&&s&&t.forEach(i=>{i.dataset.mediaId===s?i.removeAttribute("hidden"):(i.setAttribute("hidden",""),i.querySelector("video")?.pause())});const m=c?r??(d?n:null)??s:s;y(r??m),document.querySelectorAll('[data-js="thumbnail"]').forEach(i=>{const l=!i.dataset.variantMedia,u=r!==null&&i.dataset.thumbnail===r;l||u?i.removeAttribute("hidden"):i.setAttribute("hidden",""),i.setAttribute("aria-pressed",String(i.dataset.thumbnail===m))})}function g(){const t=document.querySelector('[data-js="add-to-cart"]');if(!t)return;const e={idle:a.currentVariant.available?"Add to cart":"Sold out",loading:"Adding...",success:"Added!",error:"Try again"};t.disabled=!a.currentVariant.available||a.cartState==="loading",t.setAttribute("aria-busy",String(a.cartState==="loading")),t.textContent=e[a.cartState]}function A(){const t=document.querySelector('[data-js="variant-id"]');t&&(t.value=String(a.currentVariant.id))}function I(){const t=document.querySelector('[data-js="cart-status"]');if(!t)return;const e={idle:a.currentVariant.available?"":"This variant is sold out.",loading:"",success:"Added to cart.",error:"Could not add to cart. Please try again."};t.textContent=e[a.cartState]}function M(){document.querySelectorAll('[data-js="option-value"]').forEach(t=>{const e=Number(t.dataset.optionPosition)-1,r=t.dataset.optionValue??"",n=a.selectedOptions[e]===r;t.setAttribute("aria-pressed",String(n));const d=p(a.productData.variants,a.selectedOptions,e).has(r);t.setAttribute("aria-disabled",String(!d)),t.disabled=!d}),document.querySelectorAll('[data-js="option-label"]').forEach(t=>{const e=Number(t.dataset.optionLabel)-1;t.textContent=a.selectedOptions[e]??""})}export{E as s}; diff --git a/assets/sync-CuhjKoMp.js b/assets/sync-CuhjKoMp.js new file mode 100644 index 000000000..da6f783c2 --- /dev/null +++ b/assets/sync-CuhjKoMp.js @@ -0,0 +1 @@ +import{state as e}from"./state-yJSoUqNP.js";import{getAvailableValues as t}from"./variant-picker-BcihOqVL.js";function n(e,t){return e?e.split(`,`).some(e=>e.trim()===t):!1}function r(e){if(!e)return;let t=document.querySelector(`[data-product-media]`),n=document.querySelector(`[data-product-thumbnails]`);if(t){let n=t.querySelector(`[data-js="media-item"][data-media-id="${e}"]`);n&&t.firstElementChild!==n&&t.prepend(n)}if(n){let t=n.querySelector(`[data-js="thumbnail"][data-thumbnail="${e}"]`);t&&n.firstElementChild!==t&&n.prepend(t)}}function i(e,t){return e.find(e=>n(e.dataset.variantMedia,t))?.dataset.mediaId??null}function a(){o(),s(),c(),u(),l(),d(),f()}function o(){let t=document.querySelector(`[data-js="product-price"]`);if(!t)return;let n=e.variantPrices[String(e.currentVariant.id)];n&&(t.textContent=n)}function s(){let t=document.querySelector(`[data-js="product-availability"]`);t&&(t.textContent=e.currentVariant.available?``:`Sold out`)}function c(){let t=Array.from(document.querySelectorAll(`[data-js="media-item"]`));if(t.length===0)return;let n=i(t,e.mediaContextVariantId===null?String(e.currentVariant.id):String(e.mediaContextVariantId)),a=e.currentMediaId===null?null:String(e.currentMediaId),o=a?t.find(e=>e.dataset.mediaId===a):null,s=o?!o.dataset.variantMedia:!1,c=t[0]?.dataset.mediaId??null,l=!1;t.forEach(e=>{let t=!e.dataset.variantMedia,r=n!==null&&e.dataset.mediaId===n;t||r?(e.removeAttribute(`hidden`),l=!0):(e.setAttribute(`hidden`,``),e.querySelector(`video`)?.pause())}),!l&&c&&t.forEach(e=>{e.dataset.mediaId===c?e.removeAttribute(`hidden`):(e.setAttribute(`hidden`,``),e.querySelector(`video`)?.pause())});let u=l?n??(s?a:null)??c:c;r(n??u),document.querySelectorAll(`[data-js="thumbnail"]`).forEach(e=>{let t=!e.dataset.variantMedia,r=n!==null&&e.dataset.thumbnail===n;t||r?e.removeAttribute(`hidden`):e.setAttribute(`hidden`,``),e.setAttribute(`aria-pressed`,String(e.dataset.thumbnail===u))})}function l(){let t=document.querySelector(`[data-js="add-to-cart"]`);if(!t)return;let n={idle:e.currentVariant.available?`Add to cart`:`Sold out`,loading:`Adding...`,success:`Added!`,error:`Try again`};t.disabled=!e.currentVariant.available||e.cartState===`loading`,t.setAttribute(`aria-busy`,String(e.cartState===`loading`)),t.textContent=n[e.cartState]}function u(){let t=document.querySelector(`[data-js="variant-id"]`);t&&(t.value=String(e.currentVariant.id))}function d(){let t=document.querySelector(`[data-js="cart-status"]`);t&&(t.textContent={idle:e.currentVariant.available?``:`This variant is sold out.`,loading:``,success:`Added to cart.`,error:`Could not add to cart. Please try again.`}[e.cartState])}function f(){document.querySelectorAll(`[data-js="option-value"]`).forEach(n=>{let r=Number(n.dataset.optionPosition)-1,i=n.dataset.optionValue??``,a=e.selectedOptions[r]===i;n.setAttribute(`aria-pressed`,String(a));let o=t(e.productData.variants,e.selectedOptions,r).has(i);n.setAttribute(`aria-disabled`,String(!o)),n.disabled=!o}),document.querySelectorAll(`[data-js="option-label"]`).forEach(t=>{let n=Number(t.dataset.optionLabel)-1;t.textContent=e.selectedOptions[n]??``})}export{a as syncDOM}; \ No newline at end of file diff --git a/assets/theme-DcEodf02.js b/assets/theme-DcEodf02.js new file mode 100644 index 000000000..2b8cd104a --- /dev/null +++ b/assets/theme-DcEodf02.js @@ -0,0 +1,2 @@ +const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["./drawer-BY7bJn32.js","./cart-events-DFMQ7aoZ.js","./cart-DFJfubsI.js","./section-rendering-COdqImLj.js"])))=>i.map(i=>d[i]); +(function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))n(e);new MutationObserver(e=>{for(let t of e)if(t.type===`childList`)for(let e of t.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&n(e)}).observe(document,{childList:!0,subtree:!0});function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),e.crossOrigin===`use-credentials`?t.credentials=`include`:e.crossOrigin===`anonymous`?t.credentials=`omit`:t.credentials=`same-origin`,t}function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}})();var e=`modulepreload`,t=function(e,t){return new URL(e,t).href},n={},r=function(r,i,a){let o=Promise.resolve();if(i&&i.length>0){let r=document.getElementsByTagName(`link`),s=document.querySelector(`meta[property=csp-nonce]`),c=s?.nonce||s?.getAttribute(`nonce`);function l(e){return Promise.all(e.map(e=>Promise.resolve(e).then(e=>({status:`fulfilled`,value:e}),e=>({status:`rejected`,reason:e}))))}o=l(i.map(i=>{if(i=t(i,a),i in n)return;n[i]=!0;let o=i.endsWith(`.css`),s=o?`[rel="stylesheet"]`:``;if(a)for(let e=r.length-1;e>=0;e--){let t=r[e];if(t.href===i&&(!o||t.rel===`stylesheet`))return}else if(document.querySelector(`link[href="${i}"]${s}`))return;let l=document.createElement(`link`);if(l.rel=o?`stylesheet`:e,o||(l.as=`script`),l.crossOrigin=``,l.href=i,c&&l.setAttribute(`nonce`,c),document.head.appendChild(l),o)return new Promise((e,t)=>{l.addEventListener(`load`,e),l.addEventListener(`error`,()=>t(Error(`Unable to preload CSS for ${i}`)))})}))}function s(e){let t=new Event(`vite:preloadError`,{cancelable:!0});if(t.payload=e,window.dispatchEvent(t),!t.defaultPrevented)throw e}return o.then(e=>{for(let t of e||[])t.status===`rejected`&&s(t.reason);return r().catch(s)})};document.addEventListener(`DOMContentLoaded`,()=>{let e=document.querySelector(`[data-js="cart-drawer"]`),t=document.querySelector(`[data-js="cart-open"]`),n=document.querySelector(`[data-js="search-drawer"]`),i=document.querySelector(`[data-js="search-open"]`);(e||t)&&r(async()=>{let{initCartDrawer:e}=await import(`./drawer-BY7bJn32.js`);return{initCartDrawer:e}},__vite__mapDeps([0,1,2,3]),import.meta.url).then(({initCartDrawer:e})=>{e()}),(n||i)&&r(async()=>{let{initSearchDrawer:e}=await import(`./drawer-DwueD-Lo.js`);return{initSearchDrawer:e}},[],import.meta.url).then(({initSearchDrawer:e})=>{e()})}); \ No newline at end of file diff --git a/assets/theme-skURl5jI.js b/assets/theme-skURl5jI.js deleted file mode 100644 index 6b86b1b4c..000000000 --- a/assets/theme-skURl5jI.js +++ /dev/null @@ -1,2 +0,0 @@ -const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["./drawer-CJwC366z.js","./cart-Kw4e-iq4.js","./cart-events-C4m0PwR1.js","./section-rendering-vVqNxfcB.js"])))=>i.map(i=>d[i]); -const v="modulepreload",w=function(u,o){return new URL(u,o).href},p={},y=function(o,i,a){let e=Promise.resolve();if(i&&i.length>0){let g=function(n){return Promise.all(n.map(l=>Promise.resolve(l).then(d=>({status:"fulfilled",value:d}),d=>({status:"rejected",reason:d}))))};const r=document.getElementsByTagName("link"),s=document.querySelector("meta[property=csp-nonce]"),h=s?.nonce||s?.getAttribute("nonce");e=g(i.map(n=>{if(n=w(n,a),n in p)return;p[n]=!0;const l=n.endsWith(".css"),d=l?'[rel="stylesheet"]':"";if(a)for(let f=r.length-1;f>=0;f--){const m=r[f];if(m.href===n&&(!l||m.rel==="stylesheet"))return}else if(document.querySelector(`link[href="${n}"]${d}`))return;const c=document.createElement("link");if(c.rel=l?"stylesheet":v,l||(c.as="script"),c.crossOrigin="",c.href=n,h&&c.setAttribute("nonce",h),document.head.appendChild(c),l)return new Promise((f,m)=>{c.addEventListener("load",f),c.addEventListener("error",()=>m(new Error(`Unable to preload CSS for ${n}`)))})}))}function t(r){const s=new Event("vite:preloadError",{cancelable:!0});if(s.payload=r,window.dispatchEvent(s),!s.defaultPrevented)throw r}return e.then(r=>{for(const s of r||[])s.status==="rejected"&&t(s.reason);return o().catch(t)})};(function(){const o=document.createElement("link").relList;if(o&&o.supports&&o.supports("modulepreload"))return;for(const e of document.querySelectorAll('link[rel="modulepreload"]'))a(e);new MutationObserver(e=>{for(const t of e)if(t.type==="childList")for(const r of t.addedNodes)r.tagName==="LINK"&&r.rel==="modulepreload"&&a(r)}).observe(document,{childList:!0,subtree:!0});function i(e){const t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),e.crossOrigin==="use-credentials"?t.credentials="include":e.crossOrigin==="anonymous"?t.credentials="omit":t.credentials="same-origin",t}function a(e){if(e.ep)return;e.ep=!0;const t=i(e);fetch(e.href,t)}})();document.addEventListener("DOMContentLoaded",()=>{const u=document.querySelector('[data-js="cart-drawer"]'),o=document.querySelector('[data-js="cart-open"]'),i=document.querySelector('[data-js="search-drawer"]'),a=document.querySelector('[data-js="search-open"]');(u||o)&&y(async()=>{const{initCartDrawer:e}=await import("./drawer-CJwC366z.js");return{initCartDrawer:e}},__vite__mapDeps([0,1,2,3]),import.meta.url).then(({initCartDrawer:e})=>{e()}),(i||a)&&y(async()=>{const{initSearchDrawer:e}=await import("./drawer-BOuNB4a_.js");return{initSearchDrawer:e}},[],import.meta.url).then(({initSearchDrawer:e})=>{e()})}); diff --git a/assets/variant-picker-BcihOqVL.js b/assets/variant-picker-BcihOqVL.js new file mode 100644 index 000000000..ab08904da --- /dev/null +++ b/assets/variant-picker-BcihOqVL.js @@ -0,0 +1 @@ +function e(e,t){return e.find(e=>e.options.every((e,n)=>e===t[n]))}function t(e,t,n){return new Set(e.filter(e=>e.options.every((e,r)=>r===n||e===t[r])).filter(e=>e.available).map(e=>e.options[n]))}export{e as findVariantByOptions,t as getAvailableValues}; \ No newline at end of file diff --git a/assets/variant-picker-DRrQ71gT.js b/assets/variant-picker-DRrQ71gT.js deleted file mode 100644 index f4573220c..000000000 --- a/assets/variant-picker-DRrQ71gT.js +++ /dev/null @@ -1 +0,0 @@ -function o(t,a){return t.find(n=>n.options.every((e,i)=>e===a[i]))}function f(t,a,n){return new Set(t.filter(e=>e.options.every((i,r)=>r===n||i===a[r])).filter(e=>e.available).map(e=>e.options[n]))}export{o as f,f as g}; diff --git a/bun.lock b/bun.lock index 667e56156..ef131d12f 100644 --- a/bun.lock +++ b/bun.lock @@ -4,12 +4,12 @@ "workspaces": { "": { "devDependencies": { - "@tailwindcss/vite": "^4.2.1", + "@tailwindcss/vite": "^4.2.2", "npm-run-all": "^4.1.5", - "tailwindcss": "^4.2.1", - "typescript": "~5.9.3", - "vite": "^7.3.1", - "vite-plugin-shopify": "^4.1.1", + "tailwindcss": "^4.2.2", + "typescript": "^5.9.3", + "vite": "^8.0.2", + "vite-plugin-shopify": "^4.1.2", }, }, }, @@ -30,57 +30,11 @@ "@bugsnag/safe-json-stringify": ["@bugsnag/safe-json-stringify@6.1.0", "", {}, "sha512-ImA35rnM7bGr+J30R979FQ95BhRB4UO1KfJA0J2sVqc8nwnrS9hhE5mkTmQWMs8Vh1Da+hkLKs5jJB4JjNZp4A=="], - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="], + "@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="], - "@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="], + "@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="], - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.3", "", { "os": "android", "cpu": "arm64" }, "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg=="], - - "@esbuild/android-x64": ["@esbuild/android-x64@0.27.3", "", { "os": "android", "cpu": "x64" }, "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ=="], - - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg=="], - - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg=="], - - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w=="], - - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA=="], - - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.3", "", { "os": "linux", "cpu": "arm" }, "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw=="], - - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg=="], - - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg=="], - - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA=="], - - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw=="], - - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA=="], - - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ=="], - - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw=="], - - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA=="], - - "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA=="], - - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.3", "", { "os": "none", "cpu": "x64" }, "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA=="], - - "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.3", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw=="], - - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ=="], - - "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g=="], - - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.3", "", { "os": "sunos", "cpu": "x64" }, "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA=="], - - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA=="], - - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q=="], - - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="], + "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], "@graphql-typed-document-node/core": ["@graphql-typed-document-node/core@3.2.0", "", { "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ=="], @@ -102,6 +56,8 @@ "@kwsites/promise-deferred": ["@kwsites/promise-deferred@1.1.1", "", {}, "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw=="], + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="], + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], @@ -132,6 +88,8 @@ "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="], + "@oxc-project/types": ["@oxc-project/types@0.122.0", "", {}, "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA=="], + "@pnpm/config.env-replace": ["@pnpm/config.env-replace@1.1.0", "", {}, "sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w=="], "@pnpm/network.ca-file": ["@pnpm/network.ca-file@1.0.2", "", { "dependencies": { "graceful-fs": "4.2.10" } }, "sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA=="], @@ -158,55 +116,37 @@ "@protobufjs/utf8": ["@protobufjs/utf8@1.1.0", "", {}, "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="], - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="], - - "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q=="], - - "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.59.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg=="], - - "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.59.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w=="], + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.11", "", { "os": "android", "cpu": "arm64" }, "sha512-SJ+/g+xNnOh6NqYxD0V3uVN4W3VfnrGsC9/hoglicgTNfABFG9JjISvkkU0dNY84MNHLWyOgxP9v9Y9pX4S7+A=="], - "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.59.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA=="], + "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-7WQgR8SfOPwmDZGFkThUvsmd/nwAWv91oCO4I5LS7RKrssPZmOt7jONN0cW17ydGC1n/+puol1IpoieKqQidmg=="], - "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.59.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg=="], + "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-39Ks6UvIHq4rEogIfQBoBRusj0Q0nPVWIvqmwBLaT6aqQGIakHdESBVOPRRLacy4WwUPIx4ZKzfZ9PMW+IeyUQ=="], - "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw=="], + "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.11", "", { "os": "freebsd", "cpu": "x64" }, "sha512-jfsm0ZHfhiqrvWjJAmzsqiIFPz5e7mAoCOPBNTcNgkiid/LaFKiq92+0ojH+nmJmKYkre4t71BWXUZDNp7vsag=="], - "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA=="], + "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.11", "", { "os": "linux", "cpu": "arm" }, "sha512-zjQaUtSyq1nVe3nxmlSCuR96T1LPlpvmJ0SZy0WJFEsV4kFbXcq2u68L4E6O0XeFj4aex9bEauqjW8UQBeAvfQ=="], - "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA=="], + "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-WMW1yE6IOnehTcFE9eipFkm3XN63zypWlrJQ2iF7NrQ9b2LDRjumFoOGJE8RJJTJCTBAdmLMnJ8uVitACUUo1Q=="], - "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA=="], + "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-jfndI9tsfm4APzjNt6QdBkYwre5lRPUgHeDHoI7ydKUuJvz3lZeCfMsI56BZj+7BYqiKsJm7cfd/6KYV7ubrBg=="], - "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg=="], + "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.11", "", { "os": "linux", "cpu": "ppc64" }, "sha512-ZlFgw46NOAGMgcdvdYwAGu2Q+SLFA9LzbJLW+iyMOJyhj5wk6P3KEE9Gct4xWwSzFoPI7JCdYmYMzVtlgQ+zfw=="], - "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q=="], + "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0-rc.11", "", { "os": "linux", "cpu": "s390x" }, "sha512-hIOYmuT6ofM4K04XAZd3OzMySEO4K0/nc9+jmNcxNAxRi6c5UWpqfw3KMFV4MVFWL+jQsSh+bGw2VqmaPMTLyw=="], - "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA=="], + "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.11", "", { "os": "linux", "cpu": "x64" }, "sha512-qXBQQO9OvkjjQPLdUVr7Nr2t3QTZI7s4KZtfw7HzBgjbmAPSFwSv4rmET9lLSgq3rH/ndA3ngv3Qb8l2njoPNA=="], - "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA=="], + "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.11", "", { "os": "linux", "cpu": "x64" }, "sha512-/tpFfoSTzUkH9LPY+cYbqZBDyyX62w5fICq9qzsHLL8uTI6BHip3Q9Uzft0wylk/i8OOwKik8OxW+QAhDmzwmg=="], - "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg=="], + "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.11", "", { "os": "none", "cpu": "arm64" }, "sha512-mcp3Rio2w72IvdZG0oQ4bM2c2oumtwHfUfKncUM6zGgz0KgPz4YmDPQfnXEiY5t3+KD/i8HG2rOB/LxdmieK2g=="], - "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg=="], + "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.11", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.1.1" }, "cpu": "none" }, "sha512-LXk5Hii1Ph9asuGRjBuz8TUxdc1lWzB7nyfdoRgI0WGPZKmCxvlKk8KfYysqtr4MfGElu/f/pEQRh8fcEgkrWw=="], - "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.59.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w=="], + "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-dDwf5otnx0XgRY1yqxOC4ITizcdzS/8cQ3goOWv3jFAo4F+xQYni+hnMuO6+LssHHdJW7+OCVL3CoU4ycnh35Q=="], - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg=="], + "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.11", "", { "os": "win32", "cpu": "x64" }, "sha512-LN4/skhSggybX71ews7dAj6r2geaMJfm3kMbK2KhFMg9B10AZXnKoLCVVgzhMHL0S+aKtr4p8QbAW8k+w95bAA=="], - "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg=="], - - "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.59.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ=="], - - "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.59.0", "", { "os": "none", "cpu": "arm64" }, "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA=="], - - "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.59.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A=="], - - "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.59.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA=="], - - "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA=="], - - "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA=="], + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.11", "", {}, "sha512-xQO9vbwBecJRv9EUcQ/y0dzSTJgA7Q6UVN7xp6B81+tBGSLVAK03yJ9NkJaUA7JFD91kbjxRSC/mDnmvXzbHoQ=="], "@shopify/cli-kit": ["@shopify/cli-kit@3.91.0", "", { "dependencies": { "@apidevtools/json-schema-ref-parser": "11.7.3", "@bugsnag/js": "8.6.0", "@graphql-typed-document-node/core": "3.2.0", "@iarna/toml": "2.2.5", "@oclif/core": "4.5.3", "@opentelemetry/api": "1.9.0", "@opentelemetry/core": "1.30.0", "@opentelemetry/exporter-metrics-otlp-http": "0.57.0", "@opentelemetry/resources": "1.30.0", "@opentelemetry/sdk-metrics": "1.30.0", "@opentelemetry/semantic-conventions": "1.28.0", "@types/archiver": "5.3.2", "ajv": "8.17.1", "ansi-escapes": "6.2.1", "archiver": "5.3.2", "bottleneck": "2.19.5", "brotli": "1.3.3", "chalk": "5.4.1", "change-case": "4.1.2", "color-json": "3.0.5", "commondir": "1.0.1", "conf": "11.0.2", "deepmerge": "4.3.1", "del": "6.1.1", "dotenv": "16.4.7", "env-paths": "3.0.0", "execa": "7.2.0", "fast-glob": "3.3.3", "figures": "5.0.0", "find-up": "6.3.0", "form-data": "4.0.4", "fs-extra": "11.1.0", "get-port-please": "3.1.2", "gradient-string": "2.0.2", "graphql": "16.10.0", "graphql-request": "6.1.0", "ignore": "6.0.2", "ink": "5.2.1", "is-executable": "2.0.1", "is-interactive": "2.0.0", "is-wsl": "3.1.0", "jose": "5.9.6", "latest-version": "7.0.0", "liquidjs": "10.20.1", "lodash": "4.17.23", "macaddress": "0.5.3", "minimatch": "9.0.5", "mrmime": "1.0.1", "network-interfaces": "1.1.0", "node-abort-controller": "3.1.1", "node-fetch": "3.3.2", "open": "8.4.2", "pathe": "1.1.2", "react": "^18.2.0", "semver": "7.6.3", "simple-git": "3.27.0", "stacktracey": "2.1.8", "strip-ansi": "7.1.0", "supports-hyperlinks": "3.1.0", "tempy": "3.1.0", "terminal-link": "3.0.0", "ts-error": "1.0.6", "which": "4.0.0", "zod": "3.24.1" }, "os": [ "linux", "win32", "darwin", ] }, "sha512-s+nWuxTdkY8NHPOJi6m7QYaeKR3Yd8+99ojFRHhh3O32Y1WtkDPWPWjJG5eTjx4bc0Xh/ih2ky8ivoYphxkBhQ=="], @@ -216,39 +156,39 @@ "@szmarczak/http-timer": ["@szmarczak/http-timer@5.0.1", "", { "dependencies": { "defer-to-connect": "^2.0.1" } }, "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw=="], - "@tailwindcss/node": ["@tailwindcss/node@4.2.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.31.1", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.1" } }, "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg=="], + "@tailwindcss/node": ["@tailwindcss/node@4.2.2", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.2" } }, "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA=="], - "@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.1", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.1", "@tailwindcss/oxide-darwin-arm64": "4.2.1", "@tailwindcss/oxide-darwin-x64": "4.2.1", "@tailwindcss/oxide-freebsd-x64": "4.2.1", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1", "@tailwindcss/oxide-linux-arm64-musl": "4.2.1", "@tailwindcss/oxide-linux-x64-gnu": "4.2.1", "@tailwindcss/oxide-linux-x64-musl": "4.2.1", "@tailwindcss/oxide-wasm32-wasi": "4.2.1", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1", "@tailwindcss/oxide-win32-x64-msvc": "4.2.1" } }, "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw=="], + "@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.2", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.2", "@tailwindcss/oxide-darwin-arm64": "4.2.2", "@tailwindcss/oxide-darwin-x64": "4.2.2", "@tailwindcss/oxide-freebsd-x64": "4.2.2", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2", "@tailwindcss/oxide-linux-arm64-musl": "4.2.2", "@tailwindcss/oxide-linux-x64-gnu": "4.2.2", "@tailwindcss/oxide-linux-x64-musl": "4.2.2", "@tailwindcss/oxide-wasm32-wasi": "4.2.2", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2", "@tailwindcss/oxide-win32-x64-msvc": "4.2.2" } }, "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg=="], - "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.2.1", "", { "os": "android", "cpu": "arm64" }, "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg=="], + "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.2.2", "", { "os": "android", "cpu": "arm64" }, "sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg=="], - "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.2.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw=="], + "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.2.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg=="], - "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.2.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw=="], + "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.2.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw=="], - "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.2.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA=="], + "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.2.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ=="], - "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1", "", { "os": "linux", "cpu": "arm" }, "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw=="], + "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.2.2", "", { "os": "linux", "cpu": "arm" }, "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ=="], - "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ=="], + "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.2.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw=="], - "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ=="], + "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.2.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag=="], - "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g=="], + "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.2.2", "", { "os": "linux", "cpu": "x64" }, "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg=="], - "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g=="], + "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.2.2", "", { "os": "linux", "cpu": "x64" }, "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ=="], - "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.2.1", "", { "dependencies": { "@emnapi/core": "^1.8.1", "@emnapi/runtime": "^1.8.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.1", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q=="], + "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.2.2", "", { "dependencies": { "@emnapi/core": "^1.8.1", "@emnapi/runtime": "^1.8.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.1", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q=="], - "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.2.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA=="], + "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.2.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ=="], - "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.2.1", "", { "os": "win32", "cpu": "x64" }, "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ=="], + "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.2.2", "", { "os": "win32", "cpu": "x64" }, "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA=="], - "@tailwindcss/vite": ["@tailwindcss/vite@4.2.1", "", { "dependencies": { "@tailwindcss/node": "4.2.1", "@tailwindcss/oxide": "4.2.1", "tailwindcss": "4.2.1" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-TBf2sJjYeb28jD2U/OhwdW0bbOsxkWPwQ7SrqGf9sVcoYwZj7rkXljroBO9wKBut9XnmQLXanuDUeqQK0lGg/w=="], + "@tailwindcss/vite": ["@tailwindcss/vite@4.2.2", "", { "dependencies": { "@tailwindcss/node": "4.2.2", "@tailwindcss/oxide": "4.2.2", "tailwindcss": "4.2.2" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w=="], - "@types/archiver": ["@types/archiver@5.3.2", "", { "dependencies": { "@types/readdir-glob": "*" } }, "sha512-IctHreBuWE5dvBDz/0WeKtyVKVRs4h75IblxOACL92wU66v+HGAfEYAOyXkOFphvRJMhuXdI9huDXpX0FC6lCw=="], + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], - "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + "@types/archiver": ["@types/archiver@5.3.2", "", { "dependencies": { "@types/readdir-glob": "*" } }, "sha512-IctHreBuWE5dvBDz/0WeKtyVKVRs4h75IblxOACL92wU66v+HGAfEYAOyXkOFphvRJMhuXdI9huDXpX0FC6lCw=="], "@types/http-cache-semantics": ["@types/http-cache-semantics@4.2.0", "", {}, "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q=="], @@ -458,8 +398,6 @@ "es-toolkit": ["es-toolkit@1.45.1", "", {}, "sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw=="], - "esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="], - "escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], "execa": ["execa@7.2.0", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.1", "human-signals": "^4.3.0", "is-stream": "^3.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^5.1.0", "onetime": "^6.0.0", "signal-exit": "^3.0.7", "strip-final-newline": "^3.0.0" } }, "sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA=="], @@ -690,29 +628,29 @@ "lazystream": ["lazystream@1.0.1", "", { "dependencies": { "readable-stream": "^2.0.5" } }, "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw=="], - "lightningcss": ["lightningcss@1.31.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.31.1", "lightningcss-darwin-arm64": "1.31.1", "lightningcss-darwin-x64": "1.31.1", "lightningcss-freebsd-x64": "1.31.1", "lightningcss-linux-arm-gnueabihf": "1.31.1", "lightningcss-linux-arm64-gnu": "1.31.1", "lightningcss-linux-arm64-musl": "1.31.1", "lightningcss-linux-x64-gnu": "1.31.1", "lightningcss-linux-x64-musl": "1.31.1", "lightningcss-win32-arm64-msvc": "1.31.1", "lightningcss-win32-x64-msvc": "1.31.1" } }, "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ=="], + "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], - "lightningcss-android-arm64": ["lightningcss-android-arm64@1.31.1", "", { "os": "android", "cpu": "arm64" }, "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg=="], + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], - "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.31.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg=="], + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], - "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.31.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA=="], + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="], - "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.31.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A=="], + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="], - "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.31.1", "", { "os": "linux", "cpu": "arm" }, "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g=="], + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="], - "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg=="], + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="], - "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg=="], + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="], - "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA=="], + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="], - "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA=="], + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="], - "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.31.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w=="], + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="], - "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.31.1", "", { "os": "win32", "cpu": "x64" }, "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw=="], + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], @@ -902,7 +840,7 @@ "rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="], - "rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="], + "rolldown": ["rolldown@1.0.0-rc.11", "", { "dependencies": { "@oxc-project/types": "=0.122.0", "@rolldown/pluginutils": "1.0.0-rc.11" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.11", "@rolldown/binding-darwin-arm64": "1.0.0-rc.11", "@rolldown/binding-darwin-x64": "1.0.0-rc.11", "@rolldown/binding-freebsd-x64": "1.0.0-rc.11", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.11", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.11", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.11", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.11", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.11", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.11", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.11", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.11", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.11", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.11", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.11" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-NRjoKMusSjfRbSYiH3VSumlkgFe7kYAa3pzVOsVYVFY3zb5d7nS+a3KGQ7hJKXuYWbzJKPVQ9Wxq2UvyK+ENpw=="], "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], @@ -1002,7 +940,7 @@ "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], - "tailwindcss": ["tailwindcss@4.2.1", "", {}, "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw=="], + "tailwindcss": ["tailwindcss@4.2.2", "", {}, "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q=="], "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], @@ -1056,9 +994,9 @@ "validate-npm-package-license": ["validate-npm-package-license@3.0.4", "", { "dependencies": { "spdx-correct": "^3.0.0", "spdx-expression-parse": "^3.0.0" } }, "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew=="], - "vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="], + "vite": ["vite@8.0.2", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.3", "postcss": "^8.5.8", "rolldown": "1.0.0-rc.11", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.0", "esbuild": "^0.27.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-1gFhNi+bHhRE/qKZOJXACm6tX4bA3Isy9KuKF15AgSRuRazNBOJfdDemPBU16/mpMxApDPrWvZ08DcLPEoRnuA=="], - "vite-plugin-shopify": ["vite-plugin-shopify@4.1.1", "", { "dependencies": { "@shopify/cli-kit": "^3.88.1", "@shopify/plugin-cloudflare": "^3.88.1", "debug": "^4.3.4", "fast-glob": "^3.2.11" }, "peerDependencies": { "vite": ">=5.4.12 <6.0.0 || >=6.0.9 || ^7.0.0" } }, "sha512-4kyh6NUjxb6hvTnXXqnXp2nVpwkvz5Kh4nFwhZfQ1fis3/b8k5KU0l6w8dmgU/zQcwmhJeFvfpzHoP/nBONmuw=="], + "vite-plugin-shopify": ["vite-plugin-shopify@4.1.2", "", { "dependencies": { "@shopify/cli-kit": "^3.88.1", "@shopify/plugin-cloudflare": "^3.88.1", "debug": "^4.3.4", "fast-glob": "^3.2.11" }, "peerDependencies": { "vite": ">=5.4.12 <6.0.0 || >=6.0.9 <7.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-fQ5XJ1WTI5KpA6pwkqnKZ0PiUq7yliqmJtQBZ7AQ5cZbM5sugc6Nn4gAS+xvy2O2Vs13TSUU4DdUPH1w7jmULA=="], "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], diff --git a/package.json b/package.json index 0571eccba..158c5c466 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,11 @@ { "devDependencies": { - "@tailwindcss/vite": "^4.2.1", + "@tailwindcss/vite": "^4.2.2", "npm-run-all": "^4.1.5", - "tailwindcss": "^4.2.1", - "typescript": "~5.9.3", - "vite": "^7.3.1", - "vite-plugin-shopify": "^4.1.1" + "tailwindcss": "^4.2.2", + "typescript": "^5.9.3", + "vite": "^8.0.2", + "vite-plugin-shopify": "^4.1.2" }, "type": "module", "scripts": { diff --git a/snippets/vite-tag.liquid b/snippets/vite-tag.liquid index c9cd99e7e..9a66579f4 100644 --- a/snippets/vite-tag.liquid +++ b/snippets/vite-tag.liquid @@ -6,28 +6,81 @@ assign entry = entry | default: vite-tag assign path = entry | replace: '@ts/', 'ts/' | replace: '@css/', 'css/' | replace: '~/', '../' | replace: '@/', '../' %} -{% liquid - assign path_prefix = path | slice: 0 - if path_prefix == '/' - assign file_url_prefix = 'http://localhost:5173' - else - assign file_url_prefix = 'http://localhost:5173/frontend/entrypoints/' - endif - assign file_url = path | prepend: file_url_prefix - assign file_name = path | split: '/' | last - if file_name contains '.' - assign file_extension = file_name | split: '.' | last - endif - assign css_extensions = 'css|less|sass|scss|styl|stylus|pcss|postcss' | split: '|' - assign is_css = false - if css_extensions contains file_extension - assign is_css = true - endif -%} - - -{% if is_css == true %} - -{% else %} - +{% if path == "/frontend/entrypoints/css/main.css" or path == "css/main.css" %} + {{ 'main-JmcnsjPw.css' | asset_url | split: '?' | first | stylesheet_tag: preload: preload_stylesheet }} +{% elsif path == "/frontend/entrypoints/ts/404.ts" or path == "ts/404.ts" %} + +{% elsif path == "/frontend/entrypoints/ts/article.ts" or path == "ts/article.ts" %} + +{% elsif path == "/frontend/entrypoints/ts/blog.ts" or path == "ts/blog.ts" %} + +{% elsif path == "/frontend/entrypoints/ts/cart.ts" or path == "ts/cart.ts" %} + + + +{% elsif path == "/frontend/entrypoints/ts/cart/drawer.ts" or path == "ts/cart/drawer.ts" %} + + + + +{% elsif path == "/frontend/entrypoints/ts/cart/page.ts" or path == "ts/cart/page.ts" %} + + + +{% elsif path == "/frontend/entrypoints/ts/collection.ts" or path == "ts/collection.ts" %} + + + +{% elsif path == "/frontend/entrypoints/ts/gift-card.ts" or path == "ts/gift-card.ts" %} + +{% elsif path == "/frontend/entrypoints/ts/index.ts" or path == "ts/index.ts" %} + +{% elsif path == "/frontend/entrypoints/ts/list-collections.ts" or path == "ts/list-collections.ts" %} + +{% elsif path == "/frontend/entrypoints/ts/page.ts" or path == "ts/page.ts" %} + +{% elsif path == "/frontend/entrypoints/ts/password.ts" or path == "ts/password.ts" %} + +{% elsif path == "/frontend/entrypoints/ts/product.ts" or path == "ts/product.ts" %} + + + + + + + + + +{% elsif path == "/frontend/entrypoints/ts/product/handlers.ts" or path == "ts/product/handlers.ts" %} + + + + + + + + +{% elsif path == "/frontend/entrypoints/ts/product/recommendations.ts" or path == "ts/product/recommendations.ts" %} + +{% elsif path == "/frontend/entrypoints/ts/product/state.ts" or path == "ts/product/state.ts" %} + +{% elsif path == "/frontend/entrypoints/ts/product/sync.ts" or path == "ts/product/sync.ts" %} + + + + +{% elsif path == "/frontend/entrypoints/ts/search.ts" or path == "ts/search.ts" %} + +{% elsif path == "/frontend/entrypoints/ts/search/drawer.ts" or path == "ts/search/drawer.ts" %} + +{% elsif path == "/frontend/entrypoints/ts/theme.ts" or path == "ts/theme.ts" %} + +{% elsif path == "/frontend/entrypoints/ts/utils/cart-events.ts" or path == "ts/utils/cart-events.ts" %} + +{% elsif path == "/frontend/entrypoints/ts/utils/cart.ts" or path == "ts/utils/cart.ts" %} + +{% elsif path == "/frontend/entrypoints/ts/utils/section-rendering.ts" or path == "ts/utils/section-rendering.ts" %} + +{% elsif path == "/frontend/entrypoints/ts/utils/variant-picker.ts" or path == "ts/utils/variant-picker.ts" %} + {% endif %}