From ad63c600359513d023cac9df6909e4bbc30cf927 Mon Sep 17 00:00:00 2001 From: Yotam Faraggi Date: Tue, 15 Sep 2026 02:03:10 +0200 Subject: [PATCH 01/16] docs: define Shopify 2026 platform modernization plan --- docs/SHOPIFY_2026_MODERNIZATION.md | 404 +++++++++++++++++++++++++++++ 1 file changed, 404 insertions(+) create mode 100644 docs/SHOPIFY_2026_MODERNIZATION.md diff --git a/docs/SHOPIFY_2026_MODERNIZATION.md b/docs/SHOPIFY_2026_MODERNIZATION.md new file mode 100644 index 0000000..6ced34f --- /dev/null +++ b/docs/SHOPIFY_2026_MODERNIZATION.md @@ -0,0 +1,404 @@ +# Shopify 2026 platform modernization + +Liquid Loom's core architecture still fits Shopify theme development in 2026: it keeps Liquid and Shopify CLI authoritative, produces a canonical Shopify theme, makes output ownership explicit, and lets established themes adopt tooling without an up-front rewrite. + +The next modernization step is therefore not a framework rewrite. It is a change in **defaults and reference architecture** so that fresh Liquid Loom projects demonstrate Shopify's strongest current theme primitives instead of treating Vite + Tailwind as the center of the product. + +This plan builds on the `0.3` project-intelligence work. The deterministic project model, `explain`, explicit `shopifyLiquidMode`, and preview-aware diagnostics remain part of the foundation. + +## Decision + +Liquid Loom should be described as: + +> A deterministic engineering layer for Shopify Liquid themes: safer source architecture, incremental adoption, agent-readable project understanding, modern Shopify-native composition, and optional advanced asset tooling. + +Vite and Tailwind stay supported. They stop being the default explanation for why Liquid Loom exists. + +## Core invariants that do not change + +1. Liquid remains the storefront runtime. +2. Shopify CLI remains the canonical preview/deploy tool. +3. `dist/theme` remains a normal, inspectable Shopify theme. +4. Every deployable Shopify path has exactly one source owner. +5. Ownership is planned and validated before publish. +6. Failed builds never replace the last-known-good output. +7. Existing themes can adopt Liquid Loom without moving source. +8. Migration remains preview-first and preserves Shopify output identity. +9. Existing Sass/PostCSS/Webpack/Vite/Tailwind pipelines remain valid for adopted themes. +10. The project model reports provable relationships rather than evaluating or guessing Liquid. +11. Developer-preview Shopify syntax never becomes a stable requirement implicitly. + +## Target architecture + +```text +authored Shopify/Liquid source + | + +-- native component assets + +-- theme blocks / app blocks + +-- optional Vite or Tailwind assets + +-- explicit stable/preview Liquid mode + | + v +Liquid Loom + | + +-- ownership plan + +-- project intelligence + +-- diagnostics + +-- transactional build + +-- migration safety + | + v +canonical Shopify theme + | + +-- Shopify CLI preview/push + +-- compiled GitHub deployment branch/repo +``` + +## 1. Shopify-native component assets become the fresh-project default + +Shopify supports `{% stylesheet %}` and `{% javascript %}` in sections, blocks, and snippets. Shopify now subsets stylesheet-tag CSS by the current page render tree, making component-local CSS a real platform feature rather than just a source-organization convention. + +### Target + +Fresh projects should prefer component-local assets for component-local behavior: + +```liquid +{% stylesheet %} +.product-card { ... } +{% endstylesheet %} + +{% javascript %} +customElements.define(...) +{% endjavascript %} +``` + +Global CSS should remain only for genuinely global concerns such as base styles, typography, design tokens, shared layout primitives, accessibility defaults, and intentionally global utilities. + +### Vite and Tailwind + +Vite and Tailwind remain first-class supported integrations, but they become explicit asset strategies instead of the architectural default. + +A future scaffolder can expose a small choice such as: + +```text +native Shopify-native component assets + minimal global assets +vite Vite for projects that need bundling/modules + tailwind Tailwind + Vite for teams that intentionally choose it +``` + +Do not introduce a generic asset plugin API merely to support these three paths. + +### Acceptance criteria + +- a default fresh scaffold builds without requiring a storefront Vite bundle; +- component CSS/JS is colocated with its Liquid component where practical; +- global assets are intentionally small and documented; +- Vite and Tailwind remain tested supported paths; +- existing-theme `init` continues to preserve the current asset pipeline. + +Reference: + +## 2. Theme blocks become the primary reference composition model + +Liquid Loom already understands root `blocks/*.liquid`, block references, app blocks, and preview block relationships. The reference storefront should now demonstrate that model deeply rather than treating blocks as a small flexible-content example. + +### Product page target + +Move the product page toward merchant-reorderable components: + +```text +Main product +| ++-- Product media ++-- Product information group +| +-- Vendor +| +-- Title +| +-- Price +| +-- @app ++-- Variant picker ++-- Purchase group +| +-- Quantity +| +-- Buy buttons +| +-- @app ++-- Description +``` + +The decomposition should stay intentionally small and Shopify-native. Liquid Loom should not invent its own component runtime or schema abstraction. + +### Acceptance criteria + +- merchants can reorder the supported product information blocks in Theme Editor; +- expected surfaces accept `@app` blocks; +- reusable content primitives are theme blocks where reuse creates merchant value; +- source continues to map through the existing organized-source ownership contract; +- no-JavaScript fallbacks and accessible markup remain part of the reference theme. + +## 3. Standard storefront events and actions + +Shopify now provides a standard communication layer between Liquid storefronts, apps, and agents. The reference storefront should speak that protocol rather than forcing integrations to inspect custom DOM or intercept requests. + +### Stable scope + +Implement the relevant documented events for interactions owned by the reference storefront, including appropriate page, product, collection, search, and cart events. + +Use Shopify standard actions as the integration contract. Override an action only when the reference theme can provide a better local UI than Shopify's safe default behavior. + +These events are not an analytics system. Analytics stays on Shopify Web Pixels. + +### Acceptance criteria + +- events follow Shopify's documented payload contracts; +- action overrides retain Shopify-compatible fallback behavior; +- integrations do not depend on undocumented DOM conventions or global `fetch` interception; +- event/action behavior is included in real-store/browser smoke coverage. + +References: + +- +- +- + +## 4. Adopt `color_palette` for the reference design-token model + +Shopify recommends `color_palette` for new themes and is focusing future color-system development there. + +### Target + +The reference theme should define one semantic palette in `settings_schema.json`, for example: + +```json +{ + "type": "color_palette", + "id": "colors", + "default": { + "background": "#F8F7F2", + "foreground": "#111820", + "surface": "#FFFFFF", + "accent": "#176C5B", + "accent_contrast": "#FFFFFF" + } +} +``` + +Section/block color controls can reference palette entries as defaults while remaining locally overrideable. + +CSS custom properties remain the runtime design-token bridge. This keeps the rendered theme inspectable and works equally well with native CSS, Vite, or Tailwind. + +Reference: + +## 5. Modern customer accounts + +The reference header should use Shopify's `` storefront component whenever customer accounts are enabled. + +### Target + +- render the component in a location available on desktop and mobile; +- expose the menu through a merchant setting with `customer-account-main-menu` as the default; +- customize only through supported CSS variables, parts, and slots; +- render nothing account-specific when customer accounts are disabled. + +Reference: + +## 6. First-class compiled GitHub deployment workflow + +Liquid Loom repositories can contain authored `src`, tests, package metadata, configuration, and build tooling. Shopify's GitHub theme integration only connects correctly to a branch that already has canonical Shopify theme structure. + +That constraint should become a supported Liquid Loom workflow instead of tribal knowledge. + +### Target model + +```text +main / feature branches +| ++-- authored source ++-- Liquid Loom config ++-- tests and tooling +| ++-- CI: validate + build + | + v +shopify-production branch (or deployment repo) +| ++-- assets/ ++-- blocks/ ++-- config/ ++-- layout/ ++-- locales/ ++-- sections/ ++-- snippets/ ++-- templates/ + | + v +Shopify GitHub integration +``` + +### Deliverables + +- documented deployment-branch and deployment-repository strategies; +- an opt-in generated GitHub Actions workflow or maintained recipe; +- validation before publishing compiled output; +- loop protection for Shopify-originated commits on a connected branch; +- explicit guidance for teams that allow Theme Editor/code-editor changes to write back into GitHub; +- no hosted Liquid Loom deployment service. + +Reference: + +## 7. Shopify CLI 4.8+ development-store workflow + +Shopify CLI 4.8 added `shopify store` commands for creating, listing, inspecting, and deleting dev stores. Liquid Loom should align its real-store validation guidance with that workflow. + +### Target + +Document and optionally script an opt-in flow such as: + +```text +shopify store create dev --demo-data + | + v +liquid-loom build + | + v +shopify theme dev --path dist/theme + | + v +browser smoke + | + v +shopify store delete +``` + +Public CI must continue to work without Shopify credentials. Real-store automation belongs in protected/opt-in workflows. + +Reference: + +## 8. Agent-friendly contracts build on `0.3`, not around Shopify + +PR #25 added the deterministic project model, `explain`, JSON output, preview-aware relationships, and explicit `shopifyLiquidMode`. The modernization should extend that strategy rather than create a separate agent framework. + +### Target + +- keep `doctor`, `check`, and `explain` deterministic and script-friendly; +- ensure new block/component/deployment concepts appear truthfully in the project model where statically provable; +- add architectural diff/report features only if real agent or CI workflows demonstrate the need; +- prefer standard Shopify primitives so agents can use Shopify's own documentation and AI tooling; +- keep generated deployment output visibly disposable and authored source visibly authoritative. + +A Liquid Loom-specific AI runtime or agent SDK is out of scope. + +## 9. July 2026 Liquid preview remains explicit and experimental + +Liquid Loom already supports explicit `stable` and `july-2026-preview` project modes and can report preview block/partial usage. + +That is the correct policy for the modernization release. + +### Policy + +- do not scaffold `{% block %}` or `{% partial %}` by default; +- do not make preview syntax a stable build requirement; +- continue allowing a project to declare preview intent through `shopifyLiquidMode`; +- keep preview relationships visible to `explain` and diagnostics; +- validate preview recipes separately from stable release gates; +- reconsider the default only after Shopify publishes a stable contract. + +Reference: + +## 10. Existing-theme adoption must remain boring and safe + +None of the platform modernization should force an established theme to refactor. + +For existing themes: + +- `liquid-loom init` still moves zero Shopify source files; +- current Vite/Tailwind/Sass/PostCSS/Webpack pipelines remain valid; +- native component asset tags can be adopted file-by-file; +- palettes remain optional; +- block refactors are never automatic; +- standard event/action integration is opt-in per interaction the project owns; +- migration continues to preserve output identity; +- stable mode remains the default unless preview intent is explicit. + +This compatibility rule is non-negotiable. + +## Implementation order for one modernization PR + +The implementation should land as one coherent PR, but be built internally in dependency order so every commit remains understandable: + +1. update platform-alignment docs, roadmap, and acceptance criteria; +2. update the supported Shopify CLI development-tool floor; +3. modernize palette/design-token and customer-account primitives; +4. refactor the reference storefront toward theme-block-first composition; +5. move appropriate component CSS/JS to Shopify-native tags; +6. preserve Vite/Tailwind as explicit tested asset paths; +7. implement standard storefront events/actions; +8. add compiled GitHub deployment workflow/recipe; +9. update project intelligence where new static relationships need representation; +10. expand tests, package smoke, and opt-in real-store/browser validation; +11. keep July-preview validation separate from the stable release gate. + +## Compatibility rules for that PR + +The implementation must not: + +- change the deployable Shopify path for existing organized source; +- invalidate `0.2`/`0.3` projects merely because they use Vite or Tailwind; +- require migration in adopted themes; +- require Shopify credentials for normal unit/package CI; +- depend on preview-only Liquid syntax for stable fixtures; +- change source ownership precedence silently; +- introduce a second storefront runtime; +- hide the generated canonical Shopify theme; +- regress the deterministic project model or `explain --json` contract without an explicit versioning decision. + +Any unavoidable breaking public API change requires an explicit migration note before merge. + +## Validation matrix + +### Public/local automated validation + +- default fresh scaffold builds and passes Theme Check; +- optional Vite path builds and participates in generated-output collision checks; +- optional Tailwind path builds and participates in generated-output collision checks; +- reference and embedded starter remain synchronized; +- existing-theme `init -> hybrid build -> migrate -> build` remains green; +- packed-package smoke exercises the actual published artifact shape; +- native `{% stylesheet %}` / `{% javascript %}` content survives organized-source mapping unchanged; +- `explain --json` remains deterministic across repeated runs; +- stable fixtures require no Shopify developer preview. + +### Real Shopify validation + +- fresh scaffold previews against a dev store; +- product, cart, collection, and search flows work in the browser; +- required no-JavaScript fallbacks remain usable; +- product blocks can be reordered in Theme Editor; +- app blocks work in the intended surfaces; +- `` works when accounts are enabled and disappears cleanly when disabled; +- palette edits propagate through semantic CSS tokens; +- standard storefront event payloads match Shopify contracts; +- the compiled deployment branch/repo is accepted by Shopify GitHub integration; +- Shopify-originated writes do not cause deployment loops. + +### Adoption validation + +Run the modernization against at least one established client-style theme while leaving its current asset pipeline untouched. The upgrade must provide value without requiring block, palette, native-asset, or preview migrations. + +## Success criteria + +The modernization is successful if a developer can accurately describe the project as: + +> Shopify-native Liquid theme development with deterministic source ownership, safe adoption, agent-readable architecture, and modern platform defaults. + +The next release should feel **more native to Shopify**, not more dependent on Liquid Loom-specific abstractions. + +## Deferred work + +Keep these deferred unless real usage earns them: + +- broad plugin/hook systems; +- arbitrary source/mapping plugins; +- hosted deployment infrastructure; +- headless/Hydrogen support; +- React/Vue/Svelte storefront runtimes; +- a custom analytics layer; +- a Liquid Loom-specific AI agent runtime; +- stable defaults based on developer-preview Liquid syntax. From 9ce818ced4284c3059f674124b117d429d62b295 Mon Sep 17 00:00:00 2001 From: Yotam Faraggi Date: Tue, 15 Sep 2026 02:03:29 +0200 Subject: [PATCH 02/16] docs: sequence Shopify modernization after project intelligence --- ROADMAP.md | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/ROADMAP.md b/ROADMAP.md index 6791d29..315a907 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -73,7 +73,28 @@ Now proving: See [Project model](docs/PROJECT_MODEL.md) and [Shopify Liquid July 2026 developer preview](docs/LIQUID_JULY_2026_PREVIEW.md). -## 0.4 - Extension surface, if earned +## 0.4 - Shopify 2026 platform modernization + +Modernize fresh-project defaults and the reference storefront around Shopify's current native theme capabilities without weakening the `0.2` ownership/migration guarantees or the `0.3` project-intelligence model. + +Planned: + +- [ ] make Shopify-native `{% stylesheet %}` / `{% javascript %}` component assets the preferred fresh-project path where practical; +- [ ] keep Vite and Tailwind as explicit, tested optional asset strategies rather than Liquid Loom's product identity; +- [ ] make theme blocks and merchant-reorderable composition central to the reference storefront, especially product information and purchase surfaces; +- [ ] implement relevant Shopify standard storefront events/actions in the reference theme; +- [ ] move the reference color system to `color_palette` plus semantic CSS custom properties; +- [ ] adopt the `` component with a merchant-configurable account menu; +- [ ] add a documented/generated compiled branch or deployment-repository workflow for Shopify GitHub integration; +- [ ] align opt-in real-store testing with Shopify CLI 4.8+ dev-store commands; +- [ ] extend project intelligence only where new block/component/deployment relationships are statically provable; +- [ ] preserve explicit `stable` / `july-2026-preview` modes and keep preview syntax outside the stable release gate; +- [ ] preserve zero-migration adoption and existing asset pipelines for established themes; +- [ ] expand package, Theme Check, browser, Theme Editor, and deployment-loop validation around the new defaults. + +See [Shopify 2026 platform modernization](docs/SHOPIFY_2026_MODERNIZATION.md) for the full architecture decisions, compatibility constraints, implementation order, and acceptance criteria. + +## 0.5 - Extension surface, if earned Only after repeated consumer needs justify a public extension API: @@ -97,5 +118,6 @@ Only after repeated consumer needs justify a public extension API: - Requiring migration before an existing theme can receive value from Liquid Loom. - Guessing dynamic Liquid relationships in order to make the project model look more complete. - Making developer-preview Liquid syntax the default before Shopify stabilizes it. +- Introducing a second storefront runtime to imitate headless frameworks. See [Validation](docs/VALIDATION.md) for the evidence expected before broadening the public API. From cb0c343016aca3b35487ccbefae0ce3922e44da7 Mon Sep 17 00:00:00 2001 From: Yotam Faraggi Date: Tue, 15 Sep 2026 02:14:07 +0200 Subject: [PATCH 03/16] Modernize starter for Shopify 2026 platform primitives --- docs/GITHUB_DEPLOYMENT.md | 62 ++ docs/RECIPES.md | 40 +- docs/examples/shopify-deploy.yml | 61 ++ liquid-loom.config.ts | 1 + .../create-liquid-loom/template/README.md | 4 +- .../template/liquid-loom.config.ts | 1 + .../create-liquid-loom/template/package.json | 9 +- .../template/src/entrypoints/theme.js | 118 --- .../template/src/public/theme.css | 86 +++ .../template/src/styles/theme.css | 705 ------------------ .../theme/blocks/product-description.liquid | 2 + .../src/theme/blocks/product-price.liquid | 2 + .../src/theme/blocks/product-purchase.liquid | 4 + .../src/theme/blocks/product-title.liquid | 2 + .../src/theme/blocks/product-vendor.liquid | 2 + .../src/theme/config/settings_data.json | 20 +- .../src/theme/config/settings_schema.json | 21 +- .../template/src/theme/layout/theme.liquid | 30 +- .../src/theme/sections/cart/main-cart.liquid | 66 +- .../collections/main-collection.liquid | 115 +-- .../src/theme/sections/global/header.liquid | 61 +- .../sections/home/featured-collection.liquid | 66 +- .../sections/products/main-product.liquid | 103 +-- .../theme/sections/search/main-search.liquid | 59 +- .../template/src/theme/templates/product.json | 10 +- .../template/vite.config.js | 31 - src/entrypoints/theme.js | 118 --- src/{styles => public}/theme.css | 240 +----- src/theme/blocks/product-description.liquid | 2 + src/theme/blocks/product-price.liquid | 2 + src/theme/blocks/product-purchase.liquid | 88 +++ src/theme/blocks/product-title.liquid | 2 + src/theme/blocks/product-vendor.liquid | 2 + src/theme/config/settings_data.json | 10 - src/theme/config/settings_schema.json | 18 +- src/theme/layout/theme.liquid | 26 +- src/theme/sections/cart/main-cart.liquid | 66 +- .../collections/main-collection.liquid | 113 +-- src/theme/sections/global/header.liquid | 76 +- .../sections/home/featured-collection.liquid | 65 +- .../sections/products/main-product.liquid | 120 +-- src/theme/sections/search/main-search.liquid | 79 +- src/theme/templates/product.json | 8 + tests/template-sync.test.js | 29 +- tests/theme-capabilities.test.js | 24 +- 45 files changed, 586 insertions(+), 2183 deletions(-) create mode 100644 docs/GITHUB_DEPLOYMENT.md create mode 100644 docs/examples/shopify-deploy.yml delete mode 100644 packages/create-liquid-loom/template/src/entrypoints/theme.js create mode 100644 packages/create-liquid-loom/template/src/public/theme.css delete mode 100644 packages/create-liquid-loom/template/src/styles/theme.css create mode 100644 packages/create-liquid-loom/template/src/theme/blocks/product-description.liquid create mode 100644 packages/create-liquid-loom/template/src/theme/blocks/product-price.liquid create mode 100644 packages/create-liquid-loom/template/src/theme/blocks/product-purchase.liquid create mode 100644 packages/create-liquid-loom/template/src/theme/blocks/product-title.liquid create mode 100644 packages/create-liquid-loom/template/src/theme/blocks/product-vendor.liquid delete mode 100644 packages/create-liquid-loom/template/vite.config.js delete mode 100644 src/entrypoints/theme.js rename src/{styles => public}/theme.css (70%) create mode 100644 src/theme/blocks/product-description.liquid create mode 100644 src/theme/blocks/product-price.liquid create mode 100644 src/theme/blocks/product-purchase.liquid create mode 100644 src/theme/blocks/product-title.liquid create mode 100644 src/theme/blocks/product-vendor.liquid diff --git a/docs/GITHUB_DEPLOYMENT.md b/docs/GITHUB_DEPLOYMENT.md new file mode 100644 index 0000000..34c2f53 --- /dev/null +++ b/docs/GITHUB_DEPLOYMENT.md @@ -0,0 +1,62 @@ +# Shopify GitHub deployment + +Liquid Loom intentionally keeps authored source separate from deployable Shopify output. Shopify's GitHub theme integration, however, expects the connected branch to contain Shopify's canonical theme directories. + +For repositories that use `src`, tests, and build tooling, connect Shopify to a **compiled deployment branch** or a separate deployment repository rather than to the authored branch. + +## Recommended topology + +```text +main +├── src/ +├── tests/ +├── liquid-loom.config.ts +└── package.json + | + | CI: install -> validate -> build + v +shopify-production +├── assets/ +├── blocks/ +├── config/ +├── layout/ +├── locales/ +├── sections/ +├── snippets/ +└── templates/ + | + v +Shopify GitHub integration +``` + +The deployment branch is generated output. `main` remains the source of truth. + +## Safe CI policy + +Use a workflow triggered only by authored branches. Never trigger the build from `shopify-production` itself; that prevents a Shopify-originated commit on the connected branch from creating a deployment loop. + +When publishing: + +1. fetch the current `shopify-production` head; +2. create a worktree from that head so the new deployment is a fast-forward commit; +3. replace only the canonical Shopify theme directories with `dist/theme`; +4. commit only when the compiled output changed; +5. push without `--force`. + +A concurrent Shopify write will make the push fail rather than silently overwrite history. Re-run only after reviewing the branch state. + +See [`docs/examples/shopify-deploy.yml`](examples/shopify-deploy.yml) for an opt-in workflow recipe. + +## Theme Editor writes + +A compiled branch can't automatically translate Theme Editor changes back into organized source. Choose one of these policies explicitly: + +- **Source-authoritative:** Theme Editor changes on the deployment branch are temporary and the next successful source deployment may replace them. Keep history so drift can be reviewed first. +- **Reconciliation workflow:** review Shopify-originated commits and manually port intentional changes into authored source before the next deployment. +- **Separate deployment repository:** use this when you want stronger operational separation between authored source and generated theme output. + +Do not pretend two-way synchronization is lossless when a build step changes source shape. + +## Development stores + +For protected integration CI, Shopify CLI 4.8+ can create a disposable dev store, preview the built theme, run browser smoke tests, and remove the store afterwards. Public CI should continue to run without Shopify credentials. diff --git a/docs/RECIPES.md b/docs/RECIPES.md index bb3ffdb..d1832aa 100644 --- a/docs/RECIPES.md +++ b/docs/RECIPES.md @@ -8,21 +8,51 @@ Place a file anywhere under `src/public/`. Its basename becomes the Shopify asse src/public/icons/cart.svg → dist/theme/assets/cart.svg ``` -Basenames must be unique across the entire public tree and cannot be `theme.js` or `style.css` unless generated-output configuration changes with matching Vite output. +Basenames must be unique across the entire public tree. When Vite is enabled, static assets also cannot collide with any configured generated output. -## Add a section or snippet +## Add component CSS or JavaScript -Organize by feature; Shopify’s destination remains flat: +Fresh Liquid Loom projects prefer Shopify-native component assets: + +```liquid +{% stylesheet %} +.product-card { display: grid; } +{% endstylesheet %} + +{% javascript %} +customElements.define("product-card", class extends HTMLElement {}); +{% endjavascript %} +``` + +Keep truly global styles in a static file such as `src/public/theme.css`. Liquid doesn't render inside `{% stylesheet %}` or `{% javascript %}` tags, so pass dynamic values through markup, data attributes, or CSS custom properties. + +## Add a section, block, or snippet + +Organize by feature; Shopify's destination remains flat: ```text src/theme/sections/editorial/story.liquid → dist/theme/sections/story.liquid +src/theme/blocks/product/title.liquid → dist/theme/blocks/title.liquid ``` Run `pnpm build` immediately after adding the file. Portable collisions fail before the output changes. -## Add another Vite entrypoint +## Opt into Vite or Tailwind + +The fresh starter uses Shopify-native assets by default, but the framework still supports Vite and Tailwind. Add a Vite config and enable it explicitly: + +```ts +export default defineConfig({ + viteConfig: "vite.config.js", + reservedOutputs: ["assets/theme.js", "assets/style.css"] +}); +``` + +Install the asset tooling your project needs and configure stable output names. Generated outputs participate in the same ownership/collision plan as authored Shopify assets. + +## Deploy compiled output through Shopify GitHub integration -Extend `rollupOptions.input` and give its output a stable name in `vite.config.js`. Add that output to `reservedOutputs` in `liquid-loom.config.ts` so a public asset cannot overwrite it. +If your source repository contains `src`, tests, and build tooling, connect Shopify to a generated branch or separate deployment repository that contains only canonical Shopify theme directories. See [GitHub deployment](GITHUB_DEPLOYMENT.md). ## Scan for organization-specific terms diff --git a/docs/examples/shopify-deploy.yml b/docs/examples/shopify-deploy.yml new file mode 100644 index 0000000..7ddf210 --- /dev/null +++ b/docs/examples/shopify-deploy.yml @@ -0,0 +1,61 @@ +name: Build Shopify deployment branch + +on: + workflow_dispatch: + push: + branches: [main] + +permissions: + contents: write + +concurrency: + group: shopify-theme-deployment + cancel-in-progress: false + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/setup-node@v4 + with: + node-version: 24 + cache: pnpm + + - run: corepack enable + - run: pnpm install --frozen-lockfile + - run: pnpm validate + + - name: Prepare compiled branch + shell: bash + run: | + set -euo pipefail + deploy_dir="$RUNNER_TEMP/shopify-production" + if git ls-remote --exit-code --heads origin shopify-production >/dev/null 2>&1; then + git fetch origin shopify-production:refs/remotes/origin/shopify-production + git worktree add --detach "$deploy_dir" origin/shopify-production + else + git worktree add --detach "$deploy_dir" HEAD + find "$deploy_dir" -mindepth 1 -maxdepth 1 ! -name .git -exec rm -rf {} + + fi + + for directory in assets blocks config layout locales sections snippets templates; do + rm -rf "$deploy_dir/$directory" + if [ -d "dist/theme/$directory" ]; then + cp -R "dist/theme/$directory" "$deploy_dir/$directory" + fi + done + + git -C "$deploy_dir" checkout -B shopify-production + git -C "$deploy_dir" config user.name "github-actions[bot]" + git -C "$deploy_dir" config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git -C "$deploy_dir" add -A + if git -C "$deploy_dir" diff --cached --quiet; then + echo "No deployable theme changes." + exit 0 + fi + git -C "$deploy_dir" commit -m "Deploy Shopify theme from ${GITHUB_SHA}" + git -C "$deploy_dir" push origin HEAD:shopify-production diff --git a/liquid-loom.config.ts b/liquid-loom.config.ts index a5790ef..16c3c11 100644 --- a/liquid-loom.config.ts +++ b/liquid-loom.config.ts @@ -1,6 +1,7 @@ import { defineConfig } from "liquid-loom"; export default defineConfig({ + viteConfig: false, performance: { maxAssetBytes: 500_000, maxBuildMs: 10_000, diff --git a/packages/create-liquid-loom/template/README.md b/packages/create-liquid-loom/template/README.md index 79041d9..8bf3b65 100644 --- a/packages/create-liquid-loom/template/README.md +++ b/packages/create-liquid-loom/template/README.md @@ -2,6 +2,8 @@ A Shopify storefront built with [Liquid Loom](https://github.com/yotamon/Liquid-Loom). +The starter uses Shopify-native component CSS/JavaScript by default and keeps Vite/Tailwind optional. + ## Start ```bash @@ -9,4 +11,4 @@ pnpm install pnpm dev ``` -Use `pnpm build` for a deployable theme in `dist/theme`, `pnpm doctor` for environment diagnostics, and `pnpm validate` before shipping. +Use `pnpm build` for a deployable theme in `dist/theme`, `pnpm explain -- --json` for the deterministic project model, `pnpm doctor` for environment diagnostics, and `pnpm validate` before shipping. diff --git a/packages/create-liquid-loom/template/liquid-loom.config.ts b/packages/create-liquid-loom/template/liquid-loom.config.ts index a5790ef..16c3c11 100644 --- a/packages/create-liquid-loom/template/liquid-loom.config.ts +++ b/packages/create-liquid-loom/template/liquid-loom.config.ts @@ -1,6 +1,7 @@ import { defineConfig } from "liquid-loom"; export default defineConfig({ + viteConfig: false, performance: { maxAssetBytes: 500_000, maxBuildMs: 10_000, diff --git a/packages/create-liquid-loom/template/package.json b/packages/create-liquid-loom/template/package.json index 4dfc899..e61a4a7 100644 --- a/packages/create-liquid-loom/template/package.json +++ b/packages/create-liquid-loom/template/package.json @@ -3,9 +3,7 @@ "version": "0.1.0", "private": true, "type": "module", - "engines": { - "node": ">=22.12.0" - }, + "engines": { "node": ">=22.12.0" }, "packageManager": "pnpm@11.11.0", "scripts": { "analyze": "liquid-loom analyze", @@ -15,6 +13,7 @@ "clean": "liquid-loom clean", "dev": "liquid-loom dev", "doctor": "liquid-loom doctor", + "explain": "liquid-loom explain", "format": "prettier --write .", "format:check": "prettier --check .", "shopify:push": "shopify theme push --path dist/theme", @@ -24,12 +23,10 @@ "watch": "liquid-loom watch" }, "devDependencies": { - "@shopify/cli": "^4.6.0", + "@shopify/cli": "^4.8.0", "@shopify/prettier-plugin-liquid": "^1.11.0", - "@tailwindcss/vite": "^4.3.3", "liquid-loom": "^0.2.0", "prettier": "^3.9.6", - "tailwindcss": "^4.3.3", "typescript": "^7.0.2" } } diff --git a/packages/create-liquid-loom/template/src/entrypoints/theme.js b/packages/create-liquid-loom/template/src/entrypoints/theme.js deleted file mode 100644 index fd16943..0000000 --- a/packages/create-liquid-loom/template/src/entrypoints/theme.js +++ /dev/null @@ -1,118 +0,0 @@ -import "../styles/theme.css"; - -class MenuDisclosure extends HTMLElement { - connectedCallback() { - this.button = this.querySelector("button"); - this.panel = this.querySelector("[data-menu-panel]"); - if (!this.button || !this.panel) return; - - this.button.addEventListener("click", () => this.toggle()); - this.addEventListener("keydown", (event) => { - if (event.key === "Escape" && this.isOpen()) { - this.close(); - this.button.focus(); - } - }); - } - - isOpen() { - return this.button.getAttribute("aria-expanded") === "true"; - } - - toggle() { - this.isOpen() ? this.close() : this.open(); - } - - open() { - this.button.setAttribute("aria-expanded", "true"); - this.panel.removeAttribute("hidden"); - document.documentElement.classList.add("menu-open"); - } - - close() { - this.button.setAttribute("aria-expanded", "false"); - this.panel.setAttribute("hidden", ""); - document.documentElement.classList.remove("menu-open"); - } -} - -class QuantityInput extends HTMLElement { - connectedCallback() { - this.input = this.querySelector("input[type='number']"); - this.querySelectorAll("button").forEach((button) => { - button.addEventListener("click", () => this.change(button.dataset.quantityAction)); - }); - } - - change(action) { - if (!this.input) return; - const step = Number(this.input.step) || 1; - const minimum = Number(this.input.min) || 1; - const current = Number(this.input.value) || minimum; - this.input.value = String(Math.max(minimum, current + (action === "increase" ? step : -step))); - this.input.dispatchEvent(new Event("change", { bubbles: true })); - } -} - -class PredictiveSearch extends HTMLElement { - connectedCallback() { - this.input = this.querySelector("input[type='search']"); - this.results = this.querySelector("[data-predictive-search-results]"); - if (!this.input || !this.results) return; - this.input.addEventListener("input", () => { - clearTimeout(this.timer); - this.timer = setTimeout(() => this.search(), 180); - }); - } - - async search() { - const query = this.input.value.trim(); - this.controller?.abort(); - if (query.length < 2) { - this.results.replaceChildren(); - this.input.setAttribute("aria-expanded", "false"); - return; - } - - this.controller = new AbortController(); - try { - const url = `/search/suggest?q=${encodeURIComponent(query)}&resources[type]=product§ion_id=predictive-search`; - const response = await fetch(url, { signal: this.controller.signal }); - if (!response.ok) throw new Error(`Search request failed: ${response.status}`); - const document = new DOMParser().parseFromString(await response.text(), "text/html"); - const content = document.querySelector(".predictive-search__results"); - this.results.replaceChildren(...(content ? [content] : [])); - this.input.setAttribute("aria-expanded", content ? "true" : "false"); - } catch (error) { - if (error.name !== "AbortError") console.error(error); - } - } -} - -class ProductForm extends HTMLElement { - connectedCallback() { - this.select = this.querySelector("[data-variant-select]"); - this.submit = this.querySelector("button[type='submit']"); - const data = this.querySelector("[data-product-variants]"); - if (!this.select || !data) return; - this.variants = JSON.parse(data.textContent); - this.select.addEventListener("change", () => this.variantChanged()); - } - - variantChanged() { - const variant = this.variants.find(({ id }) => String(id) === this.select.value); - if (!variant) return; - this.submit?.toggleAttribute("disabled", !variant.available); - const url = new URL(window.location.href); - url.searchParams.set("variant", variant.id); - window.history.replaceState({}, "", url); - } -} - -if (!customElements.get("menu-disclosure")) customElements.define("menu-disclosure", MenuDisclosure); -if (!customElements.get("quantity-input")) customElements.define("quantity-input", QuantityInput); -if (!customElements.get("predictive-search")) customElements.define("predictive-search", PredictiveSearch); -if (!customElements.get("product-form")) customElements.define("product-form", ProductForm); - -document.documentElement.classList.remove("no-js"); -document.documentElement.classList.add("js"); diff --git a/packages/create-liquid-loom/template/src/public/theme.css b/packages/create-liquid-loom/template/src/public/theme.css new file mode 100644 index 0000000..210c1cf --- /dev/null +++ b/packages/create-liquid-loom/template/src/public/theme.css @@ -0,0 +1,86 @@ +:root { + --color-background: #f8f7f2; + --color-foreground: #111820; + --color-surface: #ffffff; + --color-muted: #66717c; + --color-border: #dce1dd; + --color-accent: #176c5b; + --color-accent-contrast: #ffffff; + --page-width: 1200px; + --radius-sm: 0.45rem; + --radius-md: 0.9rem; + --radius-lg: 1.5rem; + --shadow-soft: 0 20px 60px rgb(17 24 32 / 8%); +} + +*, +*::before, +*::after { box-sizing: border-box; } +html { scroll-behavior: smooth; } +body { margin: 0; background: var(--color-background); color: var(--color-foreground); font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; font-size: 1rem; line-height: 1.6; text-rendering: optimizeLegibility; } +body, button, input, select, textarea { font: inherit; } +img, svg, video { display: block; max-width: 100%; } +a { color: inherit; text-decoration-thickness: 0.08em; text-underline-offset: 0.2em; } +button, input, select, textarea { color: inherit; } +:focus-visible { outline: 3px solid color-mix(in srgb, var(--color-accent) 70%, white); outline-offset: 3px; } +.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; border: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; } +.skip-link { position: fixed; top: 1rem; left: 1rem; z-index: 100; transform: translateY(-200%); border-radius: var(--radius-sm); background: var(--color-foreground); color: white; padding: 0.75rem 1rem; } +.skip-link:focus { transform: translateY(0); } +.page-width { width: min(calc(100% - 2rem), var(--page-width)); margin-inline: auto; } +.section { padding-block: clamp(3rem, 7vw, 7rem); } +.section--compact { padding-block: clamp(2rem, 4vw, 4rem); } +.eyebrow { margin: 0 0 1rem; color: var(--color-accent); font-size: 0.76rem; font-weight: 750; letter-spacing: 0.14em; text-transform: uppercase; } +.heading { margin: 0; font-size: clamp(2.2rem, 5vw, 5rem); font-weight: 760; letter-spacing: -0.055em; line-height: 0.98; } +.heading--medium { font-size: clamp(1.8rem, 3.5vw, 3.5rem); } +.muted { color: var(--color-muted); } +.button { display: inline-flex; min-height: 3rem; align-items: center; justify-content: center; gap: 0.5rem; border: 1px solid transparent; border-radius: 999px; background: var(--color-accent); color: var(--color-accent-contrast); cursor: pointer; font-weight: 700; padding: 0.75rem 1.35rem; text-decoration: none; transition: transform 160ms ease, box-shadow 160ms ease; } +.button:hover { transform: translateY(-2px); box-shadow: 0 12px 30px color-mix(in srgb, var(--color-accent) 25%, transparent); } +.button--secondary { border-color: var(--color-border); background: transparent; color: var(--color-foreground); } +.button--full { width: 100%; } +.announcement { background: var(--color-foreground); color: var(--color-background); font-size: 0.84rem; letter-spacing: 0.03em; padding: 0.55rem 1rem; text-align: center; } +.hero { position: relative; overflow: hidden; } +.hero::before { position: absolute; inset: 5% -15% auto auto; width: 55vw; height: 55vw; border-radius: 50%; background: radial-gradient(circle, color-mix(in srgb, var(--color-accent) 18%, transparent), transparent 70%); content: ""; pointer-events: none; } +.hero__grid { position: relative; display: grid; min-height: min(760px, 78vh); grid-template-columns: minmax(0, 0.95fr) minmax(300px, 1.05fr); align-items: center; gap: clamp(2rem, 7vw, 7rem); } +.hero__copy { max-width: 42rem; } +.hero__text { max-width: 34rem; margin-block: 1.5rem 2rem; color: var(--color-muted); font-size: clamp(1rem, 1.5vw, 1.2rem); } +.hero__actions { display: flex; flex-wrap: wrap; gap: 0.75rem; } +.hero__media { position: relative; overflow: hidden; aspect-ratio: 4 / 5; border-radius: var(--radius-lg); background: linear-gradient(145deg, color-mix(in srgb, var(--color-accent) 85%, black), var(--color-foreground)); box-shadow: var(--shadow-soft); } +.hero__media img { width: 100%; height: 100%; object-fit: cover; } +.hero__placeholder { display: grid; height: 100%; place-items: center; color: color-mix(in srgb, white 70%, transparent); } +.hero__placeholder svg { width: 52%; opacity: 0.5; } +.section-header { display: flex; align-items: end; justify-content: space-between; gap: 2rem; margin-bottom: 2rem; } +.product-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: clamp(1rem, 2.5vw, 2rem); } +.product-card { min-width: 0; } +.product-card__media { display: block; overflow: hidden; aspect-ratio: 4 / 5; border-radius: var(--radius-md); background: color-mix(in srgb, var(--color-border) 55%, white); } +.product-card__media img { width: 100%; height: 100%; object-fit: cover; transition: transform 300ms ease; } +.product-card:hover img { transform: scale(1.025); } +.product-card__content { display: flex; align-items: start; justify-content: space-between; gap: 1rem; padding-top: 0.9rem; } +.product-card__title { margin: 0; font-size: 1rem; font-weight: 700; } +.product-card__title a { text-decoration: none; } +.price { white-space: nowrap; font-variant-numeric: tabular-nums; } +.price__compare { margin-right: 0.35rem; color: var(--color-muted); text-decoration: line-through; } +.collection-header { max-width: 48rem; margin-bottom: 3rem; } +.facets { display: flex; flex-wrap: wrap; align-items: start; gap: 0.75rem; margin-bottom: 2rem; } +.facet { position: relative; border: 1px solid var(--color-border); border-radius: var(--radius-sm); background: var(--color-surface); padding: 0.65rem 0.8rem; } +.facet__options { display: grid; min-width: 14rem; gap: 0.55rem; padding-top: 0.75rem; } +.cart-table { width: 100%; border-collapse: collapse; } +.cart-table th, .cart-table td { border-bottom: 1px solid var(--color-border); padding: 1rem; text-align: left; } +.cart-product { display: flex; align-items: center; gap: 1rem; } +.cart-product img { width: 5rem; border-radius: var(--radius-sm); } +.cart-footer { display: grid; justify-content: end; gap: 1rem; padding-top: 2rem; text-align: right; } +.rte { max-width: 48rem; } +.rte > * + * { margin-top: 1.25em; } +.content-stack { display: grid; max-width: 52rem; gap: 1.5rem; } +.content-image { margin: 0; } +.content-image img, .content-image svg { width: 100%; border-radius: var(--radius-lg); } +.empty-state { max-width: 38rem; margin-inline: auto; padding-block: 8rem; text-align: center; } +.site-footer { margin-top: clamp(3rem, 7vw, 7rem); border-top: 1px solid var(--color-border); background: var(--color-foreground); color: var(--color-background); } +.site-footer__grid { display: grid; grid-template-columns: 1.2fr repeat(2, 0.8fr); gap: 3rem; } +.site-footer__title { margin: 0 0 1rem; font-size: 0.8rem; letter-spacing: 0.12em; text-transform: uppercase; } +.site-footer ul { margin: 0; padding: 0; list-style: none; } +.site-footer li + li { margin-top: 0.65rem; } +.site-footer a { color: color-mix(in srgb, var(--color-background) 78%, transparent); text-decoration: none; } +.site-footer__bottom { display: flex; justify-content: space-between; gap: 1rem; border-top: 1px solid color-mix(in srgb, white 14%, transparent); margin-top: 3rem; padding-top: 1.5rem; color: color-mix(in srgb, var(--color-background) 62%, transparent); font-size: 0.82rem; } +@media (max-width: 900px) { .hero__grid { grid-template-columns: 1fr; padding-block: 4rem; } .hero__media { max-height: 65vh; } .product-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } .site-footer__grid { grid-template-columns: 1fr 1fr; } .site-footer__about { grid-column: 1 / -1; } } +@media (max-width: 600px) { .page-width { width: min(calc(100% - 1.25rem), var(--page-width)); } .product-grid, .site-footer__grid { grid-template-columns: 1fr; } .section-header, .site-footer__bottom { align-items: start; flex-direction: column; } .cart-table thead { display: none; } .cart-table tr, .cart-table td { display: block; } } +@media (prefers-reduced-motion: reduce) { *, *::before, *::after { scroll-behavior: auto !important; transition-duration: 0.01ms !important; } } diff --git a/packages/create-liquid-loom/template/src/styles/theme.css b/packages/create-liquid-loom/template/src/styles/theme.css deleted file mode 100644 index 3ee152c..0000000 --- a/packages/create-liquid-loom/template/src/styles/theme.css +++ /dev/null @@ -1,705 +0,0 @@ -@import "tailwindcss"; -@source "../theme/**/*.liquid"; - -:root { - --color-background: #f8f7f2; - --color-foreground: #111820; - --color-surface: #ffffff; - --color-muted: #66717c; - --color-border: #dce1dd; - --color-accent: #176c5b; - --color-accent-contrast: #ffffff; - --page-width: 1200px; - --radius-sm: 0.45rem; - --radius-md: 0.9rem; - --radius-lg: 1.5rem; - --shadow-soft: 0 20px 60px rgb(17 24 32 / 8%); -} - -*, -*::before, -*::after { - box-sizing: border-box; -} - -html { - scroll-behavior: smooth; -} - -body { - margin: 0; - background: var(--color-background); - color: var(--color-foreground); - font-family: - Inter, - ui-sans-serif, - system-ui, - -apple-system, - BlinkMacSystemFont, - "Segoe UI", - sans-serif; - font-size: 1rem; - line-height: 1.6; - text-rendering: optimizeLegibility; -} - -body, -button, -input, -select, -textarea { - font: inherit; -} - -img, -svg, -video { - display: block; - max-width: 100%; -} - -a { - color: inherit; - text-decoration-thickness: 0.08em; - text-underline-offset: 0.2em; -} - -button, -input, -select, -textarea { - color: inherit; -} - -:focus-visible { - outline: 3px solid color-mix(in srgb, var(--color-accent) 70%, white); - outline-offset: 3px; -} - -.sr-only { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - border: 0; - margin: -1px; - overflow: hidden; - clip: rect(0, 0, 0, 0); - white-space: nowrap; -} - -.skip-link { - position: fixed; - top: 1rem; - left: 1rem; - z-index: 100; - transform: translateY(-200%); - border-radius: var(--radius-sm); - background: var(--color-foreground); - color: white; - padding: 0.75rem 1rem; -} - -.skip-link:focus { - transform: translateY(0); -} - -.page-width { - width: min(calc(100% - 2rem), var(--page-width)); - margin-inline: auto; -} - -.section { - padding-block: clamp(3rem, 7vw, 7rem); -} - -.section--compact { - padding-block: clamp(2rem, 4vw, 4rem); -} - -.eyebrow { - margin: 0 0 1rem; - color: var(--color-accent); - font-size: 0.76rem; - font-weight: 750; - letter-spacing: 0.14em; - text-transform: uppercase; -} - -.heading { - margin: 0; - font-size: clamp(2.2rem, 5vw, 5rem); - font-weight: 760; - letter-spacing: -0.055em; - line-height: 0.98; -} - -.heading--medium { - font-size: clamp(1.8rem, 3.5vw, 3.5rem); -} - -.muted { - color: var(--color-muted); -} - -.button { - display: inline-flex; - min-height: 3rem; - align-items: center; - justify-content: center; - gap: 0.5rem; - border: 1px solid transparent; - border-radius: 999px; - background: var(--color-accent); - color: var(--color-accent-contrast); - cursor: pointer; - font-weight: 700; - padding: 0.75rem 1.35rem; - text-decoration: none; - transition: - transform 160ms ease, - box-shadow 160ms ease; -} - -.button:hover { - transform: translateY(-2px); - box-shadow: 0 12px 30px color-mix(in srgb, var(--color-accent) 25%, transparent); -} - -.button--secondary { - border-color: var(--color-border); - background: transparent; - color: var(--color-foreground); -} - -.button--full { - width: 100%; -} - -.announcement { - background: var(--color-foreground); - color: var(--color-background); - font-size: 0.84rem; - letter-spacing: 0.03em; - padding: 0.55rem 1rem; - text-align: center; -} - -.site-header { - position: sticky; - top: 0; - z-index: 40; - border-bottom: 1px solid color-mix(in srgb, var(--color-border) 75%, transparent); - background: color-mix(in srgb, var(--color-background) 88%, transparent); - backdrop-filter: blur(18px); -} - -.site-header__inner { - display: grid; - min-height: 4.8rem; - grid-template-columns: 1fr auto 1fr; - align-items: center; - gap: 1rem; -} - -.site-header__brand { - justify-self: center; - font-size: 1.1rem; - font-weight: 800; - letter-spacing: -0.03em; - text-decoration: none; -} - -.site-header__logo { - width: auto; - max-height: 2.3rem; -} - -.site-header__nav, -.site-header__actions { - display: flex; - align-items: center; - gap: 1.25rem; -} - -.site-header__nav a, -.site-header__actions a { - font-size: 0.9rem; - font-weight: 650; - text-decoration: none; -} - -.site-header__actions { - justify-self: end; -} - -.site-header__menu-button { - display: none; - border: 0; - background: transparent; - cursor: pointer; - padding: 0.5rem; -} - -.mobile-menu { - border-top: 1px solid var(--color-border); - background: var(--color-background); - padding: 1rem; -} - -.mobile-menu[hidden] { - display: none; -} - -.mobile-menu a { - display: block; - padding: 0.8rem; - font-size: 1.15rem; - font-weight: 700; - text-decoration: none; -} - -.hero { - position: relative; - overflow: hidden; -} - -.hero::before { - position: absolute; - inset: 5% -15% auto auto; - width: 55vw; - height: 55vw; - border-radius: 50%; - background: radial-gradient(circle, color-mix(in srgb, var(--color-accent) 18%, transparent), transparent 70%); - content: ""; - pointer-events: none; -} - -.hero__grid { - position: relative; - display: grid; - min-height: min(760px, 78vh); - grid-template-columns: minmax(0, 0.95fr) minmax(300px, 1.05fr); - align-items: center; - gap: clamp(2rem, 7vw, 7rem); -} - -.hero__copy { - max-width: 42rem; -} - -.hero__text { - max-width: 34rem; - margin-block: 1.5rem 2rem; - color: var(--color-muted); - font-size: clamp(1rem, 1.5vw, 1.2rem); -} - -.hero__actions { - display: flex; - flex-wrap: wrap; - gap: 0.75rem; -} - -.hero__media { - position: relative; - overflow: hidden; - aspect-ratio: 4 / 5; - border-radius: var(--radius-lg); - background: linear-gradient(145deg, color-mix(in srgb, var(--color-accent) 85%, black), var(--color-foreground)); - box-shadow: var(--shadow-soft); -} - -.hero__media img { - width: 100%; - height: 100%; - object-fit: cover; -} - -.hero__placeholder { - display: grid; - height: 100%; - place-items: center; - color: color-mix(in srgb, white 70%, transparent); -} - -.hero__placeholder svg { - width: 52%; - opacity: 0.5; -} - -.section-header { - display: flex; - align-items: end; - justify-content: space-between; - gap: 2rem; - margin-bottom: 2rem; -} - -.product-grid { - display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); - gap: clamp(1rem, 2.5vw, 2rem); -} - -.product-card { - min-width: 0; -} - -.product-card__media { - display: block; - overflow: hidden; - aspect-ratio: 4 / 5; - border-radius: var(--radius-md); - background: color-mix(in srgb, var(--color-border) 55%, white); -} - -.product-card__media img { - width: 100%; - height: 100%; - object-fit: cover; - transition: transform 300ms ease; -} - -.product-card:hover img { - transform: scale(1.025); -} - -.product-card__content { - display: flex; - align-items: start; - justify-content: space-between; - gap: 1rem; - padding-top: 0.9rem; -} - -.product-card__title { - margin: 0; - font-size: 1rem; - font-weight: 700; -} - -.product-card__title a { - text-decoration: none; -} - -.price { - white-space: nowrap; - font-variant-numeric: tabular-nums; -} - -.price__compare { - margin-right: 0.35rem; - color: var(--color-muted); - text-decoration: line-through; -} - -.product { - display: grid; - grid-template-columns: minmax(0, 1.1fr) minmax(320px, 0.9fr); - gap: clamp(2rem, 7vw, 7rem); -} - -.product__gallery { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 1rem; -} - -.product__gallery img { - width: 100%; - border-radius: var(--radius-md); -} - -.product__details { - align-self: start; -} - -.product__details .price { - display: block; - margin-block: 1rem; - font-size: 1.2rem; -} - -.field { - display: grid; - gap: 0.45rem; - margin-block: 1rem; -} - -.field label { - font-size: 0.85rem; - font-weight: 700; -} - -.field input, -.field select, -.field textarea { - width: 100%; - min-height: 3rem; - border: 1px solid var(--color-border); - border-radius: var(--radius-sm); - background: var(--color-surface); - padding: 0.7rem 0.85rem; -} - -.quantity { - display: grid; - width: 9rem; - grid-template-columns: 2.6rem 1fr 2.6rem; - border: 1px solid var(--color-border); - border-radius: 999px; - background: var(--color-surface); -} - -.quantity button, -.quantity input { - min-height: 2.8rem; - border: 0; - background: transparent; - text-align: center; -} - -.quantity input { - width: 100%; - appearance: textfield; -} - -.collection-header { - max-width: 48rem; - margin-bottom: 3rem; -} - -.cart-table { - width: 100%; - border-collapse: collapse; -} - -.cart-table th, -.cart-table td { - border-bottom: 1px solid var(--color-border); - padding: 1rem; - text-align: left; -} - -.cart-product { - display: flex; - align-items: center; - gap: 1rem; -} - -.cart-product img { - width: 5rem; - border-radius: var(--radius-sm); -} - -.cart-footer { - display: grid; - justify-content: end; - gap: 1rem; - padding-top: 2rem; - text-align: right; -} - -.rte { - max-width: 48rem; -} - -.rte > * + * { - margin-top: 1.25em; -} - -.content-stack { - display: grid; - max-width: 52rem; - gap: 1.5rem; -} - -.content-image { - margin: 0; -} - -.content-image img, -.content-image svg { - width: 100%; - border-radius: var(--radius-lg); -} - -.facets { - display: flex; - flex-wrap: wrap; - align-items: start; - gap: 0.75rem; - margin-bottom: 2rem; -} - -.facet { - position: relative; - border: 1px solid var(--color-border); - border-radius: var(--radius-sm); - background: var(--color-surface); - padding: 0.65rem 0.8rem; -} - -.facet__options { - display: grid; - min-width: 14rem; - gap: 0.55rem; - padding-top: 0.75rem; -} - -.predictive-search__results { - border: 1px solid var(--color-border); - border-radius: var(--radius-md); - margin-top: 0.5rem; - background: var(--color-surface); - box-shadow: var(--shadow-soft); - padding: 1rem; -} - -.predictive-search__results ul { - display: grid; - gap: 0.65rem; - margin: 0; - padding: 0; - list-style: none; -} - -.predictive-search__results a { - display: flex; - justify-content: space-between; - gap: 1rem; - text-decoration: none; -} - -.empty-state { - max-width: 38rem; - margin-inline: auto; - padding-block: 8rem; - text-align: center; -} - -.site-footer { - margin-top: clamp(3rem, 7vw, 7rem); - border-top: 1px solid var(--color-border); - background: var(--color-foreground); - color: var(--color-background); -} - -.site-footer__grid { - display: grid; - grid-template-columns: 1.2fr repeat(2, 0.8fr); - gap: 3rem; -} - -.site-footer__title { - margin: 0 0 1rem; - font-size: 0.8rem; - letter-spacing: 0.12em; - text-transform: uppercase; -} - -.site-footer ul { - margin: 0; - padding: 0; - list-style: none; -} - -.site-footer li + li { - margin-top: 0.65rem; -} - -.site-footer a { - color: color-mix(in srgb, var(--color-background) 78%, transparent); - text-decoration: none; -} - -.site-footer__bottom { - display: flex; - justify-content: space-between; - gap: 1rem; - border-top: 1px solid color-mix(in srgb, white 14%, transparent); - margin-top: 3rem; - padding-top: 1.5rem; - color: color-mix(in srgb, var(--color-background) 62%, transparent); - font-size: 0.82rem; -} - -@media (max-width: 900px) { - .site-header__inner { - grid-template-columns: 1fr auto 1fr; - } - - .site-header__nav { - display: none; - } - - .site-header__menu-button { - display: inline-flex; - } - - .hero__grid, - .product { - grid-template-columns: 1fr; - } - - .hero__grid { - padding-block: 4rem; - } - - .hero__media { - max-height: 65vh; - } - - .product-grid { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - - .site-footer__grid { - grid-template-columns: 1fr 1fr; - } - - .site-footer__about { - grid-column: 1 / -1; - } -} - -@media (max-width: 600px) { - .page-width { - width: min(calc(100% - 1.25rem), var(--page-width)); - } - - .site-header__actions a:not([href*="cart"]) { - display: none; - } - - .product-grid, - .product__gallery, - .site-footer__grid { - grid-template-columns: 1fr; - } - - .section-header, - .site-footer__bottom { - align-items: start; - flex-direction: column; - } - - .cart-table thead { - display: none; - } - - .cart-table tr, - .cart-table td { - display: block; - } -} - -@media (prefers-reduced-motion: reduce) { - *, - *::before, - *::after { - scroll-behavior: auto !important; - transition-duration: 0.01ms !important; - } -} diff --git a/packages/create-liquid-loom/template/src/theme/blocks/product-description.liquid b/packages/create-liquid-loom/template/src/theme/blocks/product-description.liquid new file mode 100644 index 0000000..a6aedda --- /dev/null +++ b/packages/create-liquid-loom/template/src/theme/blocks/product-description.liquid @@ -0,0 +1,2 @@ +{%- if product.description != blank -%}
{{ product.description }}
{%- endif -%} +{% schema %}{"name":"Product description","presets":[{"name":"Product description"}]}{% endschema %} diff --git a/packages/create-liquid-loom/template/src/theme/blocks/product-price.liquid b/packages/create-liquid-loom/template/src/theme/blocks/product-price.liquid new file mode 100644 index 0000000..475a02f --- /dev/null +++ b/packages/create-liquid-loom/template/src/theme/blocks/product-price.liquid @@ -0,0 +1,2 @@ +
{% render 'price', product: product %}
+{% schema %}{"name":"Product price","presets":[{"name":"Product price"}]}{% endschema %} diff --git a/packages/create-liquid-loom/template/src/theme/blocks/product-purchase.liquid b/packages/create-liquid-loom/template/src/theme/blocks/product-purchase.liquid new file mode 100644 index 0000000..54a1ee4 --- /dev/null +++ b/packages/create-liquid-loom/template/src/theme/blocks/product-purchase.liquid @@ -0,0 +1,4 @@ +{%- form 'product', product, id: 'ProductForm-' | append: block.id, novalidate: 'novalidate', data-product-form: '' -%}{%- unless product.has_only_default_variant -%}
{%- else -%}{%- endunless -%}{%- if product.selling_plan_groups.size > 0 -%}
{%- endif -%}

{%- endform -%}
+{% stylesheet %}.field{display:grid;gap:.45rem;margin-block:1rem}.field label{font-size:.85rem;font-weight:700}.field input,.field select,.field textarea{width:100%;min-height:3rem;border:1px solid var(--color-border);border-radius:var(--radius-sm);background:var(--color-surface);padding:.7rem .85rem}.quantity{display:grid;width:9rem;grid-template-columns:2.6rem 1fr 2.6rem;border:1px solid var(--color-border);border-radius:999px;background:var(--color-surface)}.quantity button,.quantity input{min-height:2.8rem;border:0;background:transparent;text-align:center}.quantity input{width:100%;appearance:textfield}.product-form__status{min-height:1.5em;margin-block:.75rem 0}{% endstylesheet %} +{% javascript %}class QuantityInput extends HTMLElement{connectedCallback(){this.input=this.querySelector("input[type='number']");this.querySelectorAll("button").forEach(button=>button.addEventListener("click",()=>this.change(button.dataset.quantityAction)))}change(action){if(!this.input)return;const step=Number(this.input.step)||1;const minimum=Number(this.input.min)||1;const current=Number(this.input.value)||minimum;this.input.value=String(Math.max(minimum,current+(action==="increase"?step:-step)));this.input.dispatchEvent(new Event("change",{bubbles:true}))}}class ProductForm extends HTMLElement{connectedCallback(){this.form=this.querySelector("form");this.select=this.querySelector("[data-variant-select]");this.submit=this.querySelector("button[type='submit']");this.status=this.querySelector("[data-product-form-status]");const data=this.querySelector("[data-product-variants]");if(data)this.variants=JSON.parse(data.textContent);this.select?.addEventListener("change",()=>this.variantChanged());this.form?.addEventListener("submit",event=>this.submitWithStandardAction(event))}variantChanged(){const variant=this.variants?.find(({id})=>String(id)===this.select.value);if(!variant)return;this.submit?.toggleAttribute("disabled",!variant.available);const url=new URL(window.location.href);url.searchParams.set("variant",variant.id);window.history.replaceState({},"",url)}async submitWithStandardAction(event){if(!window.Shopify?.actions?.updateCart||!this.form)return;event.preventDefault();const formData=new FormData(this.form);const merchandiseId=formData.get("id");if(!merchandiseId)return;const line={merchandiseId:String(merchandiseId),quantity:Math.max(1,Number(formData.get("quantity"))||1)};const sellingPlanId=formData.get("selling_plan");if(sellingPlanId)line.sellingPlanId=String(sellingPlanId);this.submit?.setAttribute("disabled","");if(this.status)this.status.textContent="";try{const{cart,userErrors,warnings}=await window.Shopify.actions.updateCart({lines:[line]},{event:{context:"product"}});if(userErrors?.length){if(this.status)this.status.textContent=userErrors[0].message;return}if(this.status)this.status.textContent=warnings?.[0]?.message||"Added to cart.";if(cart)document.querySelectorAll("[data-cart-count]").forEach(node=>{node.textContent=String(cart.totalQuantity)})}catch(error){console.error(error);if(this.status)this.status.textContent="Unable to update the cart. Try again."}finally{const variant=this.variants?.find(({id})=>String(id)===String(formData.get("id")));this.submit?.toggleAttribute("disabled",variant?!variant.available:false)}}}if(!customElements.get("quantity-input"))customElements.define("quantity-input",QuantityInput);if(!customElements.get("product-form"))customElements.define("product-form",ProductForm);{% endjavascript %} +{% schema %}{"name":"Product purchase","presets":[{"name":"Product purchase"}]}{% endschema %} diff --git a/packages/create-liquid-loom/template/src/theme/blocks/product-title.liquid b/packages/create-liquid-loom/template/src/theme/blocks/product-title.liquid new file mode 100644 index 0000000..2be29a4 --- /dev/null +++ b/packages/create-liquid-loom/template/src/theme/blocks/product-title.liquid @@ -0,0 +1,2 @@ +

{{ product.title | escape }}

+{% schema %}{"name":"Product title","presets":[{"name":"Product title"}]}{% endschema %} diff --git a/packages/create-liquid-loom/template/src/theme/blocks/product-vendor.liquid b/packages/create-liquid-loom/template/src/theme/blocks/product-vendor.liquid new file mode 100644 index 0000000..df953df --- /dev/null +++ b/packages/create-liquid-loom/template/src/theme/blocks/product-vendor.liquid @@ -0,0 +1,2 @@ +{%- if product.vendor != blank -%}

{{ product.vendor | escape }}

{%- endif -%} +{% schema %}{"name":"Product vendor","presets":[{"name":"Product vendor"}]}{% endschema %} diff --git a/packages/create-liquid-loom/template/src/theme/config/settings_data.json b/packages/create-liquid-loom/template/src/theme/config/settings_data.json index 5f90608..9c3de6e 100644 --- a/packages/create-liquid-loom/template/src/theme/config/settings_data.json +++ b/packages/create-liquid-loom/template/src/theme/config/settings_data.json @@ -1,20 +1,4 @@ { - "current": { - "color_background": "#f8f7f2", - "color_foreground": "#111820", - "color_surface": "#ffffff", - "color_accent": "#176c5b", - "color_accent_contrast": "#ffffff", - "page_width": 1200 - }, - "presets": { - "Default": { - "color_background": "#f8f7f2", - "color_foreground": "#111820", - "color_surface": "#ffffff", - "color_accent": "#176c5b", - "color_accent_contrast": "#ffffff", - "page_width": 1200 - } - } + "current": { "page_width": 1200 }, + "presets": { "Default": { "page_width": 1200 } } } diff --git a/packages/create-liquid-loom/template/src/theme/config/settings_schema.json b/packages/create-liquid-loom/template/src/theme/config/settings_schema.json index f13f9bf..01b71b9 100644 --- a/packages/create-liquid-loom/template/src/theme/config/settings_schema.json +++ b/packages/create-liquid-loom/template/src/theme/config/settings_schema.json @@ -1,27 +1,10 @@ [ { "name": "Layout", - "settings": [ - { - "type": "range", - "id": "page_width", - "min": 960, - "max": 1600, - "step": 40, - "unit": "px", - "label": "Page width", - "default": 1200 - } - ] + "settings": [{ "type": "range", "id": "page_width", "min": 960, "max": 1600, "step": 40, "unit": "px", "label": "Page width", "default": 1200 }] }, { "name": "Colors", - "settings": [ - { "type": "color", "id": "color_background", "label": "Background", "default": "#f8f7f2" }, - { "type": "color", "id": "color_foreground", "label": "Text", "default": "#111820" }, - { "type": "color", "id": "color_surface", "label": "Surface", "default": "#ffffff" }, - { "type": "color", "id": "color_accent", "label": "Accent", "default": "#176c5b" }, - { "type": "color", "id": "color_accent_contrast", "label": "Accent text", "default": "#ffffff" } - ] + "settings": [{ "type": "color_palette", "id": "colors", "default": { "background": "#F8F7F2", "foreground": "#111820", "surface": "#FFFFFF", "muted": "#66717C", "border": "#DCE1DD", "accent": "#176C5B", "accent_contrast": "#FFFFFF" } }] } ] diff --git a/packages/create-liquid-loom/template/src/theme/layout/theme.liquid b/packages/create-liquid-loom/template/src/theme/layout/theme.liquid index af4d7ba..3747091 100644 --- a/packages/create-liquid-loom/template/src/theme/layout/theme.liquid +++ b/packages/create-liquid-loom/template/src/theme/layout/theme.liquid @@ -1,40 +1,20 @@ - + - + - {% render 'meta-tags' %} - - {{ 'style.css' | asset_url | stylesheet_tag: preload: true }} - - - - + {{ 'theme.css' | asset_url | stylesheet_tag: preload: true }} + {{ content_for_header }} - - {% sections 'header-group' %} - -
- {{ content_for_layout }} -
- +
{{ content_for_layout }}
{% sections 'footer-group' %} diff --git a/packages/create-liquid-loom/template/src/theme/sections/cart/main-cart.liquid b/packages/create-liquid-loom/template/src/theme/sections/cart/main-cart.liquid index 2e98701..530a305 100644 --- a/packages/create-liquid-loom/template/src/theme/sections/cart/main-cart.liquid +++ b/packages/create-liquid-loom/template/src/theme/sections/cart/main-cart.liquid @@ -1,64 +1,2 @@ -
-
-

{{ 'cart.title' | t }}

- - {%- if cart.empty? -%} -
-

{{ 'cart.empty' | t }}

- {{ 'actions.continue_shopping' | t }} -
- {%- else -%} -
- - - - - - - - - - {%- for item in cart.items -%} - - - - - - {%- endfor -%} - -
ProductQuantityTotal
-
- {%- if item.image -%} - {{ item.image | image_url: width: 200 | image_tag: loading: 'lazy' }} - {%- endif -%} -
- {{ item.product.title | escape }}
- {{ item.variant.title }} -
-
-
- - {{ item.final_line_price | money }}
- - -
- {%- endif -%} -
-
- -{% schema %} -{ - "name": "Main cart", - "settings": [] -} -{% endschema %} +

{{ 'cart.title' | t }}

{%- if cart.empty? -%}{%- else -%}
{%- for item in cart.items -%}{%- endfor -%}
ProductQuantityTotal
{%- if item.image -%}{{ item.image | image_url: width: 200 | image_tag: loading: 'lazy' }}{%- endif -%}
{{ item.product.title | escape }}
{{ item.variant.title }}
{{ item.final_line_price | money }}
{%- endif -%}
+{% schema %}{"name":"Main cart","settings":[]}{% endschema %} diff --git a/packages/create-liquid-loom/template/src/theme/sections/collections/main-collection.liquid b/packages/create-liquid-loom/template/src/theme/sections/collections/main-collection.liquid index c77edf1..47995b7 100644 --- a/packages/create-liquid-loom/template/src/theme/sections/collections/main-collection.liquid +++ b/packages/create-liquid-loom/template/src/theme/sections/collections/main-collection.liquid @@ -1,113 +1,2 @@ -
-
-
-

Collection

-

{{ collection.title | escape }}

- {%- if collection.description != blank -%} -
{{ collection.description }}
- {%- endif -%} -
- - {%- if collection.filters.size > 0 -%} -
- {%- for filter in collection.filters -%} -
- - {{ filter.label | escape -}} - {%- if filter.active_values.size > 0 %} ({{ filter.active_values.size }}){% endif %} - -
- {%- case filter.type -%} - {%- when 'boolean', 'list' -%} - {%- for value in filter.values -%} - - {%- endfor -%} - {%- when 'price_range' -%} - - - {%- endcase -%} -
-
- {%- endfor -%} - - Clear -
- {%- endif -%} - - {%- paginate collection.products by section.settings.products_per_page -%} - - - {%- if paginate.pages > 1 -%} -
{{ paginate | default_pagination }}
- {%- endif -%} - {%- endpaginate -%} -
-
- -{% schema %} -{ - "name": "Main collection", - "settings": [ - { - "type": "range", - "id": "products_per_page", - "min": 8, - "max": 32, - "step": 4, - "label": "Products per page", - "default": 16 - } - ] -} -{% endschema %} +

Collection

{{ collection.title | escape }}

{%- if collection.description != blank -%}
{{ collection.description }}
{%- endif -%}
{%- if collection.filters.size > 0 -%}
{%- for filter in collection.filters -%}
{{ filter.label | escape }}{%- if filter.active_values.size > 0 %} ({{ filter.active_values.size }}){% endif %}
{%- case filter.type -%}{%- when 'boolean', 'list' -%}{%- for value in filter.values -%}{%- endfor -%}{%- when 'price_range' -%}{%- endcase -%}
{%- endfor -%}Clear
{%- endif -%}{%- paginate collection.products by section.settings.products_per_page -%}{%- if paginate.pages > 1 -%}
{{ paginate | default_pagination }}
{%- endif -%}{%- endpaginate -%}
+{% schema %}{"name":"Main collection","settings":[{"type":"range","id":"products_per_page","min":8,"max":32,"step":4,"label":"Products per page","default":16}]}{% endschema %} diff --git a/packages/create-liquid-loom/template/src/theme/sections/global/header.liquid b/packages/create-liquid-loom/template/src/theme/sections/global/header.liquid index 6248eaa..a311dc3 100644 --- a/packages/create-liquid-loom/template/src/theme/sections/global/header.liquid +++ b/packages/create-liquid-loom/template/src/theme/sections/global/header.liquid @@ -1,59 +1,14 @@ - -{% schema %} -{ - "name": "Header", - "settings": [ - { "type": "image_picker", "id": "logo", "label": "Logo" }, - { "type": "link_list", "id": "menu", "label": "Menu", "default": "main-menu" } - ] -} -{% endschema %} +{% stylesheet %}.site-header{position:sticky;top:0;z-index:40;border-bottom:1px solid color-mix(in srgb,var(--color-border) 75%,transparent);background:color-mix(in srgb,var(--color-background) 88%,transparent);backdrop-filter:blur(18px)}.site-header__inner{display:grid;min-height:4.8rem;grid-template-columns:1fr auto 1fr;align-items:center;gap:1rem}.site-header__brand{justify-self:center;font-size:1.1rem;font-weight:800;letter-spacing:-.03em;text-decoration:none}.site-header__logo{width:auto;max-height:2.3rem}.site-header__nav,.site-header__actions{display:flex;align-items:center;gap:1.25rem}.site-header__nav a,.site-header__actions a{font-size:.9rem;font-weight:650;text-decoration:none}.site-header__actions{justify-self:end}.site-header__menu-button{display:none;border:0;background:transparent;cursor:pointer;padding:.5rem}.mobile-menu{border-top:1px solid var(--color-border);background:var(--color-background);padding:1rem}.mobile-menu[hidden]{display:none}.mobile-menu a{display:block;padding:.8rem;font-size:1.15rem;font-weight:700;text-decoration:none}shopify-account{--shopify-account-signed-in-avatar-size:2rem;--shopify-account-signed-in-avatar-color-background:var(--color-accent);--shopify-account-signed-in-avatar-color-text:var(--color-accent-contrast)}@media(max-width:900px){.site-header__nav{display:none}.site-header__menu-button{display:inline-flex}}@media(max-width:600px){.site-header__actions>a:first-child{display:none}}{% endstylesheet %} +{% javascript %}class MenuDisclosure extends HTMLElement{connectedCallback(){this.button=this.querySelector("button");this.panel=this.querySelector("[data-menu-panel]");if(!this.button||!this.panel)return;this.button.addEventListener("click",()=>this.toggle());this.addEventListener("keydown",event=>{if(event.key==="Escape"&&this.isOpen()){this.close();this.button.focus()}})}isOpen(){return this.button.getAttribute("aria-expanded")==="true"}toggle(){this.isOpen()?this.close():this.open()}open(){this.button.setAttribute("aria-expanded","true");this.panel.removeAttribute("hidden")}close(){this.button.setAttribute("aria-expanded","false");this.panel.setAttribute("hidden","")}}if(!customElements.get("menu-disclosure"))customElements.define("menu-disclosure",MenuDisclosure);{% endjavascript %} +{% schema %}{"name":"Header","settings":[{"type":"image_picker","id":"logo","label":"Logo"},{"type":"link_list","id":"menu","label":"Menu","default":"main-menu"},{"type":"link_list","id":"customer_account_menu","label":"Customer account menu","default":"customer-account-main-menu"}]}{% endschema %} diff --git a/packages/create-liquid-loom/template/src/theme/sections/home/featured-collection.liquid b/packages/create-liquid-loom/template/src/theme/sections/home/featured-collection.liquid index 2d10a33..ab16e44 100644 --- a/packages/create-liquid-loom/template/src/theme/sections/home/featured-collection.liquid +++ b/packages/create-liquid-loom/template/src/theme/sections/home/featured-collection.liquid @@ -1,64 +1,2 @@ -{%- assign featured_collection = section.settings.collection -%} - -
-
-
-
-

{{ section.settings.eyebrow | escape }}

-

{{ section.settings.heading | escape }}

-
- {%- if featured_collection != blank -%} - {{ 'actions.view_all' | t }} - {%- endif -%} -
- -
- {%- if featured_collection != blank -%} - {%- for product in featured_collection.products limit: section.settings.products_to_show -%} - - {%- endfor -%} - {%- else -%} - {%- for index in (1..4) -%} -
-
{{ 'product-' | append: index | placeholder_svg_tag }}
-
-

Example product

- {{ 2400 | money }} -
-
- {%- endfor -%} - {%- endif -%} -
-
-
- -{% schema %} -{ - "name": "Featured collection", - "settings": [ - { "type": "text", "id": "eyebrow", "label": "Eyebrow", "default": "Selected for you" }, - { "type": "text", "id": "heading", "label": "Heading", "default": "Products worth a closer look" }, - { "type": "collection", "id": "collection", "label": "Collection" }, - { "type": "range", "id": "products_to_show", "min": 2, "max": 8, "step": 1, "label": "Products", "default": 4 } - ], - "presets": [{ "name": "Featured collection" }] -} -{% endschema %} +{%- assign featured_collection = section.settings.collection -%}

{{ section.settings.eyebrow | escape }}

{{ section.settings.heading | escape }}

{%- if featured_collection != blank -%}{{ 'actions.view_all' | t }}{%- endif -%}
{%- if featured_collection != blank -%}{%- for product in featured_collection.products limit: section.settings.products_to_show -%}{%- endfor -%}{%- else -%}{%- for index in (1..4) -%}
{{ 'product-' | append: index | placeholder_svg_tag }}

Example product

{{ 2400 | money }}
{%- endfor -%}{%- endif -%}
+{% schema %}{"name":"Featured collection","settings":[{"type":"text","id":"eyebrow","label":"Eyebrow","default":"Selected for you"},{"type":"text","id":"heading","label":"Heading","default":"Products worth a closer look"},{"type":"collection","id":"collection","label":"Collection"},{"type":"range","id":"products_to_show","min":2,"max":8,"step":1,"label":"Products","default":4}],"presets":[{"name":"Featured collection"}]}{% endschema %} diff --git a/packages/create-liquid-loom/template/src/theme/sections/products/main-product.liquid b/packages/create-liquid-loom/template/src/theme/sections/products/main-product.liquid index fe76eb9..88d7044 100644 --- a/packages/create-liquid-loom/template/src/theme/sections/products/main-product.liquid +++ b/packages/create-liquid-loom/template/src/theme/sections/products/main-product.liquid @@ -1,100 +1,3 @@ -
-
- - - - {%- if product.vendor != blank -%} -

{{ product.vendor | escape }}

- {%- endif -%} -

{{ product.title | escape }}

- {% render 'price', product: product %} - - {%- form 'product', product, id: 'ProductForm', novalidate: 'novalidate', data-product-form: '' -%} - {%- unless product.has_only_default_variant -%} -
- - -
- {%- else -%} - - {%- endunless -%} - - {%- if product.selling_plan_groups.size > 0 -%} -
- - -
- {%- endif -%} - -
- - - - - - -
- - - {%- endform -%} - - - {%- if product.description != blank -%} -
{{ product.description }}
- {%- endif -%} -
-
-
- -{% schema %} -{ - "name": "Main product", - "settings": [] -} -{% endschema %} +
{% content_for 'blocks' %}
+{% stylesheet %}.product{display:grid;grid-template-columns:minmax(0,1.1fr) minmax(320px,.9fr);gap:clamp(2rem,7vw,7rem)}.product__gallery{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:1rem}.product__gallery img{width:100%;border-radius:var(--radius-md)}.product__details{display:grid;align-self:start;gap:1rem}.product__details .price{display:block;font-size:1.2rem}@media(max-width:900px){.product{grid-template-columns:1fr}}@media(max-width:600px){.product__gallery{grid-template-columns:1fr}}{% endstylesheet %} +{% schema %}{"name":"Main product","blocks":[{"type":"@app"},{"type":"@theme"},{"type":"product-vendor"},{"type":"product-title"},{"type":"product-price"},{"type":"product-purchase"},{"type":"product-description"}],"settings":[]}{% endschema %} diff --git a/packages/create-liquid-loom/template/src/theme/sections/search/main-search.liquid b/packages/create-liquid-loom/template/src/theme/sections/search/main-search.liquid index 4c9395e..80e62f0 100644 --- a/packages/create-liquid-loom/template/src/theme/sections/search/main-search.liquid +++ b/packages/create-liquid-loom/template/src/theme/sections/search/main-search.liquid @@ -1,55 +1,4 @@ -
-
-
-

Search

-

Find what you need

- -
-
- - -
- -
-
-
-
- - {%- if search.performed -%} -
- {%- for item in search.results -%} - - {%- else -%} -

No results for “{{ search.terms | escape }}”.

- {%- endfor -%} -
- {%- endif -%} -
-
- -{% schema %} -{ - "name": "Main search", - "settings": [] -} -{% endschema %} +

Search

Find what you need

{%- if search.performed -%}
{%- for item in search.results -%}{%- else -%}

No results for “{{ search.terms | escape }}”.

{%- endfor -%}
{%- endif -%}
+{% stylesheet %}.search-field{display:grid;gap:.45rem;margin-block:1rem}.search-field label{font-size:.85rem;font-weight:700}.search-field input{width:100%;min-height:3rem;border:1px solid var(--color-border);border-radius:var(--radius-sm);background:var(--color-surface);padding:.7rem .85rem}.predictive-search__results{border:1px solid var(--color-border);border-radius:var(--radius-md);margin-top:.5rem;background:var(--color-surface);box-shadow:var(--shadow-soft);padding:1rem}.predictive-search__results ul{display:grid;gap:.65rem;margin:0;padding:0;list-style:none}.predictive-search__results a{display:flex;justify-content:space-between;gap:1rem;text-decoration:none}{% endstylesheet %} +{% javascript %}class PredictiveSearch extends HTMLElement{connectedCallback(){this.input=this.querySelector("input[type='search']");this.results=this.querySelector("[data-predictive-search-results]");if(!this.input||!this.results)return;this.input.addEventListener("input",()=>{clearTimeout(this.timer);this.timer=setTimeout(()=>this.search(),180)})}async search(){const query=this.input.value.trim();this.controller?.abort();if(query.length<2){this.results.replaceChildren();this.input.setAttribute("aria-expanded","false");return}this.controller=new AbortController();try{const response=await fetch(`/search/suggest?q=${encodeURIComponent(query)}&resources[type]=product§ion_id=predictive-search`,{signal:this.controller.signal});if(!response.ok)throw new Error(`Search request failed: ${response.status}`);const documentFragment=new DOMParser().parseFromString(await response.text(),"text/html");const content=documentFragment.querySelector(".predictive-search__results");this.results.replaceChildren(...(content?[content]:[]));this.input.setAttribute("aria-expanded",content?"true":"false")}catch(error){if(error.name!=="AbortError")console.error(error)}}}if(!customElements.get("predictive-search"))customElements.define("predictive-search",PredictiveSearch);{% endjavascript %} +{% schema %}{"name":"Main search","settings":[]}{% endschema %} diff --git a/packages/create-liquid-loom/template/src/theme/templates/product.json b/packages/create-liquid-loom/template/src/theme/templates/product.json index 7701460..bedd194 100644 --- a/packages/create-liquid-loom/template/src/theme/templates/product.json +++ b/packages/create-liquid-loom/template/src/theme/templates/product.json @@ -1,9 +1 @@ -{ - "sections": { - "main": { - "type": "main-product", - "settings": {} - } - }, - "order": ["main"] -} +{"sections":{"main":{"type":"main-product","blocks":{"vendor":{"type":"product-vendor","settings":{}},"title":{"type":"product-title","settings":{}},"price":{"type":"product-price","settings":{}},"purchase":{"type":"product-purchase","settings":{}},"description":{"type":"product-description","settings":{}}},"block_order":["vendor","title","price","purchase","description"],"settings":{}}},"order":["main"]} diff --git a/packages/create-liquid-loom/template/vite.config.js b/packages/create-liquid-loom/template/vite.config.js deleted file mode 100644 index 724c668..0000000 --- a/packages/create-liquid-loom/template/vite.config.js +++ /dev/null @@ -1,31 +0,0 @@ -import path from "node:path"; -import { fileURLToPath } from "node:url"; - -import tailwindcss from "@tailwindcss/vite"; -import { defineConfig } from "vite"; - -const projectRoot = path.dirname(fileURLToPath(import.meta.url)); - -export default defineConfig(({ mode }) => ({ - base: "", - plugins: [tailwindcss()], - build: { - assetsDir: "", - cssCodeSplit: false, - emptyOutDir: false, - minify: mode === "production" ? "esbuild" : false, - outDir: path.join(projectRoot, "dist", "theme", "assets"), - rollupOptions: { - input: path.join(projectRoot, "src", "entrypoints", "theme.js"), - output: { - assetFileNames: "[name][extname]", - entryFileNames: "theme.js" - } - }, - sourcemap: mode !== "production", - target: "baseline-widely-available" - }, - css: { - devSourcemap: mode !== "production" - } -})); diff --git a/src/entrypoints/theme.js b/src/entrypoints/theme.js deleted file mode 100644 index fd16943..0000000 --- a/src/entrypoints/theme.js +++ /dev/null @@ -1,118 +0,0 @@ -import "../styles/theme.css"; - -class MenuDisclosure extends HTMLElement { - connectedCallback() { - this.button = this.querySelector("button"); - this.panel = this.querySelector("[data-menu-panel]"); - if (!this.button || !this.panel) return; - - this.button.addEventListener("click", () => this.toggle()); - this.addEventListener("keydown", (event) => { - if (event.key === "Escape" && this.isOpen()) { - this.close(); - this.button.focus(); - } - }); - } - - isOpen() { - return this.button.getAttribute("aria-expanded") === "true"; - } - - toggle() { - this.isOpen() ? this.close() : this.open(); - } - - open() { - this.button.setAttribute("aria-expanded", "true"); - this.panel.removeAttribute("hidden"); - document.documentElement.classList.add("menu-open"); - } - - close() { - this.button.setAttribute("aria-expanded", "false"); - this.panel.setAttribute("hidden", ""); - document.documentElement.classList.remove("menu-open"); - } -} - -class QuantityInput extends HTMLElement { - connectedCallback() { - this.input = this.querySelector("input[type='number']"); - this.querySelectorAll("button").forEach((button) => { - button.addEventListener("click", () => this.change(button.dataset.quantityAction)); - }); - } - - change(action) { - if (!this.input) return; - const step = Number(this.input.step) || 1; - const minimum = Number(this.input.min) || 1; - const current = Number(this.input.value) || minimum; - this.input.value = String(Math.max(minimum, current + (action === "increase" ? step : -step))); - this.input.dispatchEvent(new Event("change", { bubbles: true })); - } -} - -class PredictiveSearch extends HTMLElement { - connectedCallback() { - this.input = this.querySelector("input[type='search']"); - this.results = this.querySelector("[data-predictive-search-results]"); - if (!this.input || !this.results) return; - this.input.addEventListener("input", () => { - clearTimeout(this.timer); - this.timer = setTimeout(() => this.search(), 180); - }); - } - - async search() { - const query = this.input.value.trim(); - this.controller?.abort(); - if (query.length < 2) { - this.results.replaceChildren(); - this.input.setAttribute("aria-expanded", "false"); - return; - } - - this.controller = new AbortController(); - try { - const url = `/search/suggest?q=${encodeURIComponent(query)}&resources[type]=product§ion_id=predictive-search`; - const response = await fetch(url, { signal: this.controller.signal }); - if (!response.ok) throw new Error(`Search request failed: ${response.status}`); - const document = new DOMParser().parseFromString(await response.text(), "text/html"); - const content = document.querySelector(".predictive-search__results"); - this.results.replaceChildren(...(content ? [content] : [])); - this.input.setAttribute("aria-expanded", content ? "true" : "false"); - } catch (error) { - if (error.name !== "AbortError") console.error(error); - } - } -} - -class ProductForm extends HTMLElement { - connectedCallback() { - this.select = this.querySelector("[data-variant-select]"); - this.submit = this.querySelector("button[type='submit']"); - const data = this.querySelector("[data-product-variants]"); - if (!this.select || !data) return; - this.variants = JSON.parse(data.textContent); - this.select.addEventListener("change", () => this.variantChanged()); - } - - variantChanged() { - const variant = this.variants.find(({ id }) => String(id) === this.select.value); - if (!variant) return; - this.submit?.toggleAttribute("disabled", !variant.available); - const url = new URL(window.location.href); - url.searchParams.set("variant", variant.id); - window.history.replaceState({}, "", url); - } -} - -if (!customElements.get("menu-disclosure")) customElements.define("menu-disclosure", MenuDisclosure); -if (!customElements.get("quantity-input")) customElements.define("quantity-input", QuantityInput); -if (!customElements.get("predictive-search")) customElements.define("predictive-search", PredictiveSearch); -if (!customElements.get("product-form")) customElements.define("product-form", ProductForm); - -document.documentElement.classList.remove("no-js"); -document.documentElement.classList.add("js"); diff --git a/src/styles/theme.css b/src/public/theme.css similarity index 70% rename from src/styles/theme.css rename to src/public/theme.css index 3ee152c..64901c6 100644 --- a/src/styles/theme.css +++ b/src/public/theme.css @@ -1,6 +1,3 @@ -@import "tailwindcss"; -@source "../theme/**/*.liquid"; - :root { --color-background: #f8f7f2; --color-foreground: #111820; @@ -30,14 +27,7 @@ body { margin: 0; background: var(--color-background); color: var(--color-foreground); - font-family: - Inter, - ui-sans-serif, - system-ui, - -apple-system, - BlinkMacSystemFont, - "Segoe UI", - sans-serif; + font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; font-size: 1rem; line-height: 1.6; text-rendering: optimizeLegibility; @@ -156,9 +146,7 @@ textarea { font-weight: 700; padding: 0.75rem 1.35rem; text-decoration: none; - transition: - transform 160ms ease, - box-shadow 160ms ease; + transition: transform 160ms ease, box-shadow 160ms ease; } .button:hover { @@ -185,80 +173,6 @@ textarea { text-align: center; } -.site-header { - position: sticky; - top: 0; - z-index: 40; - border-bottom: 1px solid color-mix(in srgb, var(--color-border) 75%, transparent); - background: color-mix(in srgb, var(--color-background) 88%, transparent); - backdrop-filter: blur(18px); -} - -.site-header__inner { - display: grid; - min-height: 4.8rem; - grid-template-columns: 1fr auto 1fr; - align-items: center; - gap: 1rem; -} - -.site-header__brand { - justify-self: center; - font-size: 1.1rem; - font-weight: 800; - letter-spacing: -0.03em; - text-decoration: none; -} - -.site-header__logo { - width: auto; - max-height: 2.3rem; -} - -.site-header__nav, -.site-header__actions { - display: flex; - align-items: center; - gap: 1.25rem; -} - -.site-header__nav a, -.site-header__actions a { - font-size: 0.9rem; - font-weight: 650; - text-decoration: none; -} - -.site-header__actions { - justify-self: end; -} - -.site-header__menu-button { - display: none; - border: 0; - background: transparent; - cursor: pointer; - padding: 0.5rem; -} - -.mobile-menu { - border-top: 1px solid var(--color-border); - background: var(--color-background); - padding: 1rem; -} - -.mobile-menu[hidden] { - display: none; -} - -.mobile-menu a { - display: block; - padding: 0.8rem; - font-size: 1.15rem; - font-weight: 700; - text-decoration: none; -} - .hero { position: relative; overflow: hidden; @@ -394,80 +308,32 @@ textarea { text-decoration: line-through; } -.product { - display: grid; - grid-template-columns: minmax(0, 1.1fr) minmax(320px, 0.9fr); - gap: clamp(2rem, 7vw, 7rem); -} - -.product__gallery { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 1rem; -} - -.product__gallery img { - width: 100%; - border-radius: var(--radius-md); -} - -.product__details { - align-self: start; -} - -.product__details .price { - display: block; - margin-block: 1rem; - font-size: 1.2rem; -} - -.field { - display: grid; - gap: 0.45rem; - margin-block: 1rem; +.collection-header { + max-width: 48rem; + margin-bottom: 3rem; } -.field label { - font-size: 0.85rem; - font-weight: 700; +.facets { + display: flex; + flex-wrap: wrap; + align-items: start; + gap: 0.75rem; + margin-bottom: 2rem; } -.field input, -.field select, -.field textarea { - width: 100%; - min-height: 3rem; +.facet { + position: relative; border: 1px solid var(--color-border); border-radius: var(--radius-sm); background: var(--color-surface); - padding: 0.7rem 0.85rem; + padding: 0.65rem 0.8rem; } -.quantity { +.facet__options { display: grid; - width: 9rem; - grid-template-columns: 2.6rem 1fr 2.6rem; - border: 1px solid var(--color-border); - border-radius: 999px; - background: var(--color-surface); -} - -.quantity button, -.quantity input { - min-height: 2.8rem; - border: 0; - background: transparent; - text-align: center; -} - -.quantity input { - width: 100%; - appearance: textfield; -} - -.collection-header { - max-width: 48rem; - margin-bottom: 3rem; + min-width: 14rem; + gap: 0.55rem; + padding-top: 0.75rem; } .cart-table { @@ -525,53 +391,6 @@ textarea { border-radius: var(--radius-lg); } -.facets { - display: flex; - flex-wrap: wrap; - align-items: start; - gap: 0.75rem; - margin-bottom: 2rem; -} - -.facet { - position: relative; - border: 1px solid var(--color-border); - border-radius: var(--radius-sm); - background: var(--color-surface); - padding: 0.65rem 0.8rem; -} - -.facet__options { - display: grid; - min-width: 14rem; - gap: 0.55rem; - padding-top: 0.75rem; -} - -.predictive-search__results { - border: 1px solid var(--color-border); - border-radius: var(--radius-md); - margin-top: 0.5rem; - background: var(--color-surface); - box-shadow: var(--shadow-soft); - padding: 1rem; -} - -.predictive-search__results ul { - display: grid; - gap: 0.65rem; - margin: 0; - padding: 0; - list-style: none; -} - -.predictive-search__results a { - display: flex; - justify-content: space-between; - gap: 1rem; - text-decoration: none; -} - .empty-state { max-width: 38rem; margin-inline: auto; @@ -626,24 +445,8 @@ textarea { } @media (max-width: 900px) { - .site-header__inner { - grid-template-columns: 1fr auto 1fr; - } - - .site-header__nav { - display: none; - } - - .site-header__menu-button { - display: inline-flex; - } - - .hero__grid, - .product { - grid-template-columns: 1fr; - } - .hero__grid { + grid-template-columns: 1fr; padding-block: 4rem; } @@ -669,12 +472,7 @@ textarea { width: min(calc(100% - 1.25rem), var(--page-width)); } - .site-header__actions a:not([href*="cart"]) { - display: none; - } - .product-grid, - .product__gallery, .site-footer__grid { grid-template-columns: 1fr; } diff --git a/src/theme/blocks/product-description.liquid b/src/theme/blocks/product-description.liquid new file mode 100644 index 0000000..a6aedda --- /dev/null +++ b/src/theme/blocks/product-description.liquid @@ -0,0 +1,2 @@ +{%- if product.description != blank -%}
{{ product.description }}
{%- endif -%} +{% schema %}{"name":"Product description","presets":[{"name":"Product description"}]}{% endschema %} diff --git a/src/theme/blocks/product-price.liquid b/src/theme/blocks/product-price.liquid new file mode 100644 index 0000000..475a02f --- /dev/null +++ b/src/theme/blocks/product-price.liquid @@ -0,0 +1,2 @@ +
{% render 'price', product: product %}
+{% schema %}{"name":"Product price","presets":[{"name":"Product price"}]}{% endschema %} diff --git a/src/theme/blocks/product-purchase.liquid b/src/theme/blocks/product-purchase.liquid new file mode 100644 index 0000000..74f2b77 --- /dev/null +++ b/src/theme/blocks/product-purchase.liquid @@ -0,0 +1,88 @@ + + {%- form 'product', product, id: 'ProductForm-' | append: block.id, novalidate: 'novalidate', data-product-form: '' -%} + {%- unless product.has_only_default_variant -%} +
+ {%- else -%}{%- endunless -%} + {%- if product.selling_plan_groups.size > 0 -%}
{%- endif -%} +
+ +

+ {%- endform -%} + +
+ +{% stylesheet %} +.field { display: grid; gap: 0.45rem; margin-block: 1rem; } +.field label { font-size: 0.85rem; font-weight: 700; } +.field input, .field select, .field textarea { width: 100%; min-height: 3rem; border: 1px solid var(--color-border); border-radius: var(--radius-sm); background: var(--color-surface); padding: 0.7rem 0.85rem; } +.quantity { display: grid; width: 9rem; grid-template-columns: 2.6rem 1fr 2.6rem; border: 1px solid var(--color-border); border-radius: 999px; background: var(--color-surface); } +.quantity button, .quantity input { min-height: 2.8rem; border: 0; background: transparent; text-align: center; } +.quantity input { width: 100%; appearance: textfield; } +.product-form__status { min-height: 1.5em; margin-block: 0.75rem 0; } +{% endstylesheet %} + +{% javascript %} +class QuantityInput extends HTMLElement { + connectedCallback() { + this.input = this.querySelector("input[type='number']"); + this.querySelectorAll("button").forEach((button) => button.addEventListener("click", () => this.change(button.dataset.quantityAction))); + } + change(action) { + if (!this.input) return; + const step = Number(this.input.step) || 1; + const minimum = Number(this.input.min) || 1; + const current = Number(this.input.value) || minimum; + this.input.value = String(Math.max(minimum, current + (action === "increase" ? step : -step))); + this.input.dispatchEvent(new Event("change", { bubbles: true })); + } +} + +class ProductForm extends HTMLElement { + connectedCallback() { + this.form = this.querySelector("form"); + this.select = this.querySelector("[data-variant-select]"); + this.submit = this.querySelector("button[type='submit']"); + this.status = this.querySelector("[data-product-form-status]"); + const data = this.querySelector("[data-product-variants]"); + if (data) this.variants = JSON.parse(data.textContent); + this.select?.addEventListener("change", () => this.variantChanged()); + this.form?.addEventListener("submit", (event) => this.submitWithStandardAction(event)); + } + variantChanged() { + const variant = this.variants?.find(({ id }) => String(id) === this.select.value); + if (!variant) return; + this.submit?.toggleAttribute("disabled", !variant.available); + const url = new URL(window.location.href); + url.searchParams.set("variant", variant.id); + window.history.replaceState({}, "", url); + } + async submitWithStandardAction(event) { + if (!window.Shopify?.actions?.updateCart || !this.form) return; + event.preventDefault(); + const formData = new FormData(this.form); + const merchandiseId = formData.get("id"); + if (!merchandiseId) return; + const line = { merchandiseId: String(merchandiseId), quantity: Math.max(1, Number(formData.get("quantity")) || 1) }; + const sellingPlanId = formData.get("selling_plan"); + if (sellingPlanId) line.sellingPlanId = String(sellingPlanId); + this.submit?.setAttribute("disabled", ""); + if (this.status) this.status.textContent = ""; + try { + const { cart, userErrors, warnings } = await window.Shopify.actions.updateCart({ lines: [line] }, { event: { context: "product" } }); + if (userErrors?.length) { if (this.status) this.status.textContent = userErrors[0].message; return; } + if (this.status) this.status.textContent = warnings?.[0]?.message || "Added to cart."; + if (cart) document.querySelectorAll("[data-cart-count]").forEach((node) => { node.textContent = String(cart.totalQuantity); }); + } catch (error) { + console.error(error); + if (this.status) this.status.textContent = "Unable to update the cart. Try again."; + } finally { + const variant = this.variants?.find(({ id }) => String(id) === String(formData.get("id"))); + this.submit?.toggleAttribute("disabled", variant ? !variant.available : false); + } + } +} +if (!customElements.get("quantity-input")) customElements.define("quantity-input", QuantityInput); +if (!customElements.get("product-form")) customElements.define("product-form", ProductForm); +{% endjavascript %} + +{% schema %}{"name":"Product purchase","presets":[{"name":"Product purchase"}]}{% endschema %} diff --git a/src/theme/blocks/product-title.liquid b/src/theme/blocks/product-title.liquid new file mode 100644 index 0000000..2be29a4 --- /dev/null +++ b/src/theme/blocks/product-title.liquid @@ -0,0 +1,2 @@ +

{{ product.title | escape }}

+{% schema %}{"name":"Product title","presets":[{"name":"Product title"}]}{% endschema %} diff --git a/src/theme/blocks/product-vendor.liquid b/src/theme/blocks/product-vendor.liquid new file mode 100644 index 0000000..df953df --- /dev/null +++ b/src/theme/blocks/product-vendor.liquid @@ -0,0 +1,2 @@ +{%- if product.vendor != blank -%}

{{ product.vendor | escape }}

{%- endif -%} +{% schema %}{"name":"Product vendor","presets":[{"name":"Product vendor"}]}{% endschema %} diff --git a/src/theme/config/settings_data.json b/src/theme/config/settings_data.json index 5f90608..03b5e60 100644 --- a/src/theme/config/settings_data.json +++ b/src/theme/config/settings_data.json @@ -1,19 +1,9 @@ { "current": { - "color_background": "#f8f7f2", - "color_foreground": "#111820", - "color_surface": "#ffffff", - "color_accent": "#176c5b", - "color_accent_contrast": "#ffffff", "page_width": 1200 }, "presets": { "Default": { - "color_background": "#f8f7f2", - "color_foreground": "#111820", - "color_surface": "#ffffff", - "color_accent": "#176c5b", - "color_accent_contrast": "#ffffff", "page_width": 1200 } } diff --git a/src/theme/config/settings_schema.json b/src/theme/config/settings_schema.json index f13f9bf..285db0c 100644 --- a/src/theme/config/settings_schema.json +++ b/src/theme/config/settings_schema.json @@ -17,11 +17,19 @@ { "name": "Colors", "settings": [ - { "type": "color", "id": "color_background", "label": "Background", "default": "#f8f7f2" }, - { "type": "color", "id": "color_foreground", "label": "Text", "default": "#111820" }, - { "type": "color", "id": "color_surface", "label": "Surface", "default": "#ffffff" }, - { "type": "color", "id": "color_accent", "label": "Accent", "default": "#176c5b" }, - { "type": "color", "id": "color_accent_contrast", "label": "Accent text", "default": "#ffffff" } + { + "type": "color_palette", + "id": "colors", + "default": { + "background": "#F8F7F2", + "foreground": "#111820", + "surface": "#FFFFFF", + "muted": "#66717C", + "border": "#DCE1DD", + "accent": "#176C5B", + "accent_contrast": "#FFFFFF" + } + } ] } ] diff --git a/src/theme/layout/theme.liquid b/src/theme/layout/theme.liquid index af4d7ba..640a028 100644 --- a/src/theme/layout/theme.liquid +++ b/src/theme/layout/theme.liquid @@ -1,24 +1,25 @@ - + - + {% render 'meta-tags' %} - {{ 'style.css' | asset_url | stylesheet_tag: preload: true }} - + {{ 'theme.css' | asset_url | stylesheet_tag: preload: true }} @@ -28,13 +29,8 @@ - {% sections 'header-group' %} - -
- {{ content_for_layout }} -
- +
{{ content_for_layout }}
{% sections 'footer-group' %} diff --git a/src/theme/sections/cart/main-cart.liquid b/src/theme/sections/cart/main-cart.liquid index 2e98701..530a305 100644 --- a/src/theme/sections/cart/main-cart.liquid +++ b/src/theme/sections/cart/main-cart.liquid @@ -1,64 +1,2 @@ -
-
-

{{ 'cart.title' | t }}

- - {%- if cart.empty? -%} -
-

{{ 'cart.empty' | t }}

- {{ 'actions.continue_shopping' | t }} -
- {%- else -%} -
- - - - - - - - - - {%- for item in cart.items -%} - - - - - - {%- endfor -%} - -
ProductQuantityTotal
-
- {%- if item.image -%} - {{ item.image | image_url: width: 200 | image_tag: loading: 'lazy' }} - {%- endif -%} -
- {{ item.product.title | escape }}
- {{ item.variant.title }} -
-
-
- - {{ item.final_line_price | money }}
- - -
- {%- endif -%} -
-
- -{% schema %} -{ - "name": "Main cart", - "settings": [] -} -{% endschema %} +

{{ 'cart.title' | t }}

{%- if cart.empty? -%}{%- else -%}
{%- for item in cart.items -%}{%- endfor -%}
ProductQuantityTotal
{%- if item.image -%}{{ item.image | image_url: width: 200 | image_tag: loading: 'lazy' }}{%- endif -%}
{{ item.product.title | escape }}
{{ item.variant.title }}
{{ item.final_line_price | money }}
{%- endif -%}
+{% schema %}{"name":"Main cart","settings":[]}{% endschema %} diff --git a/src/theme/sections/collections/main-collection.liquid b/src/theme/sections/collections/main-collection.liquid index c77edf1..c449ffa 100644 --- a/src/theme/sections/collections/main-collection.liquid +++ b/src/theme/sections/collections/main-collection.liquid @@ -1,113 +1,8 @@
-
-

Collection

-

{{ collection.title | escape }}

- {%- if collection.description != blank -%} -
{{ collection.description }}
- {%- endif -%} -
- - {%- if collection.filters.size > 0 -%} -
- {%- for filter in collection.filters -%} -
- - {{ filter.label | escape -}} - {%- if filter.active_values.size > 0 %} ({{ filter.active_values.size }}){% endif %} - -
- {%- case filter.type -%} - {%- when 'boolean', 'list' -%} - {%- for value in filter.values -%} - - {%- endfor -%} - {%- when 'price_range' -%} - - - {%- endcase -%} -
-
- {%- endfor -%} - - Clear -
- {%- endif -%} - - {%- paginate collection.products by section.settings.products_per_page -%} - - - {%- if paginate.pages > 1 -%} -
{{ paginate | default_pagination }}
- {%- endif -%} - {%- endpaginate -%} +

Collection

{{ collection.title | escape }}

{%- if collection.description != blank -%}
{{ collection.description }}
{%- endif -%}
+ {%- if collection.filters.size > 0 -%}
{%- for filter in collection.filters -%}
{{ filter.label | escape }}{%- if filter.active_values.size > 0 %} ({{ filter.active_values.size }}){% endif %}
{%- case filter.type -%}{%- when 'boolean', 'list' -%}{%- for value in filter.values -%}{%- endfor -%}{%- when 'price_range' -%}{%- endcase -%}
{%- endfor -%}Clear
{%- endif -%} + {%- paginate collection.products by section.settings.products_per_page -%}{%- if paginate.pages > 1 -%}
{{ paginate | default_pagination }}
{%- endif -%}{%- endpaginate -%}
- -{% schema %} -{ - "name": "Main collection", - "settings": [ - { - "type": "range", - "id": "products_per_page", - "min": 8, - "max": 32, - "step": 4, - "label": "Products per page", - "default": 16 - } - ] -} -{% endschema %} +{% schema %}{"name":"Main collection","settings":[{"type":"range","id":"products_per_page","min":8,"max":32,"step":4,"label":"Products per page","default":16}]}{% endschema %} diff --git a/src/theme/sections/global/header.liquid b/src/theme/sections/global/header.liquid index 6248eaa..f0a2e93 100644 --- a/src/theme/sections/global/header.liquid +++ b/src/theme/sections/global/header.liquid @@ -2,58 +2,72 @@ - +{% stylesheet %} +.site-header { position: sticky; top: 0; z-index: 40; border-bottom: 1px solid color-mix(in srgb, var(--color-border) 75%, transparent); background: color-mix(in srgb, var(--color-background) 88%, transparent); backdrop-filter: blur(18px); } +.site-header__inner { display: grid; min-height: 4.8rem; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 1rem; } +.site-header__brand { justify-self: center; font-size: 1.1rem; font-weight: 800; letter-spacing: -0.03em; text-decoration: none; } +.site-header__logo { width: auto; max-height: 2.3rem; } +.site-header__nav, .site-header__actions { display: flex; align-items: center; gap: 1.25rem; } +.site-header__nav a, .site-header__actions a { font-size: 0.9rem; font-weight: 650; text-decoration: none; } +.site-header__actions { justify-self: end; } +.site-header__menu-button { display: none; border: 0; background: transparent; cursor: pointer; padding: 0.5rem; } +.mobile-menu { border-top: 1px solid var(--color-border); background: var(--color-background); padding: 1rem; } +.mobile-menu[hidden] { display: none; } +.mobile-menu a { display: block; padding: 0.8rem; font-size: 1.15rem; font-weight: 700; text-decoration: none; } +shopify-account { --shopify-account-signed-in-avatar-size: 2rem; --shopify-account-signed-in-avatar-color-background: var(--color-accent); --shopify-account-signed-in-avatar-color-text: var(--color-accent-contrast); } +@media (max-width: 900px) { .site-header__nav { display: none; } .site-header__menu-button { display: inline-flex; } } +@media (max-width: 600px) { .site-header__actions > a:first-child { display: none; } } +{% endstylesheet %} + +{% javascript %} +class MenuDisclosure extends HTMLElement { + connectedCallback() { + this.button = this.querySelector("button"); + this.panel = this.querySelector("[data-menu-panel]"); + if (!this.button || !this.panel) return; + this.button.addEventListener("click", () => this.toggle()); + this.addEventListener("keydown", (event) => { + if (event.key === "Escape" && this.isOpen()) { this.close(); this.button.focus(); } + }); + } + isOpen() { return this.button.getAttribute("aria-expanded") === "true"; } + toggle() { this.isOpen() ? this.close() : this.open(); } + open() { this.button.setAttribute("aria-expanded", "true"); this.panel.removeAttribute("hidden"); } + close() { this.button.setAttribute("aria-expanded", "false"); this.panel.setAttribute("hidden", ""); } +} +if (!customElements.get("menu-disclosure")) customElements.define("menu-disclosure", MenuDisclosure); +{% endjavascript %} + {% schema %} { "name": "Header", "settings": [ { "type": "image_picker", "id": "logo", "label": "Logo" }, - { "type": "link_list", "id": "menu", "label": "Menu", "default": "main-menu" } + { "type": "link_list", "id": "menu", "label": "Menu", "default": "main-menu" }, + { "type": "link_list", "id": "customer_account_menu", "label": "Customer account menu", "default": "customer-account-main-menu" } ] } {% endschema %} diff --git a/src/theme/sections/home/featured-collection.liquid b/src/theme/sections/home/featured-collection.liquid index 2d10a33..6b8a2c8 100644 --- a/src/theme/sections/home/featured-collection.liquid +++ b/src/theme/sections/home/featured-collection.liquid @@ -1,64 +1,3 @@ {%- assign featured_collection = section.settings.collection -%} - -
-
-
-
-

{{ section.settings.eyebrow | escape }}

-

{{ section.settings.heading | escape }}

-
- {%- if featured_collection != blank -%} - {{ 'actions.view_all' | t }} - {%- endif -%} -
- -
- {%- if featured_collection != blank -%} - {%- for product in featured_collection.products limit: section.settings.products_to_show -%} - - {%- endfor -%} - {%- else -%} - {%- for index in (1..4) -%} -
-
{{ 'product-' | append: index | placeholder_svg_tag }}
-
-

Example product

- {{ 2400 | money }} -
-
- {%- endfor -%} - {%- endif -%} -
-
-
- -{% schema %} -{ - "name": "Featured collection", - "settings": [ - { "type": "text", "id": "eyebrow", "label": "Eyebrow", "default": "Selected for you" }, - { "type": "text", "id": "heading", "label": "Heading", "default": "Products worth a closer look" }, - { "type": "collection", "id": "collection", "label": "Collection" }, - { "type": "range", "id": "products_to_show", "min": 2, "max": 8, "step": 1, "label": "Products", "default": 4 } - ], - "presets": [{ "name": "Featured collection" }] -} -{% endschema %} +

{{ section.settings.eyebrow | escape }}

{{ section.settings.heading | escape }}

{%- if featured_collection != blank -%}{{ 'actions.view_all' | t }}{%- endif -%}
{%- if featured_collection != blank -%}{%- for product in featured_collection.products limit: section.settings.products_to_show -%}{%- endfor -%}{%- else -%}{%- for index in (1..4) -%}
{{ 'product-' | append: index | placeholder_svg_tag }}

Example product

{{ 2400 | money }}
{%- endfor -%}{%- endif -%}
+{% schema %}{"name":"Featured collection","settings":[{"type":"text","id":"eyebrow","label":"Eyebrow","default":"Selected for you"},{"type":"text","id":"heading","label":"Heading","default":"Products worth a closer look"},{"type":"collection","id":"collection","label":"Collection"},{"type":"range","id":"products_to_show","min":2,"max":8,"step":1,"label":"Products","default":4}],"presets":[{"name":"Featured collection"}]}{% endschema %} diff --git a/src/theme/sections/products/main-product.liquid b/src/theme/sections/products/main-product.liquid index fe76eb9..b15bf60 100644 --- a/src/theme/sections/products/main-product.liquid +++ b/src/theme/sections/products/main-product.liquid @@ -1,100 +1,38 @@ -
-
- -
+{% stylesheet %} +.product { display: grid; grid-template-columns: minmax(0, 1.1fr) minmax(320px, 0.9fr); gap: clamp(2rem, 7vw, 7rem); } +.product__gallery { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1rem; } +.product__gallery img { width: 100%; border-radius: var(--radius-md); } +.product__details { display: grid; align-self: start; gap: 1rem; } +.product__details .price { display: block; font-size: 1.2rem; } +@media (max-width: 900px) { .product { grid-template-columns: 1fr; } } +@media (max-width: 600px) { .product__gallery { grid-template-columns: 1fr; } } +{% endstylesheet %} {% schema %} { "name": "Main product", + "blocks": [ + { "type": "@app" }, + { "type": "@theme" }, + { "type": "product-vendor" }, + { "type": "product-title" }, + { "type": "product-price" }, + { "type": "product-purchase" }, + { "type": "product-description" } + ], "settings": [] } {% endschema %} diff --git a/src/theme/sections/search/main-search.liquid b/src/theme/sections/search/main-search.liquid index 4c9395e..261fd91 100644 --- a/src/theme/sections/search/main-search.liquid +++ b/src/theme/sections/search/main-search.liquid @@ -1,55 +1,46 @@
-

Search

-

Find what you need

+

Search

Find what you need

-
-
- - -
- -
+
- - {%- if search.performed -%} -
- {%- for item in search.results -%} - - {%- else -%} -

No results for “{{ search.terms | escape }}”.

- {%- endfor -%} -
- {%- endif -%} + {%- if search.performed -%}
{%- for item in search.results -%}{%- else -%}

No results for “{{ search.terms | escape }}”.

{%- endfor -%}
{%- endif -%}
-{% schema %} -{ - "name": "Main search", - "settings": [] +{% stylesheet %} +.search-field { display: grid; gap: 0.45rem; margin-block: 1rem; } +.search-field label { font-size: 0.85rem; font-weight: 700; } +.search-field input { width: 100%; min-height: 3rem; border: 1px solid var(--color-border); border-radius: var(--radius-sm); background: var(--color-surface); padding: 0.7rem 0.85rem; } +.predictive-search__results { border: 1px solid var(--color-border); border-radius: var(--radius-md); margin-top: 0.5rem; background: var(--color-surface); box-shadow: var(--shadow-soft); padding: 1rem; } +.predictive-search__results ul { display: grid; gap: 0.65rem; margin: 0; padding: 0; list-style: none; } +.predictive-search__results a { display: flex; justify-content: space-between; gap: 1rem; text-decoration: none; } +{% endstylesheet %} + +{% javascript %} +class PredictiveSearch extends HTMLElement { + connectedCallback() { + this.input = this.querySelector("input[type='search']"); this.results = this.querySelector("[data-predictive-search-results]"); + if (!this.input || !this.results) return; + this.input.addEventListener("input", () => { clearTimeout(this.timer); this.timer = setTimeout(() => this.search(), 180); }); + } + async search() { + const query = this.input.value.trim(); this.controller?.abort(); + if (query.length < 2) { this.results.replaceChildren(); this.input.setAttribute("aria-expanded", "false"); return; } + this.controller = new AbortController(); + try { + const response = await fetch(`/search/suggest?q=${encodeURIComponent(query)}&resources[type]=product§ion_id=predictive-search`, { signal: this.controller.signal }); + if (!response.ok) throw new Error(`Search request failed: ${response.status}`); + const documentFragment = new DOMParser().parseFromString(await response.text(), "text/html"); + const content = documentFragment.querySelector(".predictive-search__results"); + this.results.replaceChildren(...(content ? [content] : [])); this.input.setAttribute("aria-expanded", content ? "true" : "false"); + } catch (error) { if (error.name !== "AbortError") console.error(error); } + } } -{% endschema %} +if (!customElements.get("predictive-search")) customElements.define("predictive-search", PredictiveSearch); +{% endjavascript %} + +{% schema %}{"name":"Main search","settings":[]}{% endschema %} diff --git a/src/theme/templates/product.json b/src/theme/templates/product.json index 7701460..2799668 100644 --- a/src/theme/templates/product.json +++ b/src/theme/templates/product.json @@ -2,6 +2,14 @@ "sections": { "main": { "type": "main-product", + "blocks": { + "vendor": { "type": "product-vendor", "settings": {} }, + "title": { "type": "product-title", "settings": {} }, + "price": { "type": "product-price", "settings": {} }, + "purchase": { "type": "product-purchase", "settings": {} }, + "description": { "type": "product-description", "settings": {} } + }, + "block_order": ["vendor", "title", "price", "purchase", "description"], "settings": {} } }, diff --git a/tests/template-sync.test.js b/tests/template-sync.test.js index 5c69f10..4154824 100644 --- a/tests/template-sync.test.js +++ b/tests/template-sync.test.js @@ -10,16 +10,11 @@ const embeddedSource = path.join(repositoryRoot, "packages", "create-liquid-loom async function listFiles(directory, baseDirectory = directory) { const entries = await readdir(directory, { withFileTypes: true }); const files = []; - for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) { const entryPath = path.join(directory, entry.name); - if (entry.isDirectory()) { - files.push(...(await listFiles(entryPath, baseDirectory))); - } else if (entry.isFile()) { - files.push(path.relative(baseDirectory, entryPath).split(path.sep).join("/")); - } + if (entry.isDirectory()) files.push(...(await listFiles(entryPath, baseDirectory))); + else if (entry.isFile()) files.push(path.relative(baseDirectory, entryPath).split(path.sep).join("/")); } - return files; } @@ -28,23 +23,17 @@ describe("embedded starter", () => { const referenceFiles = await listFiles(referenceSource); const embeddedFiles = await listFiles(embeddedSource); assert.deepEqual(embeddedFiles, referenceFiles); - for (const file of referenceFiles) { - const [reference, embedded] = await Promise.all([ - readFile(path.join(referenceSource, ...file.split("/"))), - readFile(path.join(embeddedSource, ...file.split("/"))) - ]); + const [reference, embedded] = await Promise.all([readFile(path.join(referenceSource, ...file.split("/"))), readFile(path.join(embeddedSource, ...file.split("/")))]); assert.deepEqual(embedded, reference, `Embedded starter drifted at ${file}`); } }); - it("shares the framework config and Vite config used by the reference storefront", async () => { - for (const file of ["liquid-loom.config.ts", "vite.config.js"]) { - const [reference, embedded] = await Promise.all([ - readFile(path.join(repositoryRoot, file)), - readFile(path.join(repositoryRoot, "packages", "create-liquid-loom", "template", file)) - ]); - assert.deepEqual(embedded, reference, `Embedded starter drifted at ${file}`); - } + it("shares the framework config used by the reference storefront", async () => { + const [reference, embedded] = await Promise.all([ + readFile(path.join(repositoryRoot, "liquid-loom.config.ts")), + readFile(path.join(repositoryRoot, "packages", "create-liquid-loom", "template", "liquid-loom.config.ts")) + ]); + assert.deepEqual(embedded, reference, "Embedded starter drifted at liquid-loom.config.ts"); }); }); diff --git a/tests/theme-capabilities.test.js b/tests/theme-capabilities.test.js index 3378005..ec9fb65 100644 --- a/tests/theme-capabilities.test.js +++ b/tests/theme-capabilities.test.js @@ -8,19 +8,31 @@ const read = (relativePath) => readFile(path.join(root, ...relativePath.split("/ describe("reference theme capabilities", () => { it("ships composable Shopify theme blocks and keeps block validation enabled", async () => { - for (const block of ["button", "heading", "image", "text"]) { + for (const block of ["button", "heading", "image", "text", "product-vendor", "product-title", "product-price", "product-purchase", "product-description"]) { assert.match(await read(`src/theme/blocks/${block}.liquid`), /block\.shopify_attributes/); } assert.match(await read("src/theme/sections/pages/flexible-content.liquid"), /content_for 'blocks'/); + assert.match(await read("src/theme/sections/products/main-product.liquid"), /content_for 'blocks'/); + assert.match(await read("src/theme/templates/product.json"), /product-purchase/); assert.doesNotMatch(await read(".theme-check.yml"), /JSONMissingBlock:\s*\n\s*enabled: false/); }); - it("includes modern discovery and merchandising primitives", async () => { + it("uses Shopify-native component assets and standard storefront interoperability", async () => { + assert.match(await read("liquid-loom.config.ts"), /viteConfig:\s*false/); + assert.match(await read("src/theme/sections/global/header.liquid"), /{% javascript %}/); + assert.match(await read("src/theme/sections/global/header.liquid"), / { + assert.match(await read("src/theme/config/settings_schema.json"), /color_palette/); assert.match(await read("src/theme/sections/collections/main-collection.liquid"), /collection\.filters/); assert.match(await read("src/theme/sections/search/predictive-search.liquid"), /predictive_search\.resources/); - assert.match(await read("src/entrypoints/theme.js"), /class PredictiveSearch/); - const product = await read("src/theme/sections/products/main-product.liquid"); - assert.match(product, /selling_plan_groups/); - assert.match(product, /data-product-variants/); + assert.match(await read("src/theme/sections/search/main-search.liquid"), /class PredictiveSearch/); + const purchase = await read("src/theme/blocks/product-purchase.liquid"); + assert.match(purchase, /selling_plan_groups/); + assert.match(purchase, /data-product-variants/); }); }); From 1f73271eef804454dbb7916cbc957237c624cd21 Mon Sep 17 00:00:00 2001 From: Yotam Faraggi Date: Tue, 15 Sep 2026 02:15:46 +0200 Subject: [PATCH 04/16] Keep embedded starter synchronized with reference source --- .../template/src/public/theme.css | 555 +++++++++++++++--- .../src/theme/blocks/product-purchase.liquid | 90 ++- .../src/theme/config/settings_data.json | 10 +- .../src/theme/config/settings_schema.json | 29 +- .../template/src/theme/layout/theme.liquid | 18 +- .../collections/main-collection.liquid | 8 +- .../src/theme/sections/global/header.liquid | 75 ++- .../sections/home/featured-collection.liquid | 3 +- .../sections/products/main-product.liquid | 41 +- .../theme/sections/search/main-search.liquid | 48 +- .../template/src/theme/templates/product.json | 18 +- 11 files changed, 801 insertions(+), 94 deletions(-) diff --git a/packages/create-liquid-loom/template/src/public/theme.css b/packages/create-liquid-loom/template/src/public/theme.css index 210c1cf..64901c6 100644 --- a/packages/create-liquid-loom/template/src/public/theme.css +++ b/packages/create-liquid-loom/template/src/public/theme.css @@ -15,72 +15,489 @@ *, *::before, -*::after { box-sizing: border-box; } -html { scroll-behavior: smooth; } -body { margin: 0; background: var(--color-background); color: var(--color-foreground); font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; font-size: 1rem; line-height: 1.6; text-rendering: optimizeLegibility; } -body, button, input, select, textarea { font: inherit; } -img, svg, video { display: block; max-width: 100%; } -a { color: inherit; text-decoration-thickness: 0.08em; text-underline-offset: 0.2em; } -button, input, select, textarea { color: inherit; } -:focus-visible { outline: 3px solid color-mix(in srgb, var(--color-accent) 70%, white); outline-offset: 3px; } -.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; border: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; } -.skip-link { position: fixed; top: 1rem; left: 1rem; z-index: 100; transform: translateY(-200%); border-radius: var(--radius-sm); background: var(--color-foreground); color: white; padding: 0.75rem 1rem; } -.skip-link:focus { transform: translateY(0); } -.page-width { width: min(calc(100% - 2rem), var(--page-width)); margin-inline: auto; } -.section { padding-block: clamp(3rem, 7vw, 7rem); } -.section--compact { padding-block: clamp(2rem, 4vw, 4rem); } -.eyebrow { margin: 0 0 1rem; color: var(--color-accent); font-size: 0.76rem; font-weight: 750; letter-spacing: 0.14em; text-transform: uppercase; } -.heading { margin: 0; font-size: clamp(2.2rem, 5vw, 5rem); font-weight: 760; letter-spacing: -0.055em; line-height: 0.98; } -.heading--medium { font-size: clamp(1.8rem, 3.5vw, 3.5rem); } -.muted { color: var(--color-muted); } -.button { display: inline-flex; min-height: 3rem; align-items: center; justify-content: center; gap: 0.5rem; border: 1px solid transparent; border-radius: 999px; background: var(--color-accent); color: var(--color-accent-contrast); cursor: pointer; font-weight: 700; padding: 0.75rem 1.35rem; text-decoration: none; transition: transform 160ms ease, box-shadow 160ms ease; } -.button:hover { transform: translateY(-2px); box-shadow: 0 12px 30px color-mix(in srgb, var(--color-accent) 25%, transparent); } -.button--secondary { border-color: var(--color-border); background: transparent; color: var(--color-foreground); } -.button--full { width: 100%; } -.announcement { background: var(--color-foreground); color: var(--color-background); font-size: 0.84rem; letter-spacing: 0.03em; padding: 0.55rem 1rem; text-align: center; } -.hero { position: relative; overflow: hidden; } -.hero::before { position: absolute; inset: 5% -15% auto auto; width: 55vw; height: 55vw; border-radius: 50%; background: radial-gradient(circle, color-mix(in srgb, var(--color-accent) 18%, transparent), transparent 70%); content: ""; pointer-events: none; } -.hero__grid { position: relative; display: grid; min-height: min(760px, 78vh); grid-template-columns: minmax(0, 0.95fr) minmax(300px, 1.05fr); align-items: center; gap: clamp(2rem, 7vw, 7rem); } -.hero__copy { max-width: 42rem; } -.hero__text { max-width: 34rem; margin-block: 1.5rem 2rem; color: var(--color-muted); font-size: clamp(1rem, 1.5vw, 1.2rem); } -.hero__actions { display: flex; flex-wrap: wrap; gap: 0.75rem; } -.hero__media { position: relative; overflow: hidden; aspect-ratio: 4 / 5; border-radius: var(--radius-lg); background: linear-gradient(145deg, color-mix(in srgb, var(--color-accent) 85%, black), var(--color-foreground)); box-shadow: var(--shadow-soft); } -.hero__media img { width: 100%; height: 100%; object-fit: cover; } -.hero__placeholder { display: grid; height: 100%; place-items: center; color: color-mix(in srgb, white 70%, transparent); } -.hero__placeholder svg { width: 52%; opacity: 0.5; } -.section-header { display: flex; align-items: end; justify-content: space-between; gap: 2rem; margin-bottom: 2rem; } -.product-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: clamp(1rem, 2.5vw, 2rem); } -.product-card { min-width: 0; } -.product-card__media { display: block; overflow: hidden; aspect-ratio: 4 / 5; border-radius: var(--radius-md); background: color-mix(in srgb, var(--color-border) 55%, white); } -.product-card__media img { width: 100%; height: 100%; object-fit: cover; transition: transform 300ms ease; } -.product-card:hover img { transform: scale(1.025); } -.product-card__content { display: flex; align-items: start; justify-content: space-between; gap: 1rem; padding-top: 0.9rem; } -.product-card__title { margin: 0; font-size: 1rem; font-weight: 700; } -.product-card__title a { text-decoration: none; } -.price { white-space: nowrap; font-variant-numeric: tabular-nums; } -.price__compare { margin-right: 0.35rem; color: var(--color-muted); text-decoration: line-through; } -.collection-header { max-width: 48rem; margin-bottom: 3rem; } -.facets { display: flex; flex-wrap: wrap; align-items: start; gap: 0.75rem; margin-bottom: 2rem; } -.facet { position: relative; border: 1px solid var(--color-border); border-radius: var(--radius-sm); background: var(--color-surface); padding: 0.65rem 0.8rem; } -.facet__options { display: grid; min-width: 14rem; gap: 0.55rem; padding-top: 0.75rem; } -.cart-table { width: 100%; border-collapse: collapse; } -.cart-table th, .cart-table td { border-bottom: 1px solid var(--color-border); padding: 1rem; text-align: left; } -.cart-product { display: flex; align-items: center; gap: 1rem; } -.cart-product img { width: 5rem; border-radius: var(--radius-sm); } -.cart-footer { display: grid; justify-content: end; gap: 1rem; padding-top: 2rem; text-align: right; } -.rte { max-width: 48rem; } -.rte > * + * { margin-top: 1.25em; } -.content-stack { display: grid; max-width: 52rem; gap: 1.5rem; } -.content-image { margin: 0; } -.content-image img, .content-image svg { width: 100%; border-radius: var(--radius-lg); } -.empty-state { max-width: 38rem; margin-inline: auto; padding-block: 8rem; text-align: center; } -.site-footer { margin-top: clamp(3rem, 7vw, 7rem); border-top: 1px solid var(--color-border); background: var(--color-foreground); color: var(--color-background); } -.site-footer__grid { display: grid; grid-template-columns: 1.2fr repeat(2, 0.8fr); gap: 3rem; } -.site-footer__title { margin: 0 0 1rem; font-size: 0.8rem; letter-spacing: 0.12em; text-transform: uppercase; } -.site-footer ul { margin: 0; padding: 0; list-style: none; } -.site-footer li + li { margin-top: 0.65rem; } -.site-footer a { color: color-mix(in srgb, var(--color-background) 78%, transparent); text-decoration: none; } -.site-footer__bottom { display: flex; justify-content: space-between; gap: 1rem; border-top: 1px solid color-mix(in srgb, white 14%, transparent); margin-top: 3rem; padding-top: 1.5rem; color: color-mix(in srgb, var(--color-background) 62%, transparent); font-size: 0.82rem; } -@media (max-width: 900px) { .hero__grid { grid-template-columns: 1fr; padding-block: 4rem; } .hero__media { max-height: 65vh; } .product-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } .site-footer__grid { grid-template-columns: 1fr 1fr; } .site-footer__about { grid-column: 1 / -1; } } -@media (max-width: 600px) { .page-width { width: min(calc(100% - 1.25rem), var(--page-width)); } .product-grid, .site-footer__grid { grid-template-columns: 1fr; } .section-header, .site-footer__bottom { align-items: start; flex-direction: column; } .cart-table thead { display: none; } .cart-table tr, .cart-table td { display: block; } } -@media (prefers-reduced-motion: reduce) { *, *::before, *::after { scroll-behavior: auto !important; transition-duration: 0.01ms !important; } } +*::after { + box-sizing: border-box; +} + +html { + scroll-behavior: smooth; +} + +body { + margin: 0; + background: var(--color-background); + color: var(--color-foreground); + font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + font-size: 1rem; + line-height: 1.6; + text-rendering: optimizeLegibility; +} + +body, +button, +input, +select, +textarea { + font: inherit; +} + +img, +svg, +video { + display: block; + max-width: 100%; +} + +a { + color: inherit; + text-decoration-thickness: 0.08em; + text-underline-offset: 0.2em; +} + +button, +input, +select, +textarea { + color: inherit; +} + +:focus-visible { + outline: 3px solid color-mix(in srgb, var(--color-accent) 70%, white); + outline-offset: 3px; +} + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + border: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; +} + +.skip-link { + position: fixed; + top: 1rem; + left: 1rem; + z-index: 100; + transform: translateY(-200%); + border-radius: var(--radius-sm); + background: var(--color-foreground); + color: white; + padding: 0.75rem 1rem; +} + +.skip-link:focus { + transform: translateY(0); +} + +.page-width { + width: min(calc(100% - 2rem), var(--page-width)); + margin-inline: auto; +} + +.section { + padding-block: clamp(3rem, 7vw, 7rem); +} + +.section--compact { + padding-block: clamp(2rem, 4vw, 4rem); +} + +.eyebrow { + margin: 0 0 1rem; + color: var(--color-accent); + font-size: 0.76rem; + font-weight: 750; + letter-spacing: 0.14em; + text-transform: uppercase; +} + +.heading { + margin: 0; + font-size: clamp(2.2rem, 5vw, 5rem); + font-weight: 760; + letter-spacing: -0.055em; + line-height: 0.98; +} + +.heading--medium { + font-size: clamp(1.8rem, 3.5vw, 3.5rem); +} + +.muted { + color: var(--color-muted); +} + +.button { + display: inline-flex; + min-height: 3rem; + align-items: center; + justify-content: center; + gap: 0.5rem; + border: 1px solid transparent; + border-radius: 999px; + background: var(--color-accent); + color: var(--color-accent-contrast); + cursor: pointer; + font-weight: 700; + padding: 0.75rem 1.35rem; + text-decoration: none; + transition: transform 160ms ease, box-shadow 160ms ease; +} + +.button:hover { + transform: translateY(-2px); + box-shadow: 0 12px 30px color-mix(in srgb, var(--color-accent) 25%, transparent); +} + +.button--secondary { + border-color: var(--color-border); + background: transparent; + color: var(--color-foreground); +} + +.button--full { + width: 100%; +} + +.announcement { + background: var(--color-foreground); + color: var(--color-background); + font-size: 0.84rem; + letter-spacing: 0.03em; + padding: 0.55rem 1rem; + text-align: center; +} + +.hero { + position: relative; + overflow: hidden; +} + +.hero::before { + position: absolute; + inset: 5% -15% auto auto; + width: 55vw; + height: 55vw; + border-radius: 50%; + background: radial-gradient(circle, color-mix(in srgb, var(--color-accent) 18%, transparent), transparent 70%); + content: ""; + pointer-events: none; +} + +.hero__grid { + position: relative; + display: grid; + min-height: min(760px, 78vh); + grid-template-columns: minmax(0, 0.95fr) minmax(300px, 1.05fr); + align-items: center; + gap: clamp(2rem, 7vw, 7rem); +} + +.hero__copy { + max-width: 42rem; +} + +.hero__text { + max-width: 34rem; + margin-block: 1.5rem 2rem; + color: var(--color-muted); + font-size: clamp(1rem, 1.5vw, 1.2rem); +} + +.hero__actions { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; +} + +.hero__media { + position: relative; + overflow: hidden; + aspect-ratio: 4 / 5; + border-radius: var(--radius-lg); + background: linear-gradient(145deg, color-mix(in srgb, var(--color-accent) 85%, black), var(--color-foreground)); + box-shadow: var(--shadow-soft); +} + +.hero__media img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.hero__placeholder { + display: grid; + height: 100%; + place-items: center; + color: color-mix(in srgb, white 70%, transparent); +} + +.hero__placeholder svg { + width: 52%; + opacity: 0.5; +} + +.section-header { + display: flex; + align-items: end; + justify-content: space-between; + gap: 2rem; + margin-bottom: 2rem; +} + +.product-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: clamp(1rem, 2.5vw, 2rem); +} + +.product-card { + min-width: 0; +} + +.product-card__media { + display: block; + overflow: hidden; + aspect-ratio: 4 / 5; + border-radius: var(--radius-md); + background: color-mix(in srgb, var(--color-border) 55%, white); +} + +.product-card__media img { + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 300ms ease; +} + +.product-card:hover img { + transform: scale(1.025); +} + +.product-card__content { + display: flex; + align-items: start; + justify-content: space-between; + gap: 1rem; + padding-top: 0.9rem; +} + +.product-card__title { + margin: 0; + font-size: 1rem; + font-weight: 700; +} + +.product-card__title a { + text-decoration: none; +} + +.price { + white-space: nowrap; + font-variant-numeric: tabular-nums; +} + +.price__compare { + margin-right: 0.35rem; + color: var(--color-muted); + text-decoration: line-through; +} + +.collection-header { + max-width: 48rem; + margin-bottom: 3rem; +} + +.facets { + display: flex; + flex-wrap: wrap; + align-items: start; + gap: 0.75rem; + margin-bottom: 2rem; +} + +.facet { + position: relative; + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + background: var(--color-surface); + padding: 0.65rem 0.8rem; +} + +.facet__options { + display: grid; + min-width: 14rem; + gap: 0.55rem; + padding-top: 0.75rem; +} + +.cart-table { + width: 100%; + border-collapse: collapse; +} + +.cart-table th, +.cart-table td { + border-bottom: 1px solid var(--color-border); + padding: 1rem; + text-align: left; +} + +.cart-product { + display: flex; + align-items: center; + gap: 1rem; +} + +.cart-product img { + width: 5rem; + border-radius: var(--radius-sm); +} + +.cart-footer { + display: grid; + justify-content: end; + gap: 1rem; + padding-top: 2rem; + text-align: right; +} + +.rte { + max-width: 48rem; +} + +.rte > * + * { + margin-top: 1.25em; +} + +.content-stack { + display: grid; + max-width: 52rem; + gap: 1.5rem; +} + +.content-image { + margin: 0; +} + +.content-image img, +.content-image svg { + width: 100%; + border-radius: var(--radius-lg); +} + +.empty-state { + max-width: 38rem; + margin-inline: auto; + padding-block: 8rem; + text-align: center; +} + +.site-footer { + margin-top: clamp(3rem, 7vw, 7rem); + border-top: 1px solid var(--color-border); + background: var(--color-foreground); + color: var(--color-background); +} + +.site-footer__grid { + display: grid; + grid-template-columns: 1.2fr repeat(2, 0.8fr); + gap: 3rem; +} + +.site-footer__title { + margin: 0 0 1rem; + font-size: 0.8rem; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.site-footer ul { + margin: 0; + padding: 0; + list-style: none; +} + +.site-footer li + li { + margin-top: 0.65rem; +} + +.site-footer a { + color: color-mix(in srgb, var(--color-background) 78%, transparent); + text-decoration: none; +} + +.site-footer__bottom { + display: flex; + justify-content: space-between; + gap: 1rem; + border-top: 1px solid color-mix(in srgb, white 14%, transparent); + margin-top: 3rem; + padding-top: 1.5rem; + color: color-mix(in srgb, var(--color-background) 62%, transparent); + font-size: 0.82rem; +} + +@media (max-width: 900px) { + .hero__grid { + grid-template-columns: 1fr; + padding-block: 4rem; + } + + .hero__media { + max-height: 65vh; + } + + .product-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .site-footer__grid { + grid-template-columns: 1fr 1fr; + } + + .site-footer__about { + grid-column: 1 / -1; + } +} + +@media (max-width: 600px) { + .page-width { + width: min(calc(100% - 1.25rem), var(--page-width)); + } + + .product-grid, + .site-footer__grid { + grid-template-columns: 1fr; + } + + .section-header, + .site-footer__bottom { + align-items: start; + flex-direction: column; + } + + .cart-table thead { + display: none; + } + + .cart-table tr, + .cart-table td { + display: block; + } +} + +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + scroll-behavior: auto !important; + transition-duration: 0.01ms !important; + } +} diff --git a/packages/create-liquid-loom/template/src/theme/blocks/product-purchase.liquid b/packages/create-liquid-loom/template/src/theme/blocks/product-purchase.liquid index 54a1ee4..74f2b77 100644 --- a/packages/create-liquid-loom/template/src/theme/blocks/product-purchase.liquid +++ b/packages/create-liquid-loom/template/src/theme/blocks/product-purchase.liquid @@ -1,4 +1,88 @@ -{%- form 'product', product, id: 'ProductForm-' | append: block.id, novalidate: 'novalidate', data-product-form: '' -%}{%- unless product.has_only_default_variant -%}
{%- else -%}{%- endunless -%}{%- if product.selling_plan_groups.size > 0 -%}
{%- endif -%}

{%- endform -%}
-{% stylesheet %}.field{display:grid;gap:.45rem;margin-block:1rem}.field label{font-size:.85rem;font-weight:700}.field input,.field select,.field textarea{width:100%;min-height:3rem;border:1px solid var(--color-border);border-radius:var(--radius-sm);background:var(--color-surface);padding:.7rem .85rem}.quantity{display:grid;width:9rem;grid-template-columns:2.6rem 1fr 2.6rem;border:1px solid var(--color-border);border-radius:999px;background:var(--color-surface)}.quantity button,.quantity input{min-height:2.8rem;border:0;background:transparent;text-align:center}.quantity input{width:100%;appearance:textfield}.product-form__status{min-height:1.5em;margin-block:.75rem 0}{% endstylesheet %} -{% javascript %}class QuantityInput extends HTMLElement{connectedCallback(){this.input=this.querySelector("input[type='number']");this.querySelectorAll("button").forEach(button=>button.addEventListener("click",()=>this.change(button.dataset.quantityAction)))}change(action){if(!this.input)return;const step=Number(this.input.step)||1;const minimum=Number(this.input.min)||1;const current=Number(this.input.value)||minimum;this.input.value=String(Math.max(minimum,current+(action==="increase"?step:-step)));this.input.dispatchEvent(new Event("change",{bubbles:true}))}}class ProductForm extends HTMLElement{connectedCallback(){this.form=this.querySelector("form");this.select=this.querySelector("[data-variant-select]");this.submit=this.querySelector("button[type='submit']");this.status=this.querySelector("[data-product-form-status]");const data=this.querySelector("[data-product-variants]");if(data)this.variants=JSON.parse(data.textContent);this.select?.addEventListener("change",()=>this.variantChanged());this.form?.addEventListener("submit",event=>this.submitWithStandardAction(event))}variantChanged(){const variant=this.variants?.find(({id})=>String(id)===this.select.value);if(!variant)return;this.submit?.toggleAttribute("disabled",!variant.available);const url=new URL(window.location.href);url.searchParams.set("variant",variant.id);window.history.replaceState({},"",url)}async submitWithStandardAction(event){if(!window.Shopify?.actions?.updateCart||!this.form)return;event.preventDefault();const formData=new FormData(this.form);const merchandiseId=formData.get("id");if(!merchandiseId)return;const line={merchandiseId:String(merchandiseId),quantity:Math.max(1,Number(formData.get("quantity"))||1)};const sellingPlanId=formData.get("selling_plan");if(sellingPlanId)line.sellingPlanId=String(sellingPlanId);this.submit?.setAttribute("disabled","");if(this.status)this.status.textContent="";try{const{cart,userErrors,warnings}=await window.Shopify.actions.updateCart({lines:[line]},{event:{context:"product"}});if(userErrors?.length){if(this.status)this.status.textContent=userErrors[0].message;return}if(this.status)this.status.textContent=warnings?.[0]?.message||"Added to cart.";if(cart)document.querySelectorAll("[data-cart-count]").forEach(node=>{node.textContent=String(cart.totalQuantity)})}catch(error){console.error(error);if(this.status)this.status.textContent="Unable to update the cart. Try again."}finally{const variant=this.variants?.find(({id})=>String(id)===String(formData.get("id")));this.submit?.toggleAttribute("disabled",variant?!variant.available:false)}}}if(!customElements.get("quantity-input"))customElements.define("quantity-input",QuantityInput);if(!customElements.get("product-form"))customElements.define("product-form",ProductForm);{% endjavascript %} + + {%- form 'product', product, id: 'ProductForm-' | append: block.id, novalidate: 'novalidate', data-product-form: '' -%} + {%- unless product.has_only_default_variant -%} +
+ {%- else -%}{%- endunless -%} + {%- if product.selling_plan_groups.size > 0 -%}
{%- endif -%} +
+ +

+ {%- endform -%} + +
+ +{% stylesheet %} +.field { display: grid; gap: 0.45rem; margin-block: 1rem; } +.field label { font-size: 0.85rem; font-weight: 700; } +.field input, .field select, .field textarea { width: 100%; min-height: 3rem; border: 1px solid var(--color-border); border-radius: var(--radius-sm); background: var(--color-surface); padding: 0.7rem 0.85rem; } +.quantity { display: grid; width: 9rem; grid-template-columns: 2.6rem 1fr 2.6rem; border: 1px solid var(--color-border); border-radius: 999px; background: var(--color-surface); } +.quantity button, .quantity input { min-height: 2.8rem; border: 0; background: transparent; text-align: center; } +.quantity input { width: 100%; appearance: textfield; } +.product-form__status { min-height: 1.5em; margin-block: 0.75rem 0; } +{% endstylesheet %} + +{% javascript %} +class QuantityInput extends HTMLElement { + connectedCallback() { + this.input = this.querySelector("input[type='number']"); + this.querySelectorAll("button").forEach((button) => button.addEventListener("click", () => this.change(button.dataset.quantityAction))); + } + change(action) { + if (!this.input) return; + const step = Number(this.input.step) || 1; + const minimum = Number(this.input.min) || 1; + const current = Number(this.input.value) || minimum; + this.input.value = String(Math.max(minimum, current + (action === "increase" ? step : -step))); + this.input.dispatchEvent(new Event("change", { bubbles: true })); + } +} + +class ProductForm extends HTMLElement { + connectedCallback() { + this.form = this.querySelector("form"); + this.select = this.querySelector("[data-variant-select]"); + this.submit = this.querySelector("button[type='submit']"); + this.status = this.querySelector("[data-product-form-status]"); + const data = this.querySelector("[data-product-variants]"); + if (data) this.variants = JSON.parse(data.textContent); + this.select?.addEventListener("change", () => this.variantChanged()); + this.form?.addEventListener("submit", (event) => this.submitWithStandardAction(event)); + } + variantChanged() { + const variant = this.variants?.find(({ id }) => String(id) === this.select.value); + if (!variant) return; + this.submit?.toggleAttribute("disabled", !variant.available); + const url = new URL(window.location.href); + url.searchParams.set("variant", variant.id); + window.history.replaceState({}, "", url); + } + async submitWithStandardAction(event) { + if (!window.Shopify?.actions?.updateCart || !this.form) return; + event.preventDefault(); + const formData = new FormData(this.form); + const merchandiseId = formData.get("id"); + if (!merchandiseId) return; + const line = { merchandiseId: String(merchandiseId), quantity: Math.max(1, Number(formData.get("quantity")) || 1) }; + const sellingPlanId = formData.get("selling_plan"); + if (sellingPlanId) line.sellingPlanId = String(sellingPlanId); + this.submit?.setAttribute("disabled", ""); + if (this.status) this.status.textContent = ""; + try { + const { cart, userErrors, warnings } = await window.Shopify.actions.updateCart({ lines: [line] }, { event: { context: "product" } }); + if (userErrors?.length) { if (this.status) this.status.textContent = userErrors[0].message; return; } + if (this.status) this.status.textContent = warnings?.[0]?.message || "Added to cart."; + if (cart) document.querySelectorAll("[data-cart-count]").forEach((node) => { node.textContent = String(cart.totalQuantity); }); + } catch (error) { + console.error(error); + if (this.status) this.status.textContent = "Unable to update the cart. Try again."; + } finally { + const variant = this.variants?.find(({ id }) => String(id) === String(formData.get("id"))); + this.submit?.toggleAttribute("disabled", variant ? !variant.available : false); + } + } +} +if (!customElements.get("quantity-input")) customElements.define("quantity-input", QuantityInput); +if (!customElements.get("product-form")) customElements.define("product-form", ProductForm); +{% endjavascript %} + {% schema %}{"name":"Product purchase","presets":[{"name":"Product purchase"}]}{% endschema %} diff --git a/packages/create-liquid-loom/template/src/theme/config/settings_data.json b/packages/create-liquid-loom/template/src/theme/config/settings_data.json index 9c3de6e..03b5e60 100644 --- a/packages/create-liquid-loom/template/src/theme/config/settings_data.json +++ b/packages/create-liquid-loom/template/src/theme/config/settings_data.json @@ -1,4 +1,10 @@ { - "current": { "page_width": 1200 }, - "presets": { "Default": { "page_width": 1200 } } + "current": { + "page_width": 1200 + }, + "presets": { + "Default": { + "page_width": 1200 + } + } } diff --git a/packages/create-liquid-loom/template/src/theme/config/settings_schema.json b/packages/create-liquid-loom/template/src/theme/config/settings_schema.json index 01b71b9..285db0c 100644 --- a/packages/create-liquid-loom/template/src/theme/config/settings_schema.json +++ b/packages/create-liquid-loom/template/src/theme/config/settings_schema.json @@ -1,10 +1,35 @@ [ { "name": "Layout", - "settings": [{ "type": "range", "id": "page_width", "min": 960, "max": 1600, "step": 40, "unit": "px", "label": "Page width", "default": 1200 }] + "settings": [ + { + "type": "range", + "id": "page_width", + "min": 960, + "max": 1600, + "step": 40, + "unit": "px", + "label": "Page width", + "default": 1200 + } + ] }, { "name": "Colors", - "settings": [{ "type": "color_palette", "id": "colors", "default": { "background": "#F8F7F2", "foreground": "#111820", "surface": "#FFFFFF", "muted": "#66717C", "border": "#DCE1DD", "accent": "#176C5B", "accent_contrast": "#FFFFFF" } }] + "settings": [ + { + "type": "color_palette", + "id": "colors", + "default": { + "background": "#F8F7F2", + "foreground": "#111820", + "surface": "#FFFFFF", + "muted": "#66717C", + "border": "#DCE1DD", + "accent": "#176C5B", + "accent_contrast": "#FFFFFF" + } + } + ] } ] diff --git a/packages/create-liquid-loom/template/src/theme/layout/theme.liquid b/packages/create-liquid-loom/template/src/theme/layout/theme.liquid index 3747091..640a028 100644 --- a/packages/create-liquid-loom/template/src/theme/layout/theme.liquid +++ b/packages/create-liquid-loom/template/src/theme/layout/theme.liquid @@ -6,11 +6,27 @@ + {% render 'meta-tags' %} + {{ 'theme.css' | asset_url | stylesheet_tag: preload: true }} - + + + {{ content_for_header }} + {% sections 'header-group' %} diff --git a/packages/create-liquid-loom/template/src/theme/sections/collections/main-collection.liquid b/packages/create-liquid-loom/template/src/theme/sections/collections/main-collection.liquid index 47995b7..c449ffa 100644 --- a/packages/create-liquid-loom/template/src/theme/sections/collections/main-collection.liquid +++ b/packages/create-liquid-loom/template/src/theme/sections/collections/main-collection.liquid @@ -1,2 +1,8 @@ -

Collection

{{ collection.title | escape }}

{%- if collection.description != blank -%}
{{ collection.description }}
{%- endif -%}
{%- if collection.filters.size > 0 -%}
{%- for filter in collection.filters -%}
{{ filter.label | escape }}{%- if filter.active_values.size > 0 %} ({{ filter.active_values.size }}){% endif %}
{%- case filter.type -%}{%- when 'boolean', 'list' -%}{%- for value in filter.values -%}{%- endfor -%}{%- when 'price_range' -%}{%- endcase -%}
{%- endfor -%}Clear
{%- endif -%}{%- paginate collection.products by section.settings.products_per_page -%}{%- if paginate.pages > 1 -%}
{{ paginate | default_pagination }}
{%- endif -%}{%- endpaginate -%}
+
+
+

Collection

{{ collection.title | escape }}

{%- if collection.description != blank -%}
{{ collection.description }}
{%- endif -%}
+ {%- if collection.filters.size > 0 -%}
{%- for filter in collection.filters -%}
{{ filter.label | escape }}{%- if filter.active_values.size > 0 %} ({{ filter.active_values.size }}){% endif %}
{%- case filter.type -%}{%- when 'boolean', 'list' -%}{%- for value in filter.values -%}{%- endfor -%}{%- when 'price_range' -%}{%- endcase -%}
{%- endfor -%}Clear
{%- endif -%} + {%- paginate collection.products by section.settings.products_per_page -%}{%- if paginate.pages > 1 -%}
{{ paginate | default_pagination }}
{%- endif -%}{%- endpaginate -%} +
+
{% schema %}{"name":"Main collection","settings":[{"type":"range","id":"products_per_page","min":8,"max":32,"step":4,"label":"Products per page","default":16}]}{% endschema %} diff --git a/packages/create-liquid-loom/template/src/theme/sections/global/header.liquid b/packages/create-liquid-loom/template/src/theme/sections/global/header.liquid index a311dc3..f0a2e93 100644 --- a/packages/create-liquid-loom/template/src/theme/sections/global/header.liquid +++ b/packages/create-liquid-loom/template/src/theme/sections/global/header.liquid @@ -1,14 +1,73 @@ -{% stylesheet %}.site-header{position:sticky;top:0;z-index:40;border-bottom:1px solid color-mix(in srgb,var(--color-border) 75%,transparent);background:color-mix(in srgb,var(--color-background) 88%,transparent);backdrop-filter:blur(18px)}.site-header__inner{display:grid;min-height:4.8rem;grid-template-columns:1fr auto 1fr;align-items:center;gap:1rem}.site-header__brand{justify-self:center;font-size:1.1rem;font-weight:800;letter-spacing:-.03em;text-decoration:none}.site-header__logo{width:auto;max-height:2.3rem}.site-header__nav,.site-header__actions{display:flex;align-items:center;gap:1.25rem}.site-header__nav a,.site-header__actions a{font-size:.9rem;font-weight:650;text-decoration:none}.site-header__actions{justify-self:end}.site-header__menu-button{display:none;border:0;background:transparent;cursor:pointer;padding:.5rem}.mobile-menu{border-top:1px solid var(--color-border);background:var(--color-background);padding:1rem}.mobile-menu[hidden]{display:none}.mobile-menu a{display:block;padding:.8rem;font-size:1.15rem;font-weight:700;text-decoration:none}shopify-account{--shopify-account-signed-in-avatar-size:2rem;--shopify-account-signed-in-avatar-color-background:var(--color-accent);--shopify-account-signed-in-avatar-color-text:var(--color-accent-contrast)}@media(max-width:900px){.site-header__nav{display:none}.site-header__menu-button{display:inline-flex}}@media(max-width:600px){.site-header__actions>a:first-child{display:none}}{% endstylesheet %} -{% javascript %}class MenuDisclosure extends HTMLElement{connectedCallback(){this.button=this.querySelector("button");this.panel=this.querySelector("[data-menu-panel]");if(!this.button||!this.panel)return;this.button.addEventListener("click",()=>this.toggle());this.addEventListener("keydown",event=>{if(event.key==="Escape"&&this.isOpen()){this.close();this.button.focus()}})}isOpen(){return this.button.getAttribute("aria-expanded")==="true"}toggle(){this.isOpen()?this.close():this.open()}open(){this.button.setAttribute("aria-expanded","true");this.panel.removeAttribute("hidden")}close(){this.button.setAttribute("aria-expanded","false");this.panel.setAttribute("hidden","")}}if(!customElements.get("menu-disclosure"))customElements.define("menu-disclosure",MenuDisclosure);{% endjavascript %} -{% schema %}{"name":"Header","settings":[{"type":"image_picker","id":"logo","label":"Logo"},{"type":"link_list","id":"menu","label":"Menu","default":"main-menu"},{"type":"link_list","id":"customer_account_menu","label":"Customer account menu","default":"customer-account-main-menu"}]}{% endschema %} + +{% stylesheet %} +.site-header { position: sticky; top: 0; z-index: 40; border-bottom: 1px solid color-mix(in srgb, var(--color-border) 75%, transparent); background: color-mix(in srgb, var(--color-background) 88%, transparent); backdrop-filter: blur(18px); } +.site-header__inner { display: grid; min-height: 4.8rem; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 1rem; } +.site-header__brand { justify-self: center; font-size: 1.1rem; font-weight: 800; letter-spacing: -0.03em; text-decoration: none; } +.site-header__logo { width: auto; max-height: 2.3rem; } +.site-header__nav, .site-header__actions { display: flex; align-items: center; gap: 1.25rem; } +.site-header__nav a, .site-header__actions a { font-size: 0.9rem; font-weight: 650; text-decoration: none; } +.site-header__actions { justify-self: end; } +.site-header__menu-button { display: none; border: 0; background: transparent; cursor: pointer; padding: 0.5rem; } +.mobile-menu { border-top: 1px solid var(--color-border); background: var(--color-background); padding: 1rem; } +.mobile-menu[hidden] { display: none; } +.mobile-menu a { display: block; padding: 0.8rem; font-size: 1.15rem; font-weight: 700; text-decoration: none; } +shopify-account { --shopify-account-signed-in-avatar-size: 2rem; --shopify-account-signed-in-avatar-color-background: var(--color-accent); --shopify-account-signed-in-avatar-color-text: var(--color-accent-contrast); } +@media (max-width: 900px) { .site-header__nav { display: none; } .site-header__menu-button { display: inline-flex; } } +@media (max-width: 600px) { .site-header__actions > a:first-child { display: none; } } +{% endstylesheet %} + +{% javascript %} +class MenuDisclosure extends HTMLElement { + connectedCallback() { + this.button = this.querySelector("button"); + this.panel = this.querySelector("[data-menu-panel]"); + if (!this.button || !this.panel) return; + this.button.addEventListener("click", () => this.toggle()); + this.addEventListener("keydown", (event) => { + if (event.key === "Escape" && this.isOpen()) { this.close(); this.button.focus(); } + }); + } + isOpen() { return this.button.getAttribute("aria-expanded") === "true"; } + toggle() { this.isOpen() ? this.close() : this.open(); } + open() { this.button.setAttribute("aria-expanded", "true"); this.panel.removeAttribute("hidden"); } + close() { this.button.setAttribute("aria-expanded", "false"); this.panel.setAttribute("hidden", ""); } +} +if (!customElements.get("menu-disclosure")) customElements.define("menu-disclosure", MenuDisclosure); +{% endjavascript %} + +{% schema %} +{ + "name": "Header", + "settings": [ + { "type": "image_picker", "id": "logo", "label": "Logo" }, + { "type": "link_list", "id": "menu", "label": "Menu", "default": "main-menu" }, + { "type": "link_list", "id": "customer_account_menu", "label": "Customer account menu", "default": "customer-account-main-menu" } + ] +} +{% endschema %} diff --git a/packages/create-liquid-loom/template/src/theme/sections/home/featured-collection.liquid b/packages/create-liquid-loom/template/src/theme/sections/home/featured-collection.liquid index ab16e44..6b8a2c8 100644 --- a/packages/create-liquid-loom/template/src/theme/sections/home/featured-collection.liquid +++ b/packages/create-liquid-loom/template/src/theme/sections/home/featured-collection.liquid @@ -1,2 +1,3 @@ -{%- assign featured_collection = section.settings.collection -%}

{{ section.settings.eyebrow | escape }}

{{ section.settings.heading | escape }}

{%- if featured_collection != blank -%}{{ 'actions.view_all' | t }}{%- endif -%}
{%- if featured_collection != blank -%}{%- for product in featured_collection.products limit: section.settings.products_to_show -%}{%- endfor -%}{%- else -%}{%- for index in (1..4) -%}
{{ 'product-' | append: index | placeholder_svg_tag }}

Example product

{{ 2400 | money }}
{%- endfor -%}{%- endif -%}
+{%- assign featured_collection = section.settings.collection -%} +

{{ section.settings.eyebrow | escape }}

{{ section.settings.heading | escape }}

{%- if featured_collection != blank -%}{{ 'actions.view_all' | t }}{%- endif -%}
{%- if featured_collection != blank -%}{%- for product in featured_collection.products limit: section.settings.products_to_show -%}{%- endfor -%}{%- else -%}{%- for index in (1..4) -%}
{{ 'product-' | append: index | placeholder_svg_tag }}

Example product

{{ 2400 | money }}
{%- endfor -%}{%- endif -%}
{% schema %}{"name":"Featured collection","settings":[{"type":"text","id":"eyebrow","label":"Eyebrow","default":"Selected for you"},{"type":"text","id":"heading","label":"Heading","default":"Products worth a closer look"},{"type":"collection","id":"collection","label":"Collection"},{"type":"range","id":"products_to_show","min":2,"max":8,"step":1,"label":"Products","default":4}],"presets":[{"name":"Featured collection"}]}{% endschema %} diff --git a/packages/create-liquid-loom/template/src/theme/sections/products/main-product.liquid b/packages/create-liquid-loom/template/src/theme/sections/products/main-product.liquid index 88d7044..b15bf60 100644 --- a/packages/create-liquid-loom/template/src/theme/sections/products/main-product.liquid +++ b/packages/create-liquid-loom/template/src/theme/sections/products/main-product.liquid @@ -1,3 +1,38 @@ -
{% content_for 'blocks' %}
-{% stylesheet %}.product{display:grid;grid-template-columns:minmax(0,1.1fr) minmax(320px,.9fr);gap:clamp(2rem,7vw,7rem)}.product__gallery{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:1rem}.product__gallery img{width:100%;border-radius:var(--radius-md)}.product__details{display:grid;align-self:start;gap:1rem}.product__details .price{display:block;font-size:1.2rem}@media(max-width:900px){.product{grid-template-columns:1fr}}@media(max-width:600px){.product__gallery{grid-template-columns:1fr}}{% endstylesheet %} -{% schema %}{"name":"Main product","blocks":[{"type":"@app"},{"type":"@theme"},{"type":"product-vendor"},{"type":"product-title"},{"type":"product-price"},{"type":"product-purchase"},{"type":"product-description"}],"settings":[]}{% endschema %} + +
+
+ +
{% content_for 'blocks' %}
+
+
+
+ +{% stylesheet %} +.product { display: grid; grid-template-columns: minmax(0, 1.1fr) minmax(320px, 0.9fr); gap: clamp(2rem, 7vw, 7rem); } +.product__gallery { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1rem; } +.product__gallery img { width: 100%; border-radius: var(--radius-md); } +.product__details { display: grid; align-self: start; gap: 1rem; } +.product__details .price { display: block; font-size: 1.2rem; } +@media (max-width: 900px) { .product { grid-template-columns: 1fr; } } +@media (max-width: 600px) { .product__gallery { grid-template-columns: 1fr; } } +{% endstylesheet %} + +{% schema %} +{ + "name": "Main product", + "blocks": [ + { "type": "@app" }, + { "type": "@theme" }, + { "type": "product-vendor" }, + { "type": "product-title" }, + { "type": "product-price" }, + { "type": "product-purchase" }, + { "type": "product-description" } + ], + "settings": [] +} +{% endschema %} diff --git a/packages/create-liquid-loom/template/src/theme/sections/search/main-search.liquid b/packages/create-liquid-loom/template/src/theme/sections/search/main-search.liquid index 80e62f0..261fd91 100644 --- a/packages/create-liquid-loom/template/src/theme/sections/search/main-search.liquid +++ b/packages/create-liquid-loom/template/src/theme/sections/search/main-search.liquid @@ -1,4 +1,46 @@ -

Search

Find what you need

{%- if search.performed -%}
{%- for item in search.results -%}{%- else -%}

No results for “{{ search.terms | escape }}”.

{%- endfor -%}
{%- endif -%}
-{% stylesheet %}.search-field{display:grid;gap:.45rem;margin-block:1rem}.search-field label{font-size:.85rem;font-weight:700}.search-field input{width:100%;min-height:3rem;border:1px solid var(--color-border);border-radius:var(--radius-sm);background:var(--color-surface);padding:.7rem .85rem}.predictive-search__results{border:1px solid var(--color-border);border-radius:var(--radius-md);margin-top:.5rem;background:var(--color-surface);box-shadow:var(--shadow-soft);padding:1rem}.predictive-search__results ul{display:grid;gap:.65rem;margin:0;padding:0;list-style:none}.predictive-search__results a{display:flex;justify-content:space-between;gap:1rem;text-decoration:none}{% endstylesheet %} -{% javascript %}class PredictiveSearch extends HTMLElement{connectedCallback(){this.input=this.querySelector("input[type='search']");this.results=this.querySelector("[data-predictive-search-results]");if(!this.input||!this.results)return;this.input.addEventListener("input",()=>{clearTimeout(this.timer);this.timer=setTimeout(()=>this.search(),180)})}async search(){const query=this.input.value.trim();this.controller?.abort();if(query.length<2){this.results.replaceChildren();this.input.setAttribute("aria-expanded","false");return}this.controller=new AbortController();try{const response=await fetch(`/search/suggest?q=${encodeURIComponent(query)}&resources[type]=product§ion_id=predictive-search`,{signal:this.controller.signal});if(!response.ok)throw new Error(`Search request failed: ${response.status}`);const documentFragment=new DOMParser().parseFromString(await response.text(),"text/html");const content=documentFragment.querySelector(".predictive-search__results");this.results.replaceChildren(...(content?[content]:[]));this.input.setAttribute("aria-expanded",content?"true":"false")}catch(error){if(error.name!=="AbortError")console.error(error)}}}if(!customElements.get("predictive-search"))customElements.define("predictive-search",PredictiveSearch);{% endjavascript %} +
+
+
+

Search

Find what you need

+ +
+
+
+
+ {%- if search.performed -%}
{%- for item in search.results -%}{%- else -%}

No results for “{{ search.terms | escape }}”.

{%- endfor -%}
{%- endif -%} +
+
+ +{% stylesheet %} +.search-field { display: grid; gap: 0.45rem; margin-block: 1rem; } +.search-field label { font-size: 0.85rem; font-weight: 700; } +.search-field input { width: 100%; min-height: 3rem; border: 1px solid var(--color-border); border-radius: var(--radius-sm); background: var(--color-surface); padding: 0.7rem 0.85rem; } +.predictive-search__results { border: 1px solid var(--color-border); border-radius: var(--radius-md); margin-top: 0.5rem; background: var(--color-surface); box-shadow: var(--shadow-soft); padding: 1rem; } +.predictive-search__results ul { display: grid; gap: 0.65rem; margin: 0; padding: 0; list-style: none; } +.predictive-search__results a { display: flex; justify-content: space-between; gap: 1rem; text-decoration: none; } +{% endstylesheet %} + +{% javascript %} +class PredictiveSearch extends HTMLElement { + connectedCallback() { + this.input = this.querySelector("input[type='search']"); this.results = this.querySelector("[data-predictive-search-results]"); + if (!this.input || !this.results) return; + this.input.addEventListener("input", () => { clearTimeout(this.timer); this.timer = setTimeout(() => this.search(), 180); }); + } + async search() { + const query = this.input.value.trim(); this.controller?.abort(); + if (query.length < 2) { this.results.replaceChildren(); this.input.setAttribute("aria-expanded", "false"); return; } + this.controller = new AbortController(); + try { + const response = await fetch(`/search/suggest?q=${encodeURIComponent(query)}&resources[type]=product§ion_id=predictive-search`, { signal: this.controller.signal }); + if (!response.ok) throw new Error(`Search request failed: ${response.status}`); + const documentFragment = new DOMParser().parseFromString(await response.text(), "text/html"); + const content = documentFragment.querySelector(".predictive-search__results"); + this.results.replaceChildren(...(content ? [content] : [])); this.input.setAttribute("aria-expanded", content ? "true" : "false"); + } catch (error) { if (error.name !== "AbortError") console.error(error); } + } +} +if (!customElements.get("predictive-search")) customElements.define("predictive-search", PredictiveSearch); +{% endjavascript %} + {% schema %}{"name":"Main search","settings":[]}{% endschema %} diff --git a/packages/create-liquid-loom/template/src/theme/templates/product.json b/packages/create-liquid-loom/template/src/theme/templates/product.json index bedd194..2799668 100644 --- a/packages/create-liquid-loom/template/src/theme/templates/product.json +++ b/packages/create-liquid-loom/template/src/theme/templates/product.json @@ -1 +1,17 @@ -{"sections":{"main":{"type":"main-product","blocks":{"vendor":{"type":"product-vendor","settings":{}},"title":{"type":"product-title","settings":{}},"price":{"type":"product-price","settings":{}},"purchase":{"type":"product-purchase","settings":{}},"description":{"type":"product-description","settings":{}}},"block_order":["vendor","title","price","purchase","description"],"settings":{}}},"order":["main"]} +{ + "sections": { + "main": { + "type": "main-product", + "blocks": { + "vendor": { "type": "product-vendor", "settings": {} }, + "title": { "type": "product-title", "settings": {} }, + "price": { "type": "product-price", "settings": {} }, + "purchase": { "type": "product-purchase", "settings": {} }, + "description": { "type": "product-description", "settings": {} } + }, + "block_order": ["vendor", "title", "price", "purchase", "description"], + "settings": {} + } + }, + "order": ["main"] +} From 1b9debcad9de2b63fec395d6177a6dfdcfc63b09 Mon Sep 17 00:00:00 2001 From: Yotam Faraggi Date: Tue, 15 Sep 2026 02:18:28 +0200 Subject: [PATCH 05/16] chore: format Shopify modernization branch --- .github/workflows/format-modernization.yml | 37 ++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 .github/workflows/format-modernization.yml diff --git a/.github/workflows/format-modernization.yml b/.github/workflows/format-modernization.yml new file mode 100644 index 0000000..c35297c --- /dev/null +++ b/.github/workflows/format-modernization.yml @@ -0,0 +1,37 @@ +name: Format modernization branch + +on: + push: + branches: [feat/modernize-shopify-2026] + +permissions: + contents: write + +jobs: + format: + if: github.actor != 'github-actions[bot]' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + with: + ref: feat/modernize-shopify-2026 + - uses: actions/setup-node@v7 + with: + node-version: 24 + cache: pnpm + - run: corepack enable + - run: pnpm install --frozen-lockfile + - run: pnpm format + - name: Commit formatted output and remove this one-shot workflow + shell: bash + run: | + set -euo pipefail + rm .github/workflows/format-modernization.yml + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add -A + if git diff --cached --quiet; then + exit 0 + fi + git commit -m "Format Shopify modernization changes" + git push origin HEAD:feat/modernize-shopify-2026 From a7b81ea5a82aa4005560aad526035d9ba6ecb216 Mon Sep 17 00:00:00 2001 From: Yotam Faraggi Date: Tue, 15 Sep 2026 02:20:25 +0200 Subject: [PATCH 06/16] chore: format modernization changes with repository tooling --- .github/workflows/ci.yml | 46 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6b86f86..34e83f6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,13 +6,57 @@ on: pull_request: permissions: - contents: read + contents: write concurrency: group: ci-${{ github.ref }} cancel-in-progress: true jobs: + format-modernization: + if: github.event_name == 'pull_request' && github.head_ref == 'feat/modernize-shopify-2026' && github.actor != 'github-actions[bot]' + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Check out modernization branch + uses: actions/checkout@v7 + with: + ref: ${{ github.head_ref }} + fetch-depth: 0 + + - name: Set up Node.js + uses: actions/setup-node@v7 + with: + node-version: 24 + + - name: Enable Corepack + run: corepack enable + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Format repository + run: pnpm format + + - name: Commit formatting and restore normal CI + shell: bash + env: + HEAD_BRANCH: ${{ github.head_ref }} + run: | + set -euo pipefail + git fetch origin main + git show origin/main:.github/workflows/ci.yml > .github/workflows/ci.yml + rm -f .github/workflows/format-modernization.yml + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add -A + if git diff --cached --quiet; then + exit 0 + fi + git commit -m "Format Shopify modernization changes" + git push origin HEAD:"${HEAD_BRANCH}" + validate: runs-on: ubuntu-latest timeout-minutes: 15 From 7d05bcc9d0249264f940cd15a84b164f99f7d136 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 15 Sep 2026 00:20:42 +0000 Subject: [PATCH 07/16] Format Shopify modernization changes --- .github/workflows/ci.yml | 46 +--- .github/workflows/format-modernization.yml | 37 --- docs/RECIPES.md | 6 +- docs/SHOPIFY_2026_MODERNIZATION.md | 22 +- .../create-liquid-loom/template/package.json | 4 +- .../template/src/public/theme.css | 13 +- .../theme/blocks/product-description.liquid | 4 +- .../src/theme/blocks/product-purchase.liquid | 244 +++++++++++++----- .../src/theme/blocks/product-vendor.liquid | 4 +- .../src/theme/sections/cart/main-cart.liquid | 58 ++++- .../collections/main-collection.liquid | 96 ++++++- .../src/theme/sections/global/header.liquid | 183 ++++++++++--- .../sections/home/featured-collection.liquid | 54 +++- .../sections/products/main-product.liquid | 50 +++- .../theme/sections/search/main-search.liquid | 138 ++++++++-- src/public/theme.css | 13 +- src/theme/blocks/product-description.liquid | 4 +- src/theme/blocks/product-purchase.liquid | 244 +++++++++++++----- src/theme/blocks/product-vendor.liquid | 4 +- src/theme/sections/cart/main-cart.liquid | 58 ++++- .../collections/main-collection.liquid | 96 ++++++- src/theme/sections/global/header.liquid | 183 ++++++++++--- .../sections/home/featured-collection.liquid | 54 +++- .../sections/products/main-product.liquid | 50 +++- src/theme/sections/search/main-search.liquid | 138 ++++++++-- tests/template-sync.test.js | 5 +- tests/theme-capabilities.test.js | 12 +- 27 files changed, 1414 insertions(+), 406 deletions(-) delete mode 100644 .github/workflows/format-modernization.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 34e83f6..6b86f86 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,57 +6,13 @@ on: pull_request: permissions: - contents: write + contents: read concurrency: group: ci-${{ github.ref }} cancel-in-progress: true jobs: - format-modernization: - if: github.event_name == 'pull_request' && github.head_ref == 'feat/modernize-shopify-2026' && github.actor != 'github-actions[bot]' - runs-on: ubuntu-latest - timeout-minutes: 10 - - steps: - - name: Check out modernization branch - uses: actions/checkout@v7 - with: - ref: ${{ github.head_ref }} - fetch-depth: 0 - - - name: Set up Node.js - uses: actions/setup-node@v7 - with: - node-version: 24 - - - name: Enable Corepack - run: corepack enable - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Format repository - run: pnpm format - - - name: Commit formatting and restore normal CI - shell: bash - env: - HEAD_BRANCH: ${{ github.head_ref }} - run: | - set -euo pipefail - git fetch origin main - git show origin/main:.github/workflows/ci.yml > .github/workflows/ci.yml - rm -f .github/workflows/format-modernization.yml - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add -A - if git diff --cached --quiet; then - exit 0 - fi - git commit -m "Format Shopify modernization changes" - git push origin HEAD:"${HEAD_BRANCH}" - validate: runs-on: ubuntu-latest timeout-minutes: 15 diff --git a/.github/workflows/format-modernization.yml b/.github/workflows/format-modernization.yml deleted file mode 100644 index c35297c..0000000 --- a/.github/workflows/format-modernization.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: Format modernization branch - -on: - push: - branches: [feat/modernize-shopify-2026] - -permissions: - contents: write - -jobs: - format: - if: github.actor != 'github-actions[bot]' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - with: - ref: feat/modernize-shopify-2026 - - uses: actions/setup-node@v7 - with: - node-version: 24 - cache: pnpm - - run: corepack enable - - run: pnpm install --frozen-lockfile - - run: pnpm format - - name: Commit formatted output and remove this one-shot workflow - shell: bash - run: | - set -euo pipefail - rm .github/workflows/format-modernization.yml - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add -A - if git diff --cached --quiet; then - exit 0 - fi - git commit -m "Format Shopify modernization changes" - git push origin HEAD:feat/modernize-shopify-2026 diff --git a/docs/RECIPES.md b/docs/RECIPES.md index d1832aa..7c10150 100644 --- a/docs/RECIPES.md +++ b/docs/RECIPES.md @@ -16,11 +16,13 @@ Fresh Liquid Loom projects prefer Shopify-native component assets: ```liquid {% stylesheet %} -.product-card { display: grid; } + .product-card { + display: grid; + } {% endstylesheet %} {% javascript %} -customElements.define("product-card", class extends HTMLElement {}); + customElements.define('product-card', class extends HTMLElement {}); {% endjavascript %} ``` diff --git a/docs/SHOPIFY_2026_MODERNIZATION.md b/docs/SHOPIFY_2026_MODERNIZATION.md index 6ced34f..ac675a2 100644 --- a/docs/SHOPIFY_2026_MODERNIZATION.md +++ b/docs/SHOPIFY_2026_MODERNIZATION.md @@ -64,11 +64,11 @@ Fresh projects should prefer component-local assets for component-local behavior ```liquid {% stylesheet %} -.product-card { ... } + .product-card { ... } {% endstylesheet %} {% javascript %} -customElements.define(...) + customElements.define(...) {% endjavascript %} ``` @@ -168,15 +168,15 @@ The reference theme should define one semantic palette in `settings_schema.json` ```json { - "type": "color_palette", - "id": "colors", - "default": { - "background": "#F8F7F2", - "foreground": "#111820", - "surface": "#FFFFFF", - "accent": "#176C5B", - "accent_contrast": "#FFFFFF" - } + "type": "color_palette", + "id": "colors", + "default": { + "background": "#F8F7F2", + "foreground": "#111820", + "surface": "#FFFFFF", + "accent": "#176C5B", + "accent_contrast": "#FFFFFF" + } } ``` diff --git a/packages/create-liquid-loom/template/package.json b/packages/create-liquid-loom/template/package.json index e61a4a7..fb7abc0 100644 --- a/packages/create-liquid-loom/template/package.json +++ b/packages/create-liquid-loom/template/package.json @@ -3,7 +3,9 @@ "version": "0.1.0", "private": true, "type": "module", - "engines": { "node": ">=22.12.0" }, + "engines": { + "node": ">=22.12.0" + }, "packageManager": "pnpm@11.11.0", "scripts": { "analyze": "liquid-loom analyze", diff --git a/packages/create-liquid-loom/template/src/public/theme.css b/packages/create-liquid-loom/template/src/public/theme.css index 64901c6..69995dd 100644 --- a/packages/create-liquid-loom/template/src/public/theme.css +++ b/packages/create-liquid-loom/template/src/public/theme.css @@ -27,7 +27,14 @@ body { margin: 0; background: var(--color-background); color: var(--color-foreground); - font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + "Segoe UI", + sans-serif; font-size: 1rem; line-height: 1.6; text-rendering: optimizeLegibility; @@ -146,7 +153,9 @@ textarea { font-weight: 700; padding: 0.75rem 1.35rem; text-decoration: none; - transition: transform 160ms ease, box-shadow 160ms ease; + transition: + transform 160ms ease, + box-shadow 160ms ease; } .button:hover { diff --git a/packages/create-liquid-loom/template/src/theme/blocks/product-description.liquid b/packages/create-liquid-loom/template/src/theme/blocks/product-description.liquid index a6aedda..fd4725d 100644 --- a/packages/create-liquid-loom/template/src/theme/blocks/product-description.liquid +++ b/packages/create-liquid-loom/template/src/theme/blocks/product-description.liquid @@ -1,2 +1,4 @@ -{%- if product.description != blank -%}
{{ product.description }}
{%- endif -%} +{%- if product.description != blank -%} +
{{ product.description }}
+{%- endif -%} {% schema %}{"name":"Product description","presets":[{"name":"Product description"}]}{% endschema %} diff --git a/packages/create-liquid-loom/template/src/theme/blocks/product-purchase.liquid b/packages/create-liquid-loom/template/src/theme/blocks/product-purchase.liquid index 74f2b77..49962f7 100644 --- a/packages/create-liquid-loom/template/src/theme/blocks/product-purchase.liquid +++ b/packages/create-liquid-loom/template/src/theme/blocks/product-purchase.liquid @@ -1,88 +1,192 @@ {%- form 'product', product, id: 'ProductForm-' | append: block.id, novalidate: 'novalidate', data-product-form: '' -%} {%- unless product.has_only_default_variant -%} -
- {%- else -%}{%- endunless -%} - {%- if product.selling_plan_groups.size > 0 -%}
{%- endif -%} -
- +
+ +
+ {%- else -%} + + {%- endunless -%} + {%- if product.selling_plan_groups.size > 0 -%} +
+ +
+ {%- endif -%} +
+ +
+

{%- endform -%} - +
{% stylesheet %} -.field { display: grid; gap: 0.45rem; margin-block: 1rem; } -.field label { font-size: 0.85rem; font-weight: 700; } -.field input, .field select, .field textarea { width: 100%; min-height: 3rem; border: 1px solid var(--color-border); border-radius: var(--radius-sm); background: var(--color-surface); padding: 0.7rem 0.85rem; } -.quantity { display: grid; width: 9rem; grid-template-columns: 2.6rem 1fr 2.6rem; border: 1px solid var(--color-border); border-radius: 999px; background: var(--color-surface); } -.quantity button, .quantity input { min-height: 2.8rem; border: 0; background: transparent; text-align: center; } -.quantity input { width: 100%; appearance: textfield; } -.product-form__status { min-height: 1.5em; margin-block: 0.75rem 0; } + .field { + display: grid; + gap: 0.45rem; + margin-block: 1rem; + } + .field label { + font-size: 0.85rem; + font-weight: 700; + } + .field input, + .field select, + .field textarea { + width: 100%; + min-height: 3rem; + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + background: var(--color-surface); + padding: 0.7rem 0.85rem; + } + .quantity { + display: grid; + width: 9rem; + grid-template-columns: 2.6rem 1fr 2.6rem; + border: 1px solid var(--color-border); + border-radius: 999px; + background: var(--color-surface); + } + .quantity button, + .quantity input { + min-height: 2.8rem; + border: 0; + background: transparent; + text-align: center; + } + .quantity input { + width: 100%; + appearance: textfield; + } + .product-form__status { + min-height: 1.5em; + margin-block: 0.75rem 0; + } {% endstylesheet %} {% javascript %} -class QuantityInput extends HTMLElement { - connectedCallback() { - this.input = this.querySelector("input[type='number']"); - this.querySelectorAll("button").forEach((button) => button.addEventListener("click", () => this.change(button.dataset.quantityAction))); - } - change(action) { - if (!this.input) return; - const step = Number(this.input.step) || 1; - const minimum = Number(this.input.min) || 1; - const current = Number(this.input.value) || minimum; - this.input.value = String(Math.max(minimum, current + (action === "increase" ? step : -step))); - this.input.dispatchEvent(new Event("change", { bubbles: true })); + class QuantityInput extends HTMLElement { + connectedCallback() { + this.input = this.querySelector("input[type='number']"); + this.querySelectorAll('button').forEach((button) => + button.addEventListener('click', () => this.change(button.dataset.quantityAction)) + ); + } + change(action) { + if (!this.input) return; + const step = Number(this.input.step) || 1; + const minimum = Number(this.input.min) || 1; + const current = Number(this.input.value) || minimum; + this.input.value = String(Math.max(minimum, current + (action === 'increase' ? step : -step))); + this.input.dispatchEvent(new Event('change', { bubbles: true })); + } } -} -class ProductForm extends HTMLElement { - connectedCallback() { - this.form = this.querySelector("form"); - this.select = this.querySelector("[data-variant-select]"); - this.submit = this.querySelector("button[type='submit']"); - this.status = this.querySelector("[data-product-form-status]"); - const data = this.querySelector("[data-product-variants]"); - if (data) this.variants = JSON.parse(data.textContent); - this.select?.addEventListener("change", () => this.variantChanged()); - this.form?.addEventListener("submit", (event) => this.submitWithStandardAction(event)); - } - variantChanged() { - const variant = this.variants?.find(({ id }) => String(id) === this.select.value); - if (!variant) return; - this.submit?.toggleAttribute("disabled", !variant.available); - const url = new URL(window.location.href); - url.searchParams.set("variant", variant.id); - window.history.replaceState({}, "", url); - } - async submitWithStandardAction(event) { - if (!window.Shopify?.actions?.updateCart || !this.form) return; - event.preventDefault(); - const formData = new FormData(this.form); - const merchandiseId = formData.get("id"); - if (!merchandiseId) return; - const line = { merchandiseId: String(merchandiseId), quantity: Math.max(1, Number(formData.get("quantity")) || 1) }; - const sellingPlanId = formData.get("selling_plan"); - if (sellingPlanId) line.sellingPlanId = String(sellingPlanId); - this.submit?.setAttribute("disabled", ""); - if (this.status) this.status.textContent = ""; - try { - const { cart, userErrors, warnings } = await window.Shopify.actions.updateCart({ lines: [line] }, { event: { context: "product" } }); - if (userErrors?.length) { if (this.status) this.status.textContent = userErrors[0].message; return; } - if (this.status) this.status.textContent = warnings?.[0]?.message || "Added to cart."; - if (cart) document.querySelectorAll("[data-cart-count]").forEach((node) => { node.textContent = String(cart.totalQuantity); }); - } catch (error) { - console.error(error); - if (this.status) this.status.textContent = "Unable to update the cart. Try again."; - } finally { - const variant = this.variants?.find(({ id }) => String(id) === String(formData.get("id"))); - this.submit?.toggleAttribute("disabled", variant ? !variant.available : false); + class ProductForm extends HTMLElement { + connectedCallback() { + this.form = this.querySelector('form'); + this.select = this.querySelector('[data-variant-select]'); + this.submit = this.querySelector("button[type='submit']"); + this.status = this.querySelector('[data-product-form-status]'); + const data = this.querySelector('[data-product-variants]'); + if (data) this.variants = JSON.parse(data.textContent); + this.select?.addEventListener('change', () => this.variantChanged()); + this.form?.addEventListener('submit', (event) => this.submitWithStandardAction(event)); + } + variantChanged() { + const variant = this.variants?.find(({ id }) => String(id) === this.select.value); + if (!variant) return; + this.submit?.toggleAttribute('disabled', !variant.available); + const url = new URL(window.location.href); + url.searchParams.set('variant', variant.id); + window.history.replaceState({}, '', url); + } + async submitWithStandardAction(event) { + if (!window.Shopify?.actions?.updateCart || !this.form) return; + event.preventDefault(); + const formData = new FormData(this.form); + const merchandiseId = formData.get('id'); + if (!merchandiseId) return; + const line = { + merchandiseId: String(merchandiseId), + quantity: Math.max(1, Number(formData.get('quantity')) || 1) + }; + const sellingPlanId = formData.get('selling_plan'); + if (sellingPlanId) line.sellingPlanId = String(sellingPlanId); + this.submit?.setAttribute('disabled', ''); + if (this.status) this.status.textContent = ''; + try { + const { cart, userErrors, warnings } = await window.Shopify.actions.updateCart( + { lines: [line] }, + { event: { context: 'product' } } + ); + if (userErrors?.length) { + if (this.status) this.status.textContent = userErrors[0].message; + return; + } + if (this.status) this.status.textContent = warnings?.[0]?.message || 'Added to cart.'; + if (cart) + document.querySelectorAll('[data-cart-count]').forEach((node) => { + node.textContent = String(cart.totalQuantity); + }); + } catch (error) { + console.error(error); + if (this.status) this.status.textContent = 'Unable to update the cart. Try again.'; + } finally { + const variant = this.variants?.find(({ id }) => String(id) === String(formData.get('id'))); + this.submit?.toggleAttribute('disabled', variant ? !variant.available : false); + } } } -} -if (!customElements.get("quantity-input")) customElements.define("quantity-input", QuantityInput); -if (!customElements.get("product-form")) customElements.define("product-form", ProductForm); + if (!customElements.get('quantity-input')) customElements.define('quantity-input', QuantityInput); + if (!customElements.get('product-form')) customElements.define('product-form', ProductForm); {% endjavascript %} {% schema %}{"name":"Product purchase","presets":[{"name":"Product purchase"}]}{% endschema %} diff --git a/packages/create-liquid-loom/template/src/theme/blocks/product-vendor.liquid b/packages/create-liquid-loom/template/src/theme/blocks/product-vendor.liquid index df953df..5af67ea 100644 --- a/packages/create-liquid-loom/template/src/theme/blocks/product-vendor.liquid +++ b/packages/create-liquid-loom/template/src/theme/blocks/product-vendor.liquid @@ -1,2 +1,4 @@ -{%- if product.vendor != blank -%}

{{ product.vendor | escape }}

{%- endif -%} +{%- if product.vendor != blank -%} +

{{ product.vendor | escape }}

+{%- endif -%} {% schema %}{"name":"Product vendor","presets":[{"name":"Product vendor"}]}{% endschema %} diff --git a/packages/create-liquid-loom/template/src/theme/sections/cart/main-cart.liquid b/packages/create-liquid-loom/template/src/theme/sections/cart/main-cart.liquid index 530a305..e833524 100644 --- a/packages/create-liquid-loom/template/src/theme/sections/cart/main-cart.liquid +++ b/packages/create-liquid-loom/template/src/theme/sections/cart/main-cart.liquid @@ -1,2 +1,58 @@ -

{{ 'cart.title' | t }}

{%- if cart.empty? -%}{%- else -%}
{%- for item in cart.items -%}{%- endfor -%}
ProductQuantityTotal
{%- if item.image -%}{{ item.image | image_url: width: 200 | image_tag: loading: 'lazy' }}{%- endif -%}
{{ item.product.title | escape }}
{{ item.variant.title }}
{{ item.final_line_price | money }}
{%- endif -%}
+ +
+
+

{{ 'cart.title' | t }}

+ {%- if cart.empty? -%} +
+

{{ 'cart.empty' | t }}

+ {{ 'actions.continue_shopping' | t }} +
+ {%- else -%} +
+ + + + + + + + + + {%- for item in cart.items -%} + + + + + + {%- endfor -%} + +
ProductQuantityTotal
+
+ {%- if item.image -%} + {{ item.image | image_url: width: 200 | image_tag: loading: 'lazy' }} + {%- endif -%} +
+ {{ item.product.title | escape }}
+ {{ item.variant.title }} +
+
+
+ + {{ item.final_line_price | money }}
+ +
+ {%- endif -%} +
+
+
{% schema %}{"name":"Main cart","settings":[]}{% endschema %} diff --git a/packages/create-liquid-loom/template/src/theme/sections/collections/main-collection.liquid b/packages/create-liquid-loom/template/src/theme/sections/collections/main-collection.liquid index c449ffa..f3d096a 100644 --- a/packages/create-liquid-loom/template/src/theme/sections/collections/main-collection.liquid +++ b/packages/create-liquid-loom/template/src/theme/sections/collections/main-collection.liquid @@ -1,8 +1,98 @@
-

Collection

{{ collection.title | escape }}

{%- if collection.description != blank -%}
{{ collection.description }}
{%- endif -%}
- {%- if collection.filters.size > 0 -%}
{%- for filter in collection.filters -%}
{{ filter.label | escape }}{%- if filter.active_values.size > 0 %} ({{ filter.active_values.size }}){% endif %}
{%- case filter.type -%}{%- when 'boolean', 'list' -%}{%- for value in filter.values -%}{%- endfor -%}{%- when 'price_range' -%}{%- endcase -%}
{%- endfor -%}Clear
{%- endif -%} - {%- paginate collection.products by section.settings.products_per_page -%}{%- if paginate.pages > 1 -%}
{{ paginate | default_pagination }}
{%- endif -%}{%- endpaginate -%} + +
+

Collection

+

{{ collection.title | escape }}

+ {%- if collection.description != blank -%} +
{{ collection.description }}
+ {%- endif -%} +
+
+ {%- if collection.filters.size > 0 -%} +
+ {%- for filter in collection.filters -%} +
+ + {{ filter.label | escape }} + {%- if filter.active_values.size > 0 %} ({{ filter.active_values.size }}){% endif %} + +
+ {%- case filter.type -%} + {%- when 'boolean', 'list' -%} + {%- for value in filter.values -%} + + {%- endfor -%} + {%- when 'price_range' -%} + + {%- endcase -%} +
+
+ {%- endfor -%} + Clear +
+ {%- endif -%} + {%- paginate collection.products by section.settings.products_per_page -%} + + {%- if paginate.pages > 1 -%} +
{{ paginate | default_pagination }}
+ {%- endif -%} + {%- endpaginate -%}
{% schema %}{"name":"Main collection","settings":[{"type":"range","id":"products_per_page","min":8,"max":32,"step":4,"label":"Products per page","default":16}]}{% endschema %} diff --git a/packages/create-liquid-loom/template/src/theme/sections/global/header.liquid b/packages/create-liquid-loom/template/src/theme/sections/global/header.liquid index f0a2e93..71ff71e 100644 --- a/packages/create-liquid-loom/template/src/theme/sections/global/header.liquid +++ b/packages/create-liquid-loom/template/src/theme/sections/global/header.liquid @@ -2,63 +2,167 @@ {% stylesheet %} -.site-header { position: sticky; top: 0; z-index: 40; border-bottom: 1px solid color-mix(in srgb, var(--color-border) 75%, transparent); background: color-mix(in srgb, var(--color-background) 88%, transparent); backdrop-filter: blur(18px); } -.site-header__inner { display: grid; min-height: 4.8rem; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 1rem; } -.site-header__brand { justify-self: center; font-size: 1.1rem; font-weight: 800; letter-spacing: -0.03em; text-decoration: none; } -.site-header__logo { width: auto; max-height: 2.3rem; } -.site-header__nav, .site-header__actions { display: flex; align-items: center; gap: 1.25rem; } -.site-header__nav a, .site-header__actions a { font-size: 0.9rem; font-weight: 650; text-decoration: none; } -.site-header__actions { justify-self: end; } -.site-header__menu-button { display: none; border: 0; background: transparent; cursor: pointer; padding: 0.5rem; } -.mobile-menu { border-top: 1px solid var(--color-border); background: var(--color-background); padding: 1rem; } -.mobile-menu[hidden] { display: none; } -.mobile-menu a { display: block; padding: 0.8rem; font-size: 1.15rem; font-weight: 700; text-decoration: none; } -shopify-account { --shopify-account-signed-in-avatar-size: 2rem; --shopify-account-signed-in-avatar-color-background: var(--color-accent); --shopify-account-signed-in-avatar-color-text: var(--color-accent-contrast); } -@media (max-width: 900px) { .site-header__nav { display: none; } .site-header__menu-button { display: inline-flex; } } -@media (max-width: 600px) { .site-header__actions > a:first-child { display: none; } } + .site-header { + position: sticky; + top: 0; + z-index: 40; + border-bottom: 1px solid color-mix(in srgb, var(--color-border) 75%, transparent); + background: color-mix(in srgb, var(--color-background) 88%, transparent); + backdrop-filter: blur(18px); + } + .site-header__inner { + display: grid; + min-height: 4.8rem; + grid-template-columns: 1fr auto 1fr; + align-items: center; + gap: 1rem; + } + .site-header__brand { + justify-self: center; + font-size: 1.1rem; + font-weight: 800; + letter-spacing: -0.03em; + text-decoration: none; + } + .site-header__logo { + width: auto; + max-height: 2.3rem; + } + .site-header__nav, + .site-header__actions { + display: flex; + align-items: center; + gap: 1.25rem; + } + .site-header__nav a, + .site-header__actions a { + font-size: 0.9rem; + font-weight: 650; + text-decoration: none; + } + .site-header__actions { + justify-self: end; + } + .site-header__menu-button { + display: none; + border: 0; + background: transparent; + cursor: pointer; + padding: 0.5rem; + } + .mobile-menu { + border-top: 1px solid var(--color-border); + background: var(--color-background); + padding: 1rem; + } + .mobile-menu[hidden] { + display: none; + } + .mobile-menu a { + display: block; + padding: 0.8rem; + font-size: 1.15rem; + font-weight: 700; + text-decoration: none; + } + shopify-account { + --shopify-account-signed-in-avatar-size: 2rem; + --shopify-account-signed-in-avatar-color-background: var(--color-accent); + --shopify-account-signed-in-avatar-color-text: var(--color-accent-contrast); + } + @media (max-width: 900px) { + .site-header__nav { + display: none; + } + .site-header__menu-button { + display: inline-flex; + } + } + @media (max-width: 600px) { + .site-header__actions > a:first-child { + display: none; + } + } {% endstylesheet %} {% javascript %} -class MenuDisclosure extends HTMLElement { - connectedCallback() { - this.button = this.querySelector("button"); - this.panel = this.querySelector("[data-menu-panel]"); - if (!this.button || !this.panel) return; - this.button.addEventListener("click", () => this.toggle()); - this.addEventListener("keydown", (event) => { - if (event.key === "Escape" && this.isOpen()) { this.close(); this.button.focus(); } - }); + class MenuDisclosure extends HTMLElement { + connectedCallback() { + this.button = this.querySelector('button'); + this.panel = this.querySelector('[data-menu-panel]'); + if (!this.button || !this.panel) return; + this.button.addEventListener('click', () => this.toggle()); + this.addEventListener('keydown', (event) => { + if (event.key === 'Escape' && this.isOpen()) { + this.close(); + this.button.focus(); + } + }); + } + isOpen() { + return this.button.getAttribute('aria-expanded') === 'true'; + } + toggle() { + this.isOpen() ? this.close() : this.open(); + } + open() { + this.button.setAttribute('aria-expanded', 'true'); + this.panel.removeAttribute('hidden'); + } + close() { + this.button.setAttribute('aria-expanded', 'false'); + this.panel.setAttribute('hidden', ''); + } } - isOpen() { return this.button.getAttribute("aria-expanded") === "true"; } - toggle() { this.isOpen() ? this.close() : this.open(); } - open() { this.button.setAttribute("aria-expanded", "true"); this.panel.removeAttribute("hidden"); } - close() { this.button.setAttribute("aria-expanded", "false"); this.panel.setAttribute("hidden", ""); } -} -if (!customElements.get("menu-disclosure")) customElements.define("menu-disclosure", MenuDisclosure); + if (!customElements.get('menu-disclosure')) customElements.define('menu-disclosure', MenuDisclosure); {% endjavascript %} {% schema %} @@ -67,7 +171,12 @@ if (!customElements.get("menu-disclosure")) customElements.define("menu-disclosu "settings": [ { "type": "image_picker", "id": "logo", "label": "Logo" }, { "type": "link_list", "id": "menu", "label": "Menu", "default": "main-menu" }, - { "type": "link_list", "id": "customer_account_menu", "label": "Customer account menu", "default": "customer-account-main-menu" } + { + "type": "link_list", + "id": "customer_account_menu", + "label": "Customer account menu", + "default": "customer-account-main-menu" + } ] } {% endschema %} diff --git a/packages/create-liquid-loom/template/src/theme/sections/home/featured-collection.liquid b/packages/create-liquid-loom/template/src/theme/sections/home/featured-collection.liquid index 6b8a2c8..51f12c4 100644 --- a/packages/create-liquid-loom/template/src/theme/sections/home/featured-collection.liquid +++ b/packages/create-liquid-loom/template/src/theme/sections/home/featured-collection.liquid @@ -1,3 +1,55 @@ {%- assign featured_collection = section.settings.collection -%} -

{{ section.settings.eyebrow | escape }}

{{ section.settings.heading | escape }}

{%- if featured_collection != blank -%}{{ 'actions.view_all' | t }}{%- endif -%}
{%- if featured_collection != blank -%}{%- for product in featured_collection.products limit: section.settings.products_to_show -%}{%- endfor -%}{%- else -%}{%- for index in (1..4) -%}
{{ 'product-' | append: index | placeholder_svg_tag }}

Example product

{{ 2400 | money }}
{%- endfor -%}{%- endif -%}
+
+
+
+
+

{{ section.settings.eyebrow | escape }}

+

{{ section.settings.heading | escape }}

+
+ {%- if featured_collection != blank -%} + {{ 'actions.view_all' | t }} + {%- endif -%} +
+
+ {%- if featured_collection != blank -%} + {%- for product in featured_collection.products limit: section.settings.products_to_show -%} + + + + {%- endfor -%} + {%- else -%} + {%- for index in (1..4) -%} +
+
{{ 'product-' | append: index | placeholder_svg_tag }}
+
+

Example product

+ {{ 2400 | money }} +
+
+ {%- endfor -%} + {%- endif -%} +
+
+
{% schema %}{"name":"Featured collection","settings":[{"type":"text","id":"eyebrow","label":"Eyebrow","default":"Selected for you"},{"type":"text","id":"heading","label":"Heading","default":"Products worth a closer look"},{"type":"collection","id":"collection","label":"Collection"},{"type":"range","id":"products_to_show","min":2,"max":8,"step":1,"label":"Products","default":4}],"presets":[{"name":"Featured collection"}]}{% endschema %} diff --git a/packages/create-liquid-loom/template/src/theme/sections/products/main-product.liquid b/packages/create-liquid-loom/template/src/theme/sections/products/main-product.liquid index b15bf60..c82a3d5 100644 --- a/packages/create-liquid-loom/template/src/theme/sections/products/main-product.liquid +++ b/packages/create-liquid-loom/template/src/theme/sections/products/main-product.liquid @@ -1,10 +1,14 @@ - +
{% content_for 'blocks' %}
@@ -12,13 +16,39 @@ {% stylesheet %} -.product { display: grid; grid-template-columns: minmax(0, 1.1fr) minmax(320px, 0.9fr); gap: clamp(2rem, 7vw, 7rem); } -.product__gallery { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1rem; } -.product__gallery img { width: 100%; border-radius: var(--radius-md); } -.product__details { display: grid; align-self: start; gap: 1rem; } -.product__details .price { display: block; font-size: 1.2rem; } -@media (max-width: 900px) { .product { grid-template-columns: 1fr; } } -@media (max-width: 600px) { .product__gallery { grid-template-columns: 1fr; } } + .product { + display: grid; + grid-template-columns: minmax(0, 1.1fr) minmax(320px, 0.9fr); + gap: clamp(2rem, 7vw, 7rem); + } + .product__gallery { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1rem; + } + .product__gallery img { + width: 100%; + border-radius: var(--radius-md); + } + .product__details { + display: grid; + align-self: start; + gap: 1rem; + } + .product__details .price { + display: block; + font-size: 1.2rem; + } + @media (max-width: 900px) { + .product { + grid-template-columns: 1fr; + } + } + @media (max-width: 600px) { + .product__gallery { + grid-template-columns: 1fr; + } + } {% endstylesheet %} {% schema %} diff --git a/packages/create-liquid-loom/template/src/theme/sections/search/main-search.liquid b/packages/create-liquid-loom/template/src/theme/sections/search/main-search.liquid index 261fd91..6796867 100644 --- a/packages/create-liquid-loom/template/src/theme/sections/search/main-search.liquid +++ b/packages/create-liquid-loom/template/src/theme/sections/search/main-search.liquid @@ -1,46 +1,128 @@
-

Search

Find what you need

+

Search

+

Find what you need

-
+
+
+ +
+ +
- {%- if search.performed -%}
{%- for item in search.results -%}{%- else -%}

No results for “{{ search.terms | escape }}”.

{%- endfor -%}
{%- endif -%} + {%- if search.performed -%} +
+ {%- for item in search.results -%} + + {%- else -%} +

No results for “{{ search.terms | escape }}”.

+ {%- endfor -%} +
+ {%- endif -%}
{% stylesheet %} -.search-field { display: grid; gap: 0.45rem; margin-block: 1rem; } -.search-field label { font-size: 0.85rem; font-weight: 700; } -.search-field input { width: 100%; min-height: 3rem; border: 1px solid var(--color-border); border-radius: var(--radius-sm); background: var(--color-surface); padding: 0.7rem 0.85rem; } -.predictive-search__results { border: 1px solid var(--color-border); border-radius: var(--radius-md); margin-top: 0.5rem; background: var(--color-surface); box-shadow: var(--shadow-soft); padding: 1rem; } -.predictive-search__results ul { display: grid; gap: 0.65rem; margin: 0; padding: 0; list-style: none; } -.predictive-search__results a { display: flex; justify-content: space-between; gap: 1rem; text-decoration: none; } + .search-field { + display: grid; + gap: 0.45rem; + margin-block: 1rem; + } + .search-field label { + font-size: 0.85rem; + font-weight: 700; + } + .search-field input { + width: 100%; + min-height: 3rem; + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + background: var(--color-surface); + padding: 0.7rem 0.85rem; + } + .predictive-search__results { + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + margin-top: 0.5rem; + background: var(--color-surface); + box-shadow: var(--shadow-soft); + padding: 1rem; + } + .predictive-search__results ul { + display: grid; + gap: 0.65rem; + margin: 0; + padding: 0; + list-style: none; + } + .predictive-search__results a { + display: flex; + justify-content: space-between; + gap: 1rem; + text-decoration: none; + } {% endstylesheet %} {% javascript %} -class PredictiveSearch extends HTMLElement { - connectedCallback() { - this.input = this.querySelector("input[type='search']"); this.results = this.querySelector("[data-predictive-search-results]"); - if (!this.input || !this.results) return; - this.input.addEventListener("input", () => { clearTimeout(this.timer); this.timer = setTimeout(() => this.search(), 180); }); - } - async search() { - const query = this.input.value.trim(); this.controller?.abort(); - if (query.length < 2) { this.results.replaceChildren(); this.input.setAttribute("aria-expanded", "false"); return; } - this.controller = new AbortController(); - try { - const response = await fetch(`/search/suggest?q=${encodeURIComponent(query)}&resources[type]=product§ion_id=predictive-search`, { signal: this.controller.signal }); - if (!response.ok) throw new Error(`Search request failed: ${response.status}`); - const documentFragment = new DOMParser().parseFromString(await response.text(), "text/html"); - const content = documentFragment.querySelector(".predictive-search__results"); - this.results.replaceChildren(...(content ? [content] : [])); this.input.setAttribute("aria-expanded", content ? "true" : "false"); - } catch (error) { if (error.name !== "AbortError") console.error(error); } + class PredictiveSearch extends HTMLElement { + connectedCallback() { + this.input = this.querySelector("input[type='search']"); + this.results = this.querySelector('[data-predictive-search-results]'); + if (!this.input || !this.results) return; + this.input.addEventListener('input', () => { + clearTimeout(this.timer); + this.timer = setTimeout(() => this.search(), 180); + }); + } + async search() { + const query = this.input.value.trim(); + this.controller?.abort(); + if (query.length < 2) { + this.results.replaceChildren(); + this.input.setAttribute('aria-expanded', 'false'); + return; + } + this.controller = new AbortController(); + try { + const response = await fetch( + `/search/suggest?q=${encodeURIComponent(query)}&resources[type]=product§ion_id=predictive-search`, + { signal: this.controller.signal } + ); + if (!response.ok) throw new Error(`Search request failed: ${response.status}`); + const documentFragment = new DOMParser().parseFromString(await response.text(), 'text/html'); + const content = documentFragment.querySelector('.predictive-search__results'); + this.results.replaceChildren(...(content ? [content] : [])); + this.input.setAttribute('aria-expanded', content ? 'true' : 'false'); + } catch (error) { + if (error.name !== 'AbortError') console.error(error); + } + } } -} -if (!customElements.get("predictive-search")) customElements.define("predictive-search", PredictiveSearch); + if (!customElements.get('predictive-search')) customElements.define('predictive-search', PredictiveSearch); {% endjavascript %} {% schema %}{"name":"Main search","settings":[]}{% endschema %} diff --git a/src/public/theme.css b/src/public/theme.css index 64901c6..69995dd 100644 --- a/src/public/theme.css +++ b/src/public/theme.css @@ -27,7 +27,14 @@ body { margin: 0; background: var(--color-background); color: var(--color-foreground); - font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + "Segoe UI", + sans-serif; font-size: 1rem; line-height: 1.6; text-rendering: optimizeLegibility; @@ -146,7 +153,9 @@ textarea { font-weight: 700; padding: 0.75rem 1.35rem; text-decoration: none; - transition: transform 160ms ease, box-shadow 160ms ease; + transition: + transform 160ms ease, + box-shadow 160ms ease; } .button:hover { diff --git a/src/theme/blocks/product-description.liquid b/src/theme/blocks/product-description.liquid index a6aedda..fd4725d 100644 --- a/src/theme/blocks/product-description.liquid +++ b/src/theme/blocks/product-description.liquid @@ -1,2 +1,4 @@ -{%- if product.description != blank -%}
{{ product.description }}
{%- endif -%} +{%- if product.description != blank -%} +
{{ product.description }}
+{%- endif -%} {% schema %}{"name":"Product description","presets":[{"name":"Product description"}]}{% endschema %} diff --git a/src/theme/blocks/product-purchase.liquid b/src/theme/blocks/product-purchase.liquid index 74f2b77..49962f7 100644 --- a/src/theme/blocks/product-purchase.liquid +++ b/src/theme/blocks/product-purchase.liquid @@ -1,88 +1,192 @@ {%- form 'product', product, id: 'ProductForm-' | append: block.id, novalidate: 'novalidate', data-product-form: '' -%} {%- unless product.has_only_default_variant -%} -
- {%- else -%}{%- endunless -%} - {%- if product.selling_plan_groups.size > 0 -%}
{%- endif -%} -
- +
+ +
+ {%- else -%} + + {%- endunless -%} + {%- if product.selling_plan_groups.size > 0 -%} +
+ +
+ {%- endif -%} +
+ +
+

{%- endform -%} - +
{% stylesheet %} -.field { display: grid; gap: 0.45rem; margin-block: 1rem; } -.field label { font-size: 0.85rem; font-weight: 700; } -.field input, .field select, .field textarea { width: 100%; min-height: 3rem; border: 1px solid var(--color-border); border-radius: var(--radius-sm); background: var(--color-surface); padding: 0.7rem 0.85rem; } -.quantity { display: grid; width: 9rem; grid-template-columns: 2.6rem 1fr 2.6rem; border: 1px solid var(--color-border); border-radius: 999px; background: var(--color-surface); } -.quantity button, .quantity input { min-height: 2.8rem; border: 0; background: transparent; text-align: center; } -.quantity input { width: 100%; appearance: textfield; } -.product-form__status { min-height: 1.5em; margin-block: 0.75rem 0; } + .field { + display: grid; + gap: 0.45rem; + margin-block: 1rem; + } + .field label { + font-size: 0.85rem; + font-weight: 700; + } + .field input, + .field select, + .field textarea { + width: 100%; + min-height: 3rem; + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + background: var(--color-surface); + padding: 0.7rem 0.85rem; + } + .quantity { + display: grid; + width: 9rem; + grid-template-columns: 2.6rem 1fr 2.6rem; + border: 1px solid var(--color-border); + border-radius: 999px; + background: var(--color-surface); + } + .quantity button, + .quantity input { + min-height: 2.8rem; + border: 0; + background: transparent; + text-align: center; + } + .quantity input { + width: 100%; + appearance: textfield; + } + .product-form__status { + min-height: 1.5em; + margin-block: 0.75rem 0; + } {% endstylesheet %} {% javascript %} -class QuantityInput extends HTMLElement { - connectedCallback() { - this.input = this.querySelector("input[type='number']"); - this.querySelectorAll("button").forEach((button) => button.addEventListener("click", () => this.change(button.dataset.quantityAction))); - } - change(action) { - if (!this.input) return; - const step = Number(this.input.step) || 1; - const minimum = Number(this.input.min) || 1; - const current = Number(this.input.value) || minimum; - this.input.value = String(Math.max(minimum, current + (action === "increase" ? step : -step))); - this.input.dispatchEvent(new Event("change", { bubbles: true })); + class QuantityInput extends HTMLElement { + connectedCallback() { + this.input = this.querySelector("input[type='number']"); + this.querySelectorAll('button').forEach((button) => + button.addEventListener('click', () => this.change(button.dataset.quantityAction)) + ); + } + change(action) { + if (!this.input) return; + const step = Number(this.input.step) || 1; + const minimum = Number(this.input.min) || 1; + const current = Number(this.input.value) || minimum; + this.input.value = String(Math.max(minimum, current + (action === 'increase' ? step : -step))); + this.input.dispatchEvent(new Event('change', { bubbles: true })); + } } -} -class ProductForm extends HTMLElement { - connectedCallback() { - this.form = this.querySelector("form"); - this.select = this.querySelector("[data-variant-select]"); - this.submit = this.querySelector("button[type='submit']"); - this.status = this.querySelector("[data-product-form-status]"); - const data = this.querySelector("[data-product-variants]"); - if (data) this.variants = JSON.parse(data.textContent); - this.select?.addEventListener("change", () => this.variantChanged()); - this.form?.addEventListener("submit", (event) => this.submitWithStandardAction(event)); - } - variantChanged() { - const variant = this.variants?.find(({ id }) => String(id) === this.select.value); - if (!variant) return; - this.submit?.toggleAttribute("disabled", !variant.available); - const url = new URL(window.location.href); - url.searchParams.set("variant", variant.id); - window.history.replaceState({}, "", url); - } - async submitWithStandardAction(event) { - if (!window.Shopify?.actions?.updateCart || !this.form) return; - event.preventDefault(); - const formData = new FormData(this.form); - const merchandiseId = formData.get("id"); - if (!merchandiseId) return; - const line = { merchandiseId: String(merchandiseId), quantity: Math.max(1, Number(formData.get("quantity")) || 1) }; - const sellingPlanId = formData.get("selling_plan"); - if (sellingPlanId) line.sellingPlanId = String(sellingPlanId); - this.submit?.setAttribute("disabled", ""); - if (this.status) this.status.textContent = ""; - try { - const { cart, userErrors, warnings } = await window.Shopify.actions.updateCart({ lines: [line] }, { event: { context: "product" } }); - if (userErrors?.length) { if (this.status) this.status.textContent = userErrors[0].message; return; } - if (this.status) this.status.textContent = warnings?.[0]?.message || "Added to cart."; - if (cart) document.querySelectorAll("[data-cart-count]").forEach((node) => { node.textContent = String(cart.totalQuantity); }); - } catch (error) { - console.error(error); - if (this.status) this.status.textContent = "Unable to update the cart. Try again."; - } finally { - const variant = this.variants?.find(({ id }) => String(id) === String(formData.get("id"))); - this.submit?.toggleAttribute("disabled", variant ? !variant.available : false); + class ProductForm extends HTMLElement { + connectedCallback() { + this.form = this.querySelector('form'); + this.select = this.querySelector('[data-variant-select]'); + this.submit = this.querySelector("button[type='submit']"); + this.status = this.querySelector('[data-product-form-status]'); + const data = this.querySelector('[data-product-variants]'); + if (data) this.variants = JSON.parse(data.textContent); + this.select?.addEventListener('change', () => this.variantChanged()); + this.form?.addEventListener('submit', (event) => this.submitWithStandardAction(event)); + } + variantChanged() { + const variant = this.variants?.find(({ id }) => String(id) === this.select.value); + if (!variant) return; + this.submit?.toggleAttribute('disabled', !variant.available); + const url = new URL(window.location.href); + url.searchParams.set('variant', variant.id); + window.history.replaceState({}, '', url); + } + async submitWithStandardAction(event) { + if (!window.Shopify?.actions?.updateCart || !this.form) return; + event.preventDefault(); + const formData = new FormData(this.form); + const merchandiseId = formData.get('id'); + if (!merchandiseId) return; + const line = { + merchandiseId: String(merchandiseId), + quantity: Math.max(1, Number(formData.get('quantity')) || 1) + }; + const sellingPlanId = formData.get('selling_plan'); + if (sellingPlanId) line.sellingPlanId = String(sellingPlanId); + this.submit?.setAttribute('disabled', ''); + if (this.status) this.status.textContent = ''; + try { + const { cart, userErrors, warnings } = await window.Shopify.actions.updateCart( + { lines: [line] }, + { event: { context: 'product' } } + ); + if (userErrors?.length) { + if (this.status) this.status.textContent = userErrors[0].message; + return; + } + if (this.status) this.status.textContent = warnings?.[0]?.message || 'Added to cart.'; + if (cart) + document.querySelectorAll('[data-cart-count]').forEach((node) => { + node.textContent = String(cart.totalQuantity); + }); + } catch (error) { + console.error(error); + if (this.status) this.status.textContent = 'Unable to update the cart. Try again.'; + } finally { + const variant = this.variants?.find(({ id }) => String(id) === String(formData.get('id'))); + this.submit?.toggleAttribute('disabled', variant ? !variant.available : false); + } } } -} -if (!customElements.get("quantity-input")) customElements.define("quantity-input", QuantityInput); -if (!customElements.get("product-form")) customElements.define("product-form", ProductForm); + if (!customElements.get('quantity-input')) customElements.define('quantity-input', QuantityInput); + if (!customElements.get('product-form')) customElements.define('product-form', ProductForm); {% endjavascript %} {% schema %}{"name":"Product purchase","presets":[{"name":"Product purchase"}]}{% endschema %} diff --git a/src/theme/blocks/product-vendor.liquid b/src/theme/blocks/product-vendor.liquid index df953df..5af67ea 100644 --- a/src/theme/blocks/product-vendor.liquid +++ b/src/theme/blocks/product-vendor.liquid @@ -1,2 +1,4 @@ -{%- if product.vendor != blank -%}

{{ product.vendor | escape }}

{%- endif -%} +{%- if product.vendor != blank -%} +

{{ product.vendor | escape }}

+{%- endif -%} {% schema %}{"name":"Product vendor","presets":[{"name":"Product vendor"}]}{% endschema %} diff --git a/src/theme/sections/cart/main-cart.liquid b/src/theme/sections/cart/main-cart.liquid index 530a305..e833524 100644 --- a/src/theme/sections/cart/main-cart.liquid +++ b/src/theme/sections/cart/main-cart.liquid @@ -1,2 +1,58 @@ -

{{ 'cart.title' | t }}

{%- if cart.empty? -%}{%- else -%}
{%- for item in cart.items -%}{%- endfor -%}
ProductQuantityTotal
{%- if item.image -%}{{ item.image | image_url: width: 200 | image_tag: loading: 'lazy' }}{%- endif -%}
{{ item.product.title | escape }}
{{ item.variant.title }}
{{ item.final_line_price | money }}
{%- endif -%}
+ +
+
+

{{ 'cart.title' | t }}

+ {%- if cart.empty? -%} +
+

{{ 'cart.empty' | t }}

+ {{ 'actions.continue_shopping' | t }} +
+ {%- else -%} +
+ + + + + + + + + + {%- for item in cart.items -%} + + + + + + {%- endfor -%} + +
ProductQuantityTotal
+
+ {%- if item.image -%} + {{ item.image | image_url: width: 200 | image_tag: loading: 'lazy' }} + {%- endif -%} +
+ {{ item.product.title | escape }}
+ {{ item.variant.title }} +
+
+
+ + {{ item.final_line_price | money }}
+ +
+ {%- endif -%} +
+
+
{% schema %}{"name":"Main cart","settings":[]}{% endschema %} diff --git a/src/theme/sections/collections/main-collection.liquid b/src/theme/sections/collections/main-collection.liquid index c449ffa..f3d096a 100644 --- a/src/theme/sections/collections/main-collection.liquid +++ b/src/theme/sections/collections/main-collection.liquid @@ -1,8 +1,98 @@
-

Collection

{{ collection.title | escape }}

{%- if collection.description != blank -%}
{{ collection.description }}
{%- endif -%}
- {%- if collection.filters.size > 0 -%}
{%- for filter in collection.filters -%}
{{ filter.label | escape }}{%- if filter.active_values.size > 0 %} ({{ filter.active_values.size }}){% endif %}
{%- case filter.type -%}{%- when 'boolean', 'list' -%}{%- for value in filter.values -%}{%- endfor -%}{%- when 'price_range' -%}{%- endcase -%}
{%- endfor -%}Clear
{%- endif -%} - {%- paginate collection.products by section.settings.products_per_page -%}{%- if paginate.pages > 1 -%}
{{ paginate | default_pagination }}
{%- endif -%}{%- endpaginate -%} + +
+

Collection

+

{{ collection.title | escape }}

+ {%- if collection.description != blank -%} +
{{ collection.description }}
+ {%- endif -%} +
+
+ {%- if collection.filters.size > 0 -%} +
+ {%- for filter in collection.filters -%} +
+ + {{ filter.label | escape }} + {%- if filter.active_values.size > 0 %} ({{ filter.active_values.size }}){% endif %} + +
+ {%- case filter.type -%} + {%- when 'boolean', 'list' -%} + {%- for value in filter.values -%} + + {%- endfor -%} + {%- when 'price_range' -%} + + {%- endcase -%} +
+
+ {%- endfor -%} + Clear +
+ {%- endif -%} + {%- paginate collection.products by section.settings.products_per_page -%} + + {%- if paginate.pages > 1 -%} +
{{ paginate | default_pagination }}
+ {%- endif -%} + {%- endpaginate -%}
{% schema %}{"name":"Main collection","settings":[{"type":"range","id":"products_per_page","min":8,"max":32,"step":4,"label":"Products per page","default":16}]}{% endschema %} diff --git a/src/theme/sections/global/header.liquid b/src/theme/sections/global/header.liquid index f0a2e93..71ff71e 100644 --- a/src/theme/sections/global/header.liquid +++ b/src/theme/sections/global/header.liquid @@ -2,63 +2,167 @@ {% stylesheet %} -.site-header { position: sticky; top: 0; z-index: 40; border-bottom: 1px solid color-mix(in srgb, var(--color-border) 75%, transparent); background: color-mix(in srgb, var(--color-background) 88%, transparent); backdrop-filter: blur(18px); } -.site-header__inner { display: grid; min-height: 4.8rem; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 1rem; } -.site-header__brand { justify-self: center; font-size: 1.1rem; font-weight: 800; letter-spacing: -0.03em; text-decoration: none; } -.site-header__logo { width: auto; max-height: 2.3rem; } -.site-header__nav, .site-header__actions { display: flex; align-items: center; gap: 1.25rem; } -.site-header__nav a, .site-header__actions a { font-size: 0.9rem; font-weight: 650; text-decoration: none; } -.site-header__actions { justify-self: end; } -.site-header__menu-button { display: none; border: 0; background: transparent; cursor: pointer; padding: 0.5rem; } -.mobile-menu { border-top: 1px solid var(--color-border); background: var(--color-background); padding: 1rem; } -.mobile-menu[hidden] { display: none; } -.mobile-menu a { display: block; padding: 0.8rem; font-size: 1.15rem; font-weight: 700; text-decoration: none; } -shopify-account { --shopify-account-signed-in-avatar-size: 2rem; --shopify-account-signed-in-avatar-color-background: var(--color-accent); --shopify-account-signed-in-avatar-color-text: var(--color-accent-contrast); } -@media (max-width: 900px) { .site-header__nav { display: none; } .site-header__menu-button { display: inline-flex; } } -@media (max-width: 600px) { .site-header__actions > a:first-child { display: none; } } + .site-header { + position: sticky; + top: 0; + z-index: 40; + border-bottom: 1px solid color-mix(in srgb, var(--color-border) 75%, transparent); + background: color-mix(in srgb, var(--color-background) 88%, transparent); + backdrop-filter: blur(18px); + } + .site-header__inner { + display: grid; + min-height: 4.8rem; + grid-template-columns: 1fr auto 1fr; + align-items: center; + gap: 1rem; + } + .site-header__brand { + justify-self: center; + font-size: 1.1rem; + font-weight: 800; + letter-spacing: -0.03em; + text-decoration: none; + } + .site-header__logo { + width: auto; + max-height: 2.3rem; + } + .site-header__nav, + .site-header__actions { + display: flex; + align-items: center; + gap: 1.25rem; + } + .site-header__nav a, + .site-header__actions a { + font-size: 0.9rem; + font-weight: 650; + text-decoration: none; + } + .site-header__actions { + justify-self: end; + } + .site-header__menu-button { + display: none; + border: 0; + background: transparent; + cursor: pointer; + padding: 0.5rem; + } + .mobile-menu { + border-top: 1px solid var(--color-border); + background: var(--color-background); + padding: 1rem; + } + .mobile-menu[hidden] { + display: none; + } + .mobile-menu a { + display: block; + padding: 0.8rem; + font-size: 1.15rem; + font-weight: 700; + text-decoration: none; + } + shopify-account { + --shopify-account-signed-in-avatar-size: 2rem; + --shopify-account-signed-in-avatar-color-background: var(--color-accent); + --shopify-account-signed-in-avatar-color-text: var(--color-accent-contrast); + } + @media (max-width: 900px) { + .site-header__nav { + display: none; + } + .site-header__menu-button { + display: inline-flex; + } + } + @media (max-width: 600px) { + .site-header__actions > a:first-child { + display: none; + } + } {% endstylesheet %} {% javascript %} -class MenuDisclosure extends HTMLElement { - connectedCallback() { - this.button = this.querySelector("button"); - this.panel = this.querySelector("[data-menu-panel]"); - if (!this.button || !this.panel) return; - this.button.addEventListener("click", () => this.toggle()); - this.addEventListener("keydown", (event) => { - if (event.key === "Escape" && this.isOpen()) { this.close(); this.button.focus(); } - }); + class MenuDisclosure extends HTMLElement { + connectedCallback() { + this.button = this.querySelector('button'); + this.panel = this.querySelector('[data-menu-panel]'); + if (!this.button || !this.panel) return; + this.button.addEventListener('click', () => this.toggle()); + this.addEventListener('keydown', (event) => { + if (event.key === 'Escape' && this.isOpen()) { + this.close(); + this.button.focus(); + } + }); + } + isOpen() { + return this.button.getAttribute('aria-expanded') === 'true'; + } + toggle() { + this.isOpen() ? this.close() : this.open(); + } + open() { + this.button.setAttribute('aria-expanded', 'true'); + this.panel.removeAttribute('hidden'); + } + close() { + this.button.setAttribute('aria-expanded', 'false'); + this.panel.setAttribute('hidden', ''); + } } - isOpen() { return this.button.getAttribute("aria-expanded") === "true"; } - toggle() { this.isOpen() ? this.close() : this.open(); } - open() { this.button.setAttribute("aria-expanded", "true"); this.panel.removeAttribute("hidden"); } - close() { this.button.setAttribute("aria-expanded", "false"); this.panel.setAttribute("hidden", ""); } -} -if (!customElements.get("menu-disclosure")) customElements.define("menu-disclosure", MenuDisclosure); + if (!customElements.get('menu-disclosure')) customElements.define('menu-disclosure', MenuDisclosure); {% endjavascript %} {% schema %} @@ -67,7 +171,12 @@ if (!customElements.get("menu-disclosure")) customElements.define("menu-disclosu "settings": [ { "type": "image_picker", "id": "logo", "label": "Logo" }, { "type": "link_list", "id": "menu", "label": "Menu", "default": "main-menu" }, - { "type": "link_list", "id": "customer_account_menu", "label": "Customer account menu", "default": "customer-account-main-menu" } + { + "type": "link_list", + "id": "customer_account_menu", + "label": "Customer account menu", + "default": "customer-account-main-menu" + } ] } {% endschema %} diff --git a/src/theme/sections/home/featured-collection.liquid b/src/theme/sections/home/featured-collection.liquid index 6b8a2c8..51f12c4 100644 --- a/src/theme/sections/home/featured-collection.liquid +++ b/src/theme/sections/home/featured-collection.liquid @@ -1,3 +1,55 @@ {%- assign featured_collection = section.settings.collection -%} -

{{ section.settings.eyebrow | escape }}

{{ section.settings.heading | escape }}

{%- if featured_collection != blank -%}{{ 'actions.view_all' | t }}{%- endif -%}
{%- if featured_collection != blank -%}{%- for product in featured_collection.products limit: section.settings.products_to_show -%}{%- endfor -%}{%- else -%}{%- for index in (1..4) -%}
{{ 'product-' | append: index | placeholder_svg_tag }}

Example product

{{ 2400 | money }}
{%- endfor -%}{%- endif -%}
+
+
+
+
+

{{ section.settings.eyebrow | escape }}

+

{{ section.settings.heading | escape }}

+
+ {%- if featured_collection != blank -%} + {{ 'actions.view_all' | t }} + {%- endif -%} +
+
+ {%- if featured_collection != blank -%} + {%- for product in featured_collection.products limit: section.settings.products_to_show -%} + + + + {%- endfor -%} + {%- else -%} + {%- for index in (1..4) -%} +
+
{{ 'product-' | append: index | placeholder_svg_tag }}
+
+

Example product

+ {{ 2400 | money }} +
+
+ {%- endfor -%} + {%- endif -%} +
+
+
{% schema %}{"name":"Featured collection","settings":[{"type":"text","id":"eyebrow","label":"Eyebrow","default":"Selected for you"},{"type":"text","id":"heading","label":"Heading","default":"Products worth a closer look"},{"type":"collection","id":"collection","label":"Collection"},{"type":"range","id":"products_to_show","min":2,"max":8,"step":1,"label":"Products","default":4}],"presets":[{"name":"Featured collection"}]}{% endschema %} diff --git a/src/theme/sections/products/main-product.liquid b/src/theme/sections/products/main-product.liquid index b15bf60..c82a3d5 100644 --- a/src/theme/sections/products/main-product.liquid +++ b/src/theme/sections/products/main-product.liquid @@ -1,10 +1,14 @@ - +
{% content_for 'blocks' %}
@@ -12,13 +16,39 @@ {% stylesheet %} -.product { display: grid; grid-template-columns: minmax(0, 1.1fr) minmax(320px, 0.9fr); gap: clamp(2rem, 7vw, 7rem); } -.product__gallery { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1rem; } -.product__gallery img { width: 100%; border-radius: var(--radius-md); } -.product__details { display: grid; align-self: start; gap: 1rem; } -.product__details .price { display: block; font-size: 1.2rem; } -@media (max-width: 900px) { .product { grid-template-columns: 1fr; } } -@media (max-width: 600px) { .product__gallery { grid-template-columns: 1fr; } } + .product { + display: grid; + grid-template-columns: minmax(0, 1.1fr) minmax(320px, 0.9fr); + gap: clamp(2rem, 7vw, 7rem); + } + .product__gallery { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1rem; + } + .product__gallery img { + width: 100%; + border-radius: var(--radius-md); + } + .product__details { + display: grid; + align-self: start; + gap: 1rem; + } + .product__details .price { + display: block; + font-size: 1.2rem; + } + @media (max-width: 900px) { + .product { + grid-template-columns: 1fr; + } + } + @media (max-width: 600px) { + .product__gallery { + grid-template-columns: 1fr; + } + } {% endstylesheet %} {% schema %} diff --git a/src/theme/sections/search/main-search.liquid b/src/theme/sections/search/main-search.liquid index 261fd91..6796867 100644 --- a/src/theme/sections/search/main-search.liquid +++ b/src/theme/sections/search/main-search.liquid @@ -1,46 +1,128 @@
-

Search

Find what you need

+

Search

+

Find what you need

-
+
+
+ +
+ +
- {%- if search.performed -%}
{%- for item in search.results -%}{%- else -%}

No results for “{{ search.terms | escape }}”.

{%- endfor -%}
{%- endif -%} + {%- if search.performed -%} +
+ {%- for item in search.results -%} + + {%- else -%} +

No results for “{{ search.terms | escape }}”.

+ {%- endfor -%} +
+ {%- endif -%}
{% stylesheet %} -.search-field { display: grid; gap: 0.45rem; margin-block: 1rem; } -.search-field label { font-size: 0.85rem; font-weight: 700; } -.search-field input { width: 100%; min-height: 3rem; border: 1px solid var(--color-border); border-radius: var(--radius-sm); background: var(--color-surface); padding: 0.7rem 0.85rem; } -.predictive-search__results { border: 1px solid var(--color-border); border-radius: var(--radius-md); margin-top: 0.5rem; background: var(--color-surface); box-shadow: var(--shadow-soft); padding: 1rem; } -.predictive-search__results ul { display: grid; gap: 0.65rem; margin: 0; padding: 0; list-style: none; } -.predictive-search__results a { display: flex; justify-content: space-between; gap: 1rem; text-decoration: none; } + .search-field { + display: grid; + gap: 0.45rem; + margin-block: 1rem; + } + .search-field label { + font-size: 0.85rem; + font-weight: 700; + } + .search-field input { + width: 100%; + min-height: 3rem; + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + background: var(--color-surface); + padding: 0.7rem 0.85rem; + } + .predictive-search__results { + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + margin-top: 0.5rem; + background: var(--color-surface); + box-shadow: var(--shadow-soft); + padding: 1rem; + } + .predictive-search__results ul { + display: grid; + gap: 0.65rem; + margin: 0; + padding: 0; + list-style: none; + } + .predictive-search__results a { + display: flex; + justify-content: space-between; + gap: 1rem; + text-decoration: none; + } {% endstylesheet %} {% javascript %} -class PredictiveSearch extends HTMLElement { - connectedCallback() { - this.input = this.querySelector("input[type='search']"); this.results = this.querySelector("[data-predictive-search-results]"); - if (!this.input || !this.results) return; - this.input.addEventListener("input", () => { clearTimeout(this.timer); this.timer = setTimeout(() => this.search(), 180); }); - } - async search() { - const query = this.input.value.trim(); this.controller?.abort(); - if (query.length < 2) { this.results.replaceChildren(); this.input.setAttribute("aria-expanded", "false"); return; } - this.controller = new AbortController(); - try { - const response = await fetch(`/search/suggest?q=${encodeURIComponent(query)}&resources[type]=product§ion_id=predictive-search`, { signal: this.controller.signal }); - if (!response.ok) throw new Error(`Search request failed: ${response.status}`); - const documentFragment = new DOMParser().parseFromString(await response.text(), "text/html"); - const content = documentFragment.querySelector(".predictive-search__results"); - this.results.replaceChildren(...(content ? [content] : [])); this.input.setAttribute("aria-expanded", content ? "true" : "false"); - } catch (error) { if (error.name !== "AbortError") console.error(error); } + class PredictiveSearch extends HTMLElement { + connectedCallback() { + this.input = this.querySelector("input[type='search']"); + this.results = this.querySelector('[data-predictive-search-results]'); + if (!this.input || !this.results) return; + this.input.addEventListener('input', () => { + clearTimeout(this.timer); + this.timer = setTimeout(() => this.search(), 180); + }); + } + async search() { + const query = this.input.value.trim(); + this.controller?.abort(); + if (query.length < 2) { + this.results.replaceChildren(); + this.input.setAttribute('aria-expanded', 'false'); + return; + } + this.controller = new AbortController(); + try { + const response = await fetch( + `/search/suggest?q=${encodeURIComponent(query)}&resources[type]=product§ion_id=predictive-search`, + { signal: this.controller.signal } + ); + if (!response.ok) throw new Error(`Search request failed: ${response.status}`); + const documentFragment = new DOMParser().parseFromString(await response.text(), 'text/html'); + const content = documentFragment.querySelector('.predictive-search__results'); + this.results.replaceChildren(...(content ? [content] : [])); + this.input.setAttribute('aria-expanded', content ? 'true' : 'false'); + } catch (error) { + if (error.name !== 'AbortError') console.error(error); + } + } } -} -if (!customElements.get("predictive-search")) customElements.define("predictive-search", PredictiveSearch); + if (!customElements.get('predictive-search')) customElements.define('predictive-search', PredictiveSearch); {% endjavascript %} {% schema %}{"name":"Main search","settings":[]}{% endschema %} diff --git a/tests/template-sync.test.js b/tests/template-sync.test.js index 4154824..2901a5b 100644 --- a/tests/template-sync.test.js +++ b/tests/template-sync.test.js @@ -24,7 +24,10 @@ describe("embedded starter", () => { const embeddedFiles = await listFiles(embeddedSource); assert.deepEqual(embeddedFiles, referenceFiles); for (const file of referenceFiles) { - const [reference, embedded] = await Promise.all([readFile(path.join(referenceSource, ...file.split("/"))), readFile(path.join(embeddedSource, ...file.split("/")))]); + const [reference, embedded] = await Promise.all([ + readFile(path.join(referenceSource, ...file.split("/"))), + readFile(path.join(embeddedSource, ...file.split("/"))) + ]); assert.deepEqual(embedded, reference, `Embedded starter drifted at ${file}`); } }); diff --git a/tests/theme-capabilities.test.js b/tests/theme-capabilities.test.js index ec9fb65..22717d0 100644 --- a/tests/theme-capabilities.test.js +++ b/tests/theme-capabilities.test.js @@ -8,7 +8,17 @@ const read = (relativePath) => readFile(path.join(root, ...relativePath.split("/ describe("reference theme capabilities", () => { it("ships composable Shopify theme blocks and keeps block validation enabled", async () => { - for (const block of ["button", "heading", "image", "text", "product-vendor", "product-title", "product-price", "product-purchase", "product-description"]) { + for (const block of [ + "button", + "heading", + "image", + "text", + "product-vendor", + "product-title", + "product-price", + "product-purchase", + "product-description" + ]) { assert.match(await read(`src/theme/blocks/${block}.liquid`), /block\.shopify_attributes/); } assert.match(await read("src/theme/sections/pages/flexible-content.liquid"), /content_for 'blocks'/); From 768eb546c4b0dea188dbb185cbfedb7780a82ae2 Mon Sep 17 00:00:00 2001 From: Yotam Faraggi Date: Tue, 15 Sep 2026 02:21:59 +0200 Subject: [PATCH 08/16] docs: align README with Shopify-native defaults --- README.md | 373 ++++++++++++++++++++++++++---------------------------- 1 file changed, 179 insertions(+), 194 deletions(-) diff --git a/README.md b/README.md index f09408c..77a0fee 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Liquid Loom

-

A deterministic engineering layer for Shopify themes, from legacy adoption to agent-readable architecture.

+

A deterministic engineering layer for Shopify Liquid themes, from legacy adoption to agent-readable architecture.

npm version @@ -15,9 +15,9 @@ Liquid Loom gives Shopify theme development a safer, source-first engineering layer while keeping Shopify's runtime completely standard. -Use it to organize new code by feature, add modern tooling to an existing client theme without a rewrite, migrate gradually when it is worth it, understand source ownership before an agent edits the project, and still ship a normal Shopify theme through Shopify CLI. +Use it to organize theme code by feature, adopt better tooling inside an existing client theme without a rewrite, migrate source gradually, inspect project ownership before a developer or coding agent edits it, and still ship a conventional Shopify theme through Shopify CLI. -It does **not** replace Liquid, Online Store 2.0, Shopify CLI, or Shopify's runtime model. Vite and Tailwind are first-class integrations for fresh projects, not abstractions that Liquid Loom depends on for its identity. +Liquid Loom does **not** replace Liquid, Online Store 2.0, Shopify CLI, the Theme Editor, or Shopify's storefront runtime. ## Start with the theme you already have @@ -29,7 +29,7 @@ npx liquid-loom@latest init Liquid Loom shows the complete setup plan before writing anything. Existing Shopify source is not moved. -After setup, keep the current theme exactly as it is: +You can keep the theme exactly as it is: ```text assets/ @@ -39,19 +39,19 @@ snippets/ templates/ ``` -and put only new work into organized source when you want to: +and put only new work into organized source when that helps: ```text src/theme/sections/product/upsell.liquid src/theme/snippets/product/upsell-price.liquid ``` -Both become one normal deployable theme: +Both source layers become one normal deployable theme: ```text existing Shopify source ----+ | -organized Liquid Loom source +--> deterministic ownership plan --> dist/theme --> Shopify CLI +organized Liquid Loom source +--> ownership plan --> dist/theme --> Shopify CLI ``` Build with the namespaced script added by `init`: @@ -60,9 +60,9 @@ Build with the namespaced script added by `init`: npm run loom:build ``` -Use your preferred package manager. `init` detects npm, pnpm, Yarn, or Bun and supports `--package-manager`, `--no-install`, `--dry-run`, `--theme-dir`, and `--yes`. +`init` detects npm, pnpm, Yarn, or Bun and supports `--package-manager`, `--no-install`, `--dry-run`, `--theme-dir`, and `--yes`. -Read the full [existing-theme adoption guide](docs/EXISTING_THEME_ADOPTION.md). +Read [Existing-theme adoption](docs/EXISTING_THEME_ADOPTION.md) for the full zero-migration workflow. ## Or start a new theme @@ -71,101 +71,136 @@ corepack enable pnpm create liquid-loom@latest my-storefront cd my-storefront pnpm build -``` - -Then preview against a Shopify development store: - -```bash pnpm dev ``` -The scaffolder supports `--package-manager npm|pnpm|yarn|bun` and `--no-install`. +A fresh project uses Shopify-native component CSS and JavaScript by default. Vite and Tailwind remain supported integrations when a project actually needs bundling, module graphs, or utility-first CSS. ## Why Liquid Loom -Liquid Loom targets the parts of custom theme development that become painful as projects, teams, and coding agents grow: +Liquid Loom targets the engineering problems that become painful as custom themes, teams, and coding agents grow: - **Adopt incrementally.** Existing themes do not need an up-front rewrite. - **Feature-oriented source.** Organize authored Liquid by feature without inventing a new runtime. - **Dual-source builds.** Native Shopify source and organized Liquid Loom source can coexist safely. -- **No silent precedence.** Every Shopify output path has exactly one owner. -- **No silent overwrites.** Destination collisions fail before a new build is published. -- **Last-known-good output.** Failed builds do not replace the previous working theme. -- **Transactional migration.** Preview first, preserve Shopify output identity, and roll back completed moves if a later move fails. +- **One output, one owner.** No silent overlay or last-write-wins behavior. +- **Transactional builds.** Failed builds never replace the last-known-good theme. +- **Safe migration.** Preview ownership transfers before moving source and preserve Shopify output identity. - **Project intelligence.** `explain` turns ownership and provable static relationships into deterministic human- and machine-readable data. -- **Preview-aware, not preview-dependent.** Shopify developer-preview Liquid is explicit and observable instead of silently becoming a production requirement. -- **Optional Vite + Tailwind.** Fresh projects get the modern asset workflow; existing themes can keep their current pipeline. -- **Portable behavior.** Collision checks are case-insensitive and Unicode-normalized across filesystems. +- **Shopify-native defaults.** Theme blocks, app blocks, component asset tags, color palettes, customer accounts, and standard storefront interoperability stay visible as Shopify concepts. +- **Optional advanced assets.** Vite and Tailwind remain available without defining the framework's architecture. +- **Portable behavior.** Destination collisions are case-insensitive and Unicode-normalized across filesystems. - **Useful diagnostics.** `doctor`, `check`, `analyze`, Theme Check, and performance budgets make failures explainable. -- **Standard Shopify output.** `dist/theme` remains inspectable, pushable, and debuggable with Shopify's own tooling. +- **Standard output.** `dist/theme` remains inspectable, pushable, and debuggable with Shopify's own tools. -## Understand the theme before editing it +## Shopify-native fresh-project architecture -Liquid Loom can describe the source model without requiring a build first: +The reference storefront now demonstrates Shopify's 2026-native theme model instead of loading one application bundle for ordinary theme behavior. -```bash -liquid-loom explain -liquid-loom explain product -``` - -Coding agents and CI can request deterministic JSON: +A fresh source tree can look like: -```bash -liquid-loom explain --json -liquid-loom explain product --json +```text +src/ +├── public/ +│ └── theme.css +└── theme/ + ├── blocks/ + │ ├── product-title.liquid + │ ├── product-price.liquid + │ └── product-purchase.liquid + ├── sections/ + │ ├── products/main-product.liquid + │ └── global/header.liquid + ├── snippets/ + ├── templates/ + └── layout/theme.liquid ``` -The model reports source ownership, semantic feature groups, static snippet/section/block/asset relationships, JSON-template section references, preview partial regions, preview-tag usage, and unresolved static references. +Component-local behavior lives with the Liquid component when practical: -It does not evaluate Liquid or guess dynamic dependencies. The goal is a smaller truthful architecture model, not a second Liquid interpreter. +```liquid +{% stylesheet %} +.product-purchase { ... } +{% endstylesheet %} -Read [Project model](docs/PROJECT_MODEL.md) for the complete contract and recommended agent workflow. +{% javascript %} +customElements.define(...) +{% endjavascript %} +``` -## The source model +Shopify can then process component assets through its own render-aware pipeline. Global CSS is reserved for genuinely global concerns such as base styles, design tokens, accessibility defaults, and shared layout primitives. -A fresh Liquid Loom project can look like this: +The output stays conventional: ```text -src/theme/sections/home/hero.liquid -src/theme/sections/product/recommendations.liquid -src/theme/snippets/product/price.liquid -src/entrypoints/theme.js -src/styles/theme.css +dist/theme/ +├── assets/theme.css +├── blocks/product-title.liquid +├── blocks/product-price.liquid +├── blocks/product-purchase.liquid +├── sections/main-product.liquid +├── snippets/ +├── templates/product.json +└── layout/theme.liquid ``` -Liquid Loom produces: +## Theme-block-first composition + +The reference product page is composed from Shopify theme blocks rather than hard-coding all product information into one monolithic section. ```text -dist/theme/sections/hero.liquid -dist/theme/sections/recommendations.liquid -dist/theme/snippets/price.liquid -dist/theme/assets/theme.js -dist/theme/assets/style.css +Main product +├── Product media +└── Merchant-reorderable blocks + ├── Vendor + ├── Title + ├── Price + ├── Purchase controls + ├── Description + ├── @theme + └── @app ``` -An adopted project may simultaneously contain: +This keeps customization inside Shopify's Theme Editor and makes the structure understandable to merchants, developers, apps, and coding agents without adding a Liquid Loom component runtime. -```text -sections/header.liquid -assets/legacy-theme.css -src/theme/sections/product/upsell.liquid +## Storefront interoperability + +The reference storefront adopts Shopify's standard storefront events and actions where the theme owns the interaction. + +Examples include: + +- standardized product, collection, recommendation, and cart view events through `s-view-event` and `standard_event_data`; +- cart mutation through `Shopify.actions.updateCart()` with the native product form retained as the no-JavaScript fallback; +- merchant-configurable `` integration in the header; +- `color_palette` as the source for semantic CSS design tokens. + +The goal is interoperability with Shopify, apps, and agents, not a Liquid Loom-specific browser protocol. + +## Understand the theme before editing it + +Liquid Loom can describe the project without evaluating Liquid or requiring a build first: + +```bash +liquid-loom explain +liquid-loom explain product ``` -and produce: +Coding agents and CI can request deterministic JSON: -```text -dist/theme/sections/header.liquid -dist/theme/sections/upsell.liquid -dist/theme/assets/legacy-theme.css +```bash +liquid-loom explain --json +liquid-loom explain product --json ``` -Shopify still receives the conventional structure it expects. +The model reports source ownership, semantic feature groups, static snippet/section/block/asset relationships, JSON-template section references, preview partial regions, preview-tag usage, and unresolved static references. + +It intentionally does not guess dynamic Liquid relationships. Read [Project model](docs/PROJECT_MODEL.md) for the complete contract. ## One output, one owner There is intentionally no "new source wins" rule. -This fails: +This is invalid: ```text sections/hero.liquid @@ -178,13 +213,13 @@ because both map to: sections/hero.liquid ``` -Liquid Loom validates the global ownership plan before changing the last-known-good output and tells you which sources collide. +Liquid Loom plans the complete ownership map before publishing a build and reports the conflicting owners instead of silently overwriting one of them. -The same ownership model includes generated asset reservations when Vite is enabled. +Generated Vite outputs participate in the same ownership map when Vite is enabled. ## Migrate only when it helps -Preview migration of one file: +Preview a migration: ```bash liquid-loom migrate sections/hero.liquid @@ -196,7 +231,7 @@ Apply it: liquid-loom migrate sections/hero.liquid --apply ``` -Organize it during migration: +Organize it while preserving its Shopify identity: ```bash liquid-loom migrate sections/hero.liquid \ @@ -204,141 +239,85 @@ liquid-loom migrate sections/hero.liquid \ --apply ``` -The Shopify output must remain `sections/hero.liquid`. If the requested destination would change the deployable path, Liquid Loom refuses the migration. +The deployable path must remain `sections/hero.liquid`. Liquid Loom rejects a migration that would silently rename Shopify output. -Migrate a directory: +Bulk migration remains optional: ```bash liquid-loom migrate sections --apply -``` - -Or preview/apply all remaining native theme source: - -```bash liquid-loom migrate --all liquid-loom migrate --all --apply ``` -Liquid Loom never invents your feature taxonomy during bulk migration. +Liquid Loom never invents a feature taxonomy for existing source. -## Existing asset pipelines are allowed +## Asset strategies -Existing-theme initialization defaults to: +### Shopify-native default -```js -import { defineConfig } from "liquid-loom"; +Fresh projects default to `viteConfig: false` and use static global assets plus `{% stylesheet %}` / `{% javascript %}` for component-local code. -export default defineConfig({ - shopifySourceDir: ".", - sourceDir: "src", - outputDir: "dist/theme", - viteConfig: false, - performance: false -}); -``` - -That means native `assets/*` pass through unchanged and your existing Sass/PostCSS/Webpack/Vite process can keep running independently. - -Fresh projects still use Liquid Loom's Vite + Tailwind workflow by default. Tailwind remains an integration choice rather than part of the source-ownership contract. - -If `src/entrypoints` exists while Vite is disabled, `doctor` warns that those files are not being compiled. +### Existing pipeline -## Shopify mapping contract - -| Authoring source | Deployable Shopify path | Rule | -| ----------------------------------------------- | -------------------------------------- | ------------------------------------- | -| `sections/hero.liquid` | `sections/hero.liquid` | Native Shopify identity mapping | -| `src/theme/sections/home/hero.liquid` | `sections/hero.liquid` | Organized source flattens by filename | -| `src/theme/snippets/product/price.liquid` | `snippets/price.liquid` | Organized source flattens by filename | -| `src/theme/config/editor/settings_schema.json` | `config/settings_schema.json` | Flatten by filename | -| `src/theme/locales/markets/en.default.json` | `locales/en.default.json` | Flatten by filename | -| `src/theme/templates/catalog/product.json` | `templates/product.json` | Feature folders flatten | -| `templates/customers/account.json` | `templates/customers/account.json` | Preserve Shopify-supported nesting | -| `src/theme/templates/metaobject/book.json` | `templates/metaobject/book.json` | Preserve Shopify-supported nesting | -| `src/public/icons/cart.svg` | `assets/cart.svg` | Flatten passthrough assets | -| `src/entrypoints/theme.js` + `src/styles/*.css` | `assets/theme.js` + `assets/style.css` | Vite-generated outputs when enabled | - -Framework validation checks Shopify's upload minimum from the final ownership plan: `layout/theme.liquid` must exist in deployable output, regardless of which source layer owns it. - -For Shopify's canonical directory rules, see the official [theme architecture documentation](https://shopify.dev/docs/storefronts/themes/architecture). - -## Build guarantees +An adopted theme can keep Sass, PostCSS, Webpack, Vite, Tailwind, or another existing pipeline unchanged. Native `assets/*` pass through as Shopify assets. -### Complete planning before publish +### Optional Vite and Tailwind -Source discovery, Shopify path validation, traversal guards, collision detection, and generated-output reservations happen before a new output is promoted. +Projects that need bundling can explicitly enable Vite and reserve stable generated Shopify asset names. Tailwind remains available through the native Tailwind Vite plugin. -### Last-known-good output +The framework still validates static and generated outputs in one collision map. -Static mapping and optional Vite bundling happen in isolated staging paths. Output and cache are promoted together only after the build succeeds. +See [Recipes](docs/RECIPES.md) for setup examples. -### Safe concurrent builds +## Shopify GitHub deployment -Independent CLI processes serialize through a recoverable project lock. An abandoned lock from a terminated process is detected and removed. +A Liquid Loom source branch contains development files that Shopify's GitHub theme integration does not treat as the deployable theme root. -### Migration-safe caching - -Hybrid manifests track source-layer identity. Moving ownership from: - -```text -sections/hero.liquid -``` - -to: +For GitHub-connected themes, use a generated `shopify-production` branch or a separate deployment repository containing only the canonical Shopify theme directories: ```text -src/theme/sections/home/hero.liquid +main + | + | validate + build + v +shopify-production + | + v +Shopify GitHub integration ``` -does not cause `sections/hero.liquid` to be removed as stale. - -### Consumer-shaped release validation +The repository includes an opt-in deployment recipe that publishes compiled output without force-pushing and avoids deployment loops from Shopify-originated commits. -CI packs the actual npm artifacts, installs both CLIs into independent temporary projects, validates `explain` from the packed package, validates a fresh scaffold, and validates an existing-theme `init -> hybrid build -> migrate -> build` flow. +Read [Shopify GitHub deployment](docs/GITHUB_DEPLOYMENT.md). -Releases use npm Trusted Publishing with GitHub OIDC and signed provenance. +## Shopify Liquid preview mode -## Configuration +Shopify's July 2026 `{% block %}` / `{% partial %}` developer preview is never enabled implicitly. -Fresh project example: +Projects intentionally targeting it can declare: ```ts -import { defineConfig } from "liquid-loom"; - export default defineConfig({ - sourceDir: "src", - outputDir: "dist/theme", - performance: { - maxBuildMs: 10_000, - maxThemeBytes: 5_000_000, - maxAssetBytes: 500_000 - } + shopifyLiquidMode: "july-2026-preview" }); ``` -Existing theme in a monorepo: +This records project intent for diagnostics and project intelligence. It does not enable the preview on a Shopify store. -```ts -export default defineConfig({ - shopifySourceDir: "theme", - sourceDir: "src", - outputDir: "dist/theme", - viteConfig: false, - performance: false -}); -``` +Read [Shopify Liquid July 2026 developer preview](docs/LIQUID_JULY_2026_PREVIEW.md). -All paths remain project-relative and portable. +## Build guarantees -Shopify's July 2026 Liquid developer-preview syntax is never enabled implicitly. Projects intentionally targeting that preview can declare: +Liquid Loom keeps the existing engineering guarantees regardless of asset strategy: -```ts -export default defineConfig({ - shopifyLiquidMode: "july-2026-preview" -}); -``` +1. discover all managed source; +2. map the complete Shopify ownership plan; +3. validate paths, collisions, and Shopify's upload minimum; +4. build in isolated staging; +5. optionally run Vite and performance budgets; +6. atomically promote output and cache together. -This only declares project intent to Liquid Loom; it does not enable the preview on a Shopify development store. See [Shopify Liquid July 2026 developer preview](docs/LIQUID_JULY_2026_PREVIEW.md). +A failed build leaves the previous deployable output intact. Independent CLI processes serialize through a recoverable project lock. ## Commands @@ -358,58 +337,64 @@ This only declares project intent to Liquid Loom; it does not enable the preview Scaffolded projects expose package-manager scripts for the same commands. Existing-theme `init` uses namespaced `loom:*` scripts so it does not replace a project's existing `build` or `dev` commands. +## Reference storefront + +The fresh-project scaffold is a merchant-neutral reference theme that demonstrates: + +- Shopify theme blocks and app blocks; +- merchant-reorderable product information and purchase controls; +- Shopify-native component CSS and JavaScript; +- standard storefront view events and cart actions; +- `color_palette` design tokens; +- `` customer-account integration; +- JSON templates and editable header/footer section groups; +- storefront filtering and predictive search; +- variant URL state, selling plans, quantities, and progressive enhancement; +- responsive images, semantic navigation, skip links, visible focus states, reduced-motion support, and no-JavaScript fallbacks. + +It is a reference implementation. The product is Liquid Loom's engineering layer and workflow. + ## Who it is for Liquid Loom is a good fit when you: -- maintain a custom or client Shopify theme and want better tooling without rewriting it; +- maintain a custom or client Shopify theme and want stronger tooling without rewriting it; - want new theme work organized by feature rather than only by Shopify's flat directories; -- want deterministic build ownership and explicit collision behavior; -- want coding agents to query a conservative architectural model before editing source; -- expect modern frontend build and CI guarantees from the rest of your stack. +- need deterministic ownership and collision behavior; +- want coding agents to inspect a conservative architectural model before editing source; +- want Shopify-native theme capabilities without giving up modern engineering guarantees. It is probably not the right starting point when: - the theme is tiny and needs no build pipeline; - you are building a headless storefront rather than a Shopify Liquid theme; -- your primary goal is Shopify Theme Store submission, where Shopify's official starting points and policies should remain authoritative. - -## Reference storefront - -The fresh-project scaffolder includes a merchant-neutral Online Store 2.0 reference theme with theme blocks, JSON templates, editable header/footer groups, storefront filtering, predictive search, variant URL state, selling plans, responsive images, semantic navigation, reduced-motion support, visible focus states, and no-JavaScript fallbacks. - -It is a reference implementation. The product is the tooling and workflow. - -## Validation and feedback - -Liquid Loom is being validated against real Shopify workflows, not just repository metrics. +- your primary goal is Shopify Theme Store submission, where Shopify's official starting points and policies remain authoritative. -Useful evidence includes: +## Validation -- a successful public-package install; -- a real Shopify development-store preview; -- existing-theme adoption without source migration; -- incremental migration on a real client-style repository; -- coding-agent use of the deterministic project model; -- developers independently choosing the workflow again. +Protected CI validates: -If you try it, feedback about what is confusing, slower, or worse is especially useful. +- unit and integration tests with coverage thresholds; +- transactional clean builds; +- zero-offense Shopify Theme Check; +- Linux/Node 22, Windows/Node 24, and macOS/Node 24 portability; +- packed-package installation and fresh-scaffold smoke tests; +- Dependency Review and CodeQL; +- a non-blocking Node 26 canary. -- [Run the first-project test](docs/FIRST_PROJECT.md) -- [Read the existing-theme adoption guide](docs/EXISTING_THEME_ADOPTION.md) -- [Share first-project feedback](https://github.com/yotamon/Liquid-Loom/issues/new?template=early_adopter_feedback.yml) -- [Report a bug](https://github.com/yotamon/Liquid-Loom/issues/new?template=bug_report.yml) -- [Start a discussion](https://github.com/yotamon/Liquid-Loom/discussions) +Real-store validation remains separate from public CI so contributors do not need Shopify credentials. ## Documentation +- [Shopify 2026 platform modernization](docs/SHOPIFY_2026_MODERNIZATION.md) - architecture decisions and acceptance criteria - [Existing-theme adoption](docs/EXISTING_THEME_ADOPTION.md) - zero-migration setup, hybrid source, and safe migration - [Project model](docs/PROJECT_MODEL.md) - deterministic architecture queries for developers, agents, and CI - [Shopify Liquid July 2026 developer preview](docs/LIQUID_JULY_2026_PREVIEW.md) - explicit preview-mode contract and safety boundaries +- [Shopify GitHub deployment](docs/GITHUB_DEPLOYMENT.md) - compiled deployment branches and write-back constraints - [First project](docs/FIRST_PROJECT.md) - short evaluation from public install to a real source edit - [Architecture](docs/ARCHITECTURE.md) - ownership, transactions, cache, and migration invariants - [Validation](docs/VALIDATION.md) - real-user and real-store validation plan -- [Recipes](docs/RECIPES.md) - entrypoints, static assets, private-term policies, and budgets +- [Recipes](docs/RECIPES.md) - native assets, optional bundling, private-term policies, and budgets - [Troubleshooting](docs/TROUBLESHOOTING.md) - collisions, locks, Shopify CLI, and build failures - [Benchmarks](docs/BENCHMARKS.md) - reproducible build measurements - [Releasing](docs/RELEASING.md) - provenance-backed npm release process @@ -421,12 +406,12 @@ If you try it, feedback about what is confusing, slower, or worse is especially - Adopt tooling before forcing migration. - Source ownership must be explicit. - Shopify's runtime contract stays visible. -- Integrate with Shopify primitives instead of duplicating them. +- Prefer Shopify primitives over competing abstractions. - Generated output is disposable; authored source is the product. - Prefer provable project intelligence over guessed relationships. - Fail early, explain specifically, and preserve the last-known-good build. - Treat Vite and Tailwind as replaceable integrations, not the framework's durable moat. -- Prefer a dependable core over speculative abstractions. +- Keep preview APIs explicit until Shopify stabilizes them. - Add framework surface only after real users demonstrate the need.

From 6514af277cfe43d0298f8d3b29e7a7fef64d6d56 Mon Sep 17 00:00:00 2001 From: Yotam Faraggi Date: Tue, 15 Sep 2026 02:22:14 +0200 Subject: [PATCH 09/16] chore: align package positioning with Shopify-native architecture --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 494bf81..678b360 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "liquid-loom", "version": "0.2.0", - "description": "A deterministic engineering layer for Shopify themes: source architecture, safe incremental adoption, Vite, and Tailwind CSS.", + "description": "A deterministic engineering layer for Shopify Liquid themes: safe adoption, explicit ownership, project intelligence, and modern platform defaults.", "type": "module", "license": "MIT", "bin": { From 63edc69b7ea9248cd1504296a79144c31a07b16e Mon Sep 17 00:00:00 2001 From: Yotam Faraggi Date: Tue, 15 Sep 2026 02:22:19 +0200 Subject: [PATCH 10/16] chore: remove obsolete default Vite config --- vite.config.js | 31 ------------------------------- 1 file changed, 31 deletions(-) delete mode 100644 vite.config.js diff --git a/vite.config.js b/vite.config.js deleted file mode 100644 index 724c668..0000000 --- a/vite.config.js +++ /dev/null @@ -1,31 +0,0 @@ -import path from "node:path"; -import { fileURLToPath } from "node:url"; - -import tailwindcss from "@tailwindcss/vite"; -import { defineConfig } from "vite"; - -const projectRoot = path.dirname(fileURLToPath(import.meta.url)); - -export default defineConfig(({ mode }) => ({ - base: "", - plugins: [tailwindcss()], - build: { - assetsDir: "", - cssCodeSplit: false, - emptyOutDir: false, - minify: mode === "production" ? "esbuild" : false, - outDir: path.join(projectRoot, "dist", "theme", "assets"), - rollupOptions: { - input: path.join(projectRoot, "src", "entrypoints", "theme.js"), - output: { - assetFileNames: "[name][extname]", - entryFileNames: "theme.js" - } - }, - sourcemap: mode !== "production", - target: "baseline-widely-available" - }, - css: { - devSourcemap: mode !== "production" - } -})); From 412fdb5651f5b9ff969462b6d4e049658b9d6015 Mon Sep 17 00:00:00 2001 From: Yotam Faraggi Date: Tue, 15 Sep 2026 02:23:46 +0200 Subject: [PATCH 11/16] docs: record Shopify 2026 modernization changes --- CHANGELOG.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8c543c..f69fe2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,28 @@ All notable changes follow [Keep a Changelog](https://keepachangelog.com/) and t ## [Unreleased] +### Added + +- Shopify-native component CSS and JavaScript in the reference storefront through `{% stylesheet %}` and `{% javascript %}` tags. +- Theme-block-first product composition with merchant-reorderable vendor, title, price, purchase, description, `@theme`, and `@app` blocks. +- Shopify standard storefront view events for product, collection, recommendation, and cart surfaces, plus cart mutation through `Shopify.actions.updateCart()` with the native product form retained as the no-JavaScript fallback. +- Shopify `color_palette` design tokens and `` customer-account integration in the fresh reference theme. +- A documented compiled-branch/deployment-repository workflow and opt-in GitHub Actions recipe for Shopify GitHub theme integration. +- A complete Shopify 2026 modernization plan covering architecture, compatibility constraints, implementation order, and real-store acceptance criteria. + +### Changed + +- Fresh projects now default to Shopify-native static/component assets with `viteConfig: false` instead of requiring a Vite/Tailwind storefront bundle. +- Vite and Tailwind remain supported framework integrations but are now documented as explicit optional asset strategies rather than the framework's product identity. +- The reference product, header, predictive search, collection, featured collection, and cart surfaces now demonstrate current Shopify-native platform primitives. +- The embedded `create-liquid-loom` starter stays byte-for-byte synchronized with the modernized reference storefront and targets Shopify CLI 4.8+ for new projects. +- README, recipes, and roadmap now describe Liquid Loom as a deterministic engineering layer for Shopify Liquid themes and sequence platform modernization after the `0.3` project-intelligence work. + +### Removed + +- The fresh starter's default global Vite entrypoint, Tailwind stylesheet source, and bundled `theme.js`/`style.css` architecture. +- The obsolete default Vite configuration from the reference project and embedded starter. Existing projects that explicitly enable Vite remain supported. + ## [0.2.0] - 2026-09-09 ### Added From a361418ab4d23858107f51a0f9531ad10a5b5765 Mon Sep 17 00:00:00 2001 From: Yotam Faraggi Date: Tue, 15 Sep 2026 02:24:03 +0200 Subject: [PATCH 12/16] docs: mark implemented Shopify modernization work --- ROADMAP.md | 38 ++++++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 315a907..c5b3987 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -77,20 +77,30 @@ See [Project model](docs/PROJECT_MODEL.md) and [Shopify Liquid July 2026 develop Modernize fresh-project defaults and the reference storefront around Shopify's current native theme capabilities without weakening the `0.2` ownership/migration guarantees or the `0.3` project-intelligence model. -Planned: - -- [ ] make Shopify-native `{% stylesheet %}` / `{% javascript %}` component assets the preferred fresh-project path where practical; -- [ ] keep Vite and Tailwind as explicit, tested optional asset strategies rather than Liquid Loom's product identity; -- [ ] make theme blocks and merchant-reorderable composition central to the reference storefront, especially product information and purchase surfaces; -- [ ] implement relevant Shopify standard storefront events/actions in the reference theme; -- [ ] move the reference color system to `color_palette` plus semantic CSS custom properties; -- [ ] adopt the `` component with a merchant-configurable account menu; -- [ ] add a documented/generated compiled branch or deployment-repository workflow for Shopify GitHub integration; -- [ ] align opt-in real-store testing with Shopify CLI 4.8+ dev-store commands; -- [ ] extend project intelligence only where new block/component/deployment relationships are statically provable; -- [ ] preserve explicit `stable` / `july-2026-preview` modes and keep preview syntax outside the stable release gate; -- [ ] preserve zero-migration adoption and existing asset pipelines for established themes; -- [ ] expand package, Theme Check, browser, Theme Editor, and deployment-loop validation around the new defaults. +Implemented in the modernization PR: + +- [x] make Shopify-native `{% stylesheet %}` / `{% javascript %}` component assets the preferred fresh-project path where practical; +- [x] keep Vite and Tailwind as explicit, tested optional asset strategies rather than Liquid Loom's product identity; +- [x] make theme blocks and merchant-reorderable composition central to the reference storefront, especially product information and purchase surfaces; +- [x] implement relevant Shopify standard storefront view events and cart actions in the reference theme; +- [x] move the reference color system to `color_palette` plus semantic CSS custom properties; +- [x] adopt the `` component with a merchant-configurable account menu; +- [x] add a documented compiled branch/deployment-repository workflow and opt-in GitHub Actions recipe for Shopify GitHub integration; +- [x] target Shopify CLI 4.8+ in fresh scaffolds and document disposable dev-store validation; +- [x] preserve explicit `stable` / `july-2026-preview` modes and keep preview syntax outside the stable release gate; +- [x] preserve zero-migration adoption and existing asset pipelines for established themes; +- [x] add automated capability tests and keep the reference storefront byte-for-byte synchronized with the embedded starter. + +Still requiring real Shopify evidence before calling the milestone complete: + +- [ ] preview the modernized fresh scaffold on a Shopify development store; +- [ ] verify product-block reordering and app-block placement in Theme Editor; +- [ ] verify `` with customer accounts enabled and disabled; +- [ ] verify palette editing through Theme Editor; +- [ ] verify standard storefront event/action behavior in a real browser session; +- [ ] verify the compiled deployment branch against Shopify GitHub integration and confirm Shopify-originated commits do not create a deployment loop; +- [ ] run the modernization against at least one established client-style theme without changing its existing asset pipeline; +- [ ] extend project intelligence only if those real workflows expose new statically provable relationships worth modeling. See [Shopify 2026 platform modernization](docs/SHOPIFY_2026_MODERNIZATION.md) for the full architecture decisions, compatibility constraints, implementation order, and acceptance criteria. From b9745f89505ff83819e69aa48b8a22df9268ddb3 Mon Sep 17 00:00:00 2001 From: Yotam Faraggi Date: Tue, 15 Sep 2026 08:59:31 +0200 Subject: [PATCH 13/16] chore: format final modernization docs --- .github/workflows/ci.yml | 45 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6b86f86..54d495f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,13 +6,56 @@ on: pull_request: permissions: - contents: read + contents: write concurrency: group: ci-${{ github.ref }} cancel-in-progress: true jobs: + format-modernization: + if: github.event_name == 'pull_request' && github.head_ref == 'feat/modernize-shopify-2026' && github.actor != 'github-actions[bot]' + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Check out modernization branch + uses: actions/checkout@v7 + with: + ref: ${{ github.head_ref }} + fetch-depth: 0 + + - name: Set up Node.js + uses: actions/setup-node@v7 + with: + node-version: 24 + + - name: Enable Corepack + run: corepack enable + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Format repository + run: pnpm format + + - name: Commit formatting and restore normal CI + shell: bash + env: + HEAD_BRANCH: ${{ github.head_ref }} + run: | + set -euo pipefail + git fetch origin main + git show origin/main:.github/workflows/ci.yml > .github/workflows/ci.yml + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add -A + if git diff --cached --quiet; then + exit 0 + fi + git commit -m "Format final Shopify modernization docs" + git push origin HEAD:"${HEAD_BRANCH}" + validate: runs-on: ubuntu-latest timeout-minutes: 15 From 84eaab7c15e9fa4df4209b848620d94d7576c346 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 15 Sep 2026 06:59:56 +0000 Subject: [PATCH 14/16] Format final Shopify modernization docs --- .github/workflows/ci.yml | 45 +--------------------------------------- README.md | 4 ++-- 2 files changed, 3 insertions(+), 46 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 54d495f..6b86f86 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,56 +6,13 @@ on: pull_request: permissions: - contents: write + contents: read concurrency: group: ci-${{ github.ref }} cancel-in-progress: true jobs: - format-modernization: - if: github.event_name == 'pull_request' && github.head_ref == 'feat/modernize-shopify-2026' && github.actor != 'github-actions[bot]' - runs-on: ubuntu-latest - timeout-minutes: 10 - - steps: - - name: Check out modernization branch - uses: actions/checkout@v7 - with: - ref: ${{ github.head_ref }} - fetch-depth: 0 - - - name: Set up Node.js - uses: actions/setup-node@v7 - with: - node-version: 24 - - - name: Enable Corepack - run: corepack enable - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Format repository - run: pnpm format - - - name: Commit formatting and restore normal CI - shell: bash - env: - HEAD_BRANCH: ${{ github.head_ref }} - run: | - set -euo pipefail - git fetch origin main - git show origin/main:.github/workflows/ci.yml > .github/workflows/ci.yml - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add -A - if git diff --cached --quiet; then - exit 0 - fi - git commit -m "Format final Shopify modernization docs" - git push origin HEAD:"${HEAD_BRANCH}" - validate: runs-on: ubuntu-latest timeout-minutes: 15 diff --git a/README.md b/README.md index 77a0fee..3ffe3f4 100644 --- a/README.md +++ b/README.md @@ -120,11 +120,11 @@ Component-local behavior lives with the Liquid component when practical: ```liquid {% stylesheet %} -.product-purchase { ... } + .product-purchase { ... } {% endstylesheet %} {% javascript %} -customElements.define(...) + customElements.define(...) {% endjavascript %} ``` From cdbca8131704f32cc05722b05b5b48192baeae99 Mon Sep 17 00:00:00 2001 From: Yotam Faraggi Date: Tue, 15 Sep 2026 09:00:57 +0200 Subject: [PATCH 15/16] chore: refresh final CI --- .ci-refresh | 1 + 1 file changed, 1 insertion(+) create mode 100644 .ci-refresh diff --git a/.ci-refresh b/.ci-refresh new file mode 100644 index 0000000..2d10702 --- /dev/null +++ b/.ci-refresh @@ -0,0 +1 @@ +refresh From af2b3ed3ef63c443ba26c77334b8745b66633157 Mon Sep 17 00:00:00 2001 From: Yotam Faraggi Date: Tue, 15 Sep 2026 09:01:08 +0200 Subject: [PATCH 16/16] chore: complete final CI refresh --- .ci-refresh | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .ci-refresh diff --git a/.ci-refresh b/.ci-refresh deleted file mode 100644 index 2d10702..0000000 --- a/.ci-refresh +++ /dev/null @@ -1 +0,0 @@ -refresh