From f201b95c4ceb3f7856c0eed6e219216d9c8fc967 Mon Sep 17 00:00:00 2001
From: feispro <262678496+feiscs@users.noreply.github.com>
Date: Tue, 12 May 2026 23:05:37 -0400
Subject: [PATCH] Add Turborepo starter workspace layout
---
.github/ISSUE_TEMPLATE/ai-agent-task.md | 24 +
.github/pull_request_template.md | 20 +
.github/workflows/check.yml | 22 +
.gitignore | 1 +
CLAUDE.md | 9 +
README.md | 11 +
apps/docs/index.html | 23 +
apps/docs/package.json | 15 +
apps/storefront/index.html | 24 +
apps/storefront/package.json | 16 +
assets/config.js | 26 +
assets/integrations.js | 189 ++++++
assets/store.css | 570 ++++++++++++++++++
assets/store.js | 507 ++++++++++++++++
docs/GITHUB_PLAIN_DIFFS.md | 34 ++
docs/GO_LIVE_CHECKLIST.md | 93 +++
docs/INTEGRATIONS.md | 106 ++++
docs/TURBOREPO.md | 82 +++
docs/UPSTREAM_SHOPIFY_RUBY.md | 76 +++
index.html | 264 +++++++-
package-lock.json | 63 +-
package.json | 31 +
packages/config/index.js | 11 +
packages/config/package.json | 11 +
packages/storefront-data/index.js | 9 +
packages/storefront-data/package.json | 11 +
packages/ui/index.js | 7 +
packages/ui/package.json | 11 +
scripts/github-plain-diff.js | 30 +
scripts/prepare-shopify-upstream.sh | 53 ++
scripts/smoke-app.js | 27 +
scripts/smoke-static.js | 55 ++
scripts/turbo-doctor.js | 27 +
scripts/verify-files.js | 106 ++++
shopify-ruby/.env.example | 11 +
shopify-ruby/Gemfile | 6 +
shopify-ruby/README.md | 40 ++
shopify-ruby/config/shopify_context.rb | 29 +
.../lib/forma_shopify/admin_client.rb | 48 ++
.../lib/forma_shopify/storefront_client.rb | 40 ++
shopify-ruby/scripts/list_products.rb | 14 +
shopify-theme/Gemfile | 9 +
shopify-theme/README.md | 42 ++
shopify-theme/assets/ajax-cart.js.liquid | 33 +
shopify-theme/assets/forma-theme.css.liquid | 152 +++++
shopify-theme/assets/forma-theme.js | 22 +
shopify-theme/assets/gift-card.scss.liquid | 29 +
shopify-theme/assets/timber.js.liquid | 33 +
shopify-theme/config.yml | 5 +
shopify-theme/config/settings_data.json | 7 +
shopify-theme/config/settings_schema.json | 30 +
shopify-theme/layout/theme.liquid | 27 +
shopify-theme/locales/en.default.json | 96 +++
shopify-theme/locales/es.default.json | 96 +++
.../snippets/ajax-cart-template.liquid | 12 +
shopify-theme/snippets/breadcrumb.liquid | 28 +
.../snippets/collection-sorting.liquid | 14 +
shopify-theme/snippets/comment.liquid | 6 +
shopify-theme/snippets/oldIE-js.liquid | 7 +
.../onboarding-empty-collection.liquid | 10 +
.../onboarding-featured-collections.liquid | 10 +
shopify-theme/snippets/product-card.liquid | 16 +
shopify-theme/snippets/site-footer.liquid | 13 +
shopify-theme/snippets/site-header.liquid | 14 +
shopify-theme/spec/helpers/html_helper.rb | 32 +
shopify-theme/spec/helpers/i18n_helper.rb | 38 ++
shopify-theme/spec/html_validity_spec.rb | 11 +
shopify-theme/spec/i18n_validity_spec.rb | 27 +
shopify-theme/spec/spec_helper.rb | 13 +
shopify-theme/spec/theme_structure_spec.rb | 50 ++
shopify-theme/templates/404.liquid | 7 +
shopify-theme/templates/article.liquid | 14 +
shopify-theme/templates/blog.liquid | 9 +
shopify-theme/templates/cart.liquid | 18 +
shopify-theme/templates/collection.liquid | 15 +
.../templates/collection.list.liquid | 9 +
.../templates/customers/account.liquid | 5 +
.../templates/customers/addresses.liquid | 8 +
.../templates/customers/login.liquid | 9 +
.../templates/customers/order.liquid | 5 +
.../templates/customers/register.liquid | 11 +
shopify-theme/templates/gift_card.liquid | 23 +
shopify-theme/templates/index.liquid | 31 +
.../templates/list-collections.liquid | 10 +
shopify-theme/templates/page.contact.liquid | 13 +
shopify-theme/templates/page.liquid | 4 +
shopify-theme/templates/product.liquid | 19 +
shopify-theme/templates/search.liquid | 18 +
.../202605130001_storefront_core.sql | 32 +
turbo.json | 42 ++
vercel.json | 24 +
91 files changed, 3941 insertions(+), 19 deletions(-)
create mode 100644 .github/ISSUE_TEMPLATE/ai-agent-task.md
create mode 100644 .github/pull_request_template.md
create mode 100644 .github/workflows/check.yml
create mode 100644 CLAUDE.md
create mode 100644 README.md
create mode 100644 apps/docs/index.html
create mode 100644 apps/docs/package.json
create mode 100644 apps/storefront/index.html
create mode 100644 apps/storefront/package.json
create mode 100644 assets/config.js
create mode 100644 assets/integrations.js
create mode 100644 assets/store.css
create mode 100644 assets/store.js
create mode 100644 docs/GITHUB_PLAIN_DIFFS.md
create mode 100644 docs/GO_LIVE_CHECKLIST.md
create mode 100644 docs/INTEGRATIONS.md
create mode 100644 docs/TURBOREPO.md
create mode 100644 docs/UPSTREAM_SHOPIFY_RUBY.md
create mode 100644 package.json
create mode 100644 packages/config/index.js
create mode 100644 packages/config/package.json
create mode 100644 packages/storefront-data/index.js
create mode 100644 packages/storefront-data/package.json
create mode 100644 packages/ui/index.js
create mode 100644 packages/ui/package.json
create mode 100755 scripts/github-plain-diff.js
create mode 100755 scripts/prepare-shopify-upstream.sh
create mode 100755 scripts/smoke-app.js
create mode 100644 scripts/smoke-static.js
create mode 100755 scripts/turbo-doctor.js
create mode 100644 scripts/verify-files.js
create mode 100644 shopify-ruby/.env.example
create mode 100644 shopify-ruby/Gemfile
create mode 100644 shopify-ruby/README.md
create mode 100644 shopify-ruby/config/shopify_context.rb
create mode 100644 shopify-ruby/lib/forma_shopify/admin_client.rb
create mode 100644 shopify-ruby/lib/forma_shopify/storefront_client.rb
create mode 100755 shopify-ruby/scripts/list_products.rb
create mode 100644 shopify-theme/Gemfile
create mode 100644 shopify-theme/README.md
create mode 100644 shopify-theme/assets/ajax-cart.js.liquid
create mode 100644 shopify-theme/assets/forma-theme.css.liquid
create mode 100644 shopify-theme/assets/forma-theme.js
create mode 100644 shopify-theme/assets/gift-card.scss.liquid
create mode 100644 shopify-theme/assets/timber.js.liquid
create mode 100644 shopify-theme/config.yml
create mode 100644 shopify-theme/config/settings_data.json
create mode 100644 shopify-theme/config/settings_schema.json
create mode 100644 shopify-theme/layout/theme.liquid
create mode 100644 shopify-theme/locales/en.default.json
create mode 100644 shopify-theme/locales/es.default.json
create mode 100644 shopify-theme/snippets/ajax-cart-template.liquid
create mode 100644 shopify-theme/snippets/breadcrumb.liquid
create mode 100644 shopify-theme/snippets/collection-sorting.liquid
create mode 100644 shopify-theme/snippets/comment.liquid
create mode 100644 shopify-theme/snippets/oldIE-js.liquid
create mode 100644 shopify-theme/snippets/onboarding-empty-collection.liquid
create mode 100644 shopify-theme/snippets/onboarding-featured-collections.liquid
create mode 100644 shopify-theme/snippets/product-card.liquid
create mode 100644 shopify-theme/snippets/site-footer.liquid
create mode 100644 shopify-theme/snippets/site-header.liquid
create mode 100644 shopify-theme/spec/helpers/html_helper.rb
create mode 100644 shopify-theme/spec/helpers/i18n_helper.rb
create mode 100644 shopify-theme/spec/html_validity_spec.rb
create mode 100644 shopify-theme/spec/i18n_validity_spec.rb
create mode 100644 shopify-theme/spec/spec_helper.rb
create mode 100644 shopify-theme/spec/theme_structure_spec.rb
create mode 100644 shopify-theme/templates/404.liquid
create mode 100644 shopify-theme/templates/article.liquid
create mode 100644 shopify-theme/templates/blog.liquid
create mode 100644 shopify-theme/templates/cart.liquid
create mode 100644 shopify-theme/templates/collection.liquid
create mode 100644 shopify-theme/templates/collection.list.liquid
create mode 100644 shopify-theme/templates/customers/account.liquid
create mode 100644 shopify-theme/templates/customers/addresses.liquid
create mode 100644 shopify-theme/templates/customers/login.liquid
create mode 100644 shopify-theme/templates/customers/order.liquid
create mode 100644 shopify-theme/templates/customers/register.liquid
create mode 100644 shopify-theme/templates/gift_card.liquid
create mode 100644 shopify-theme/templates/index.liquid
create mode 100644 shopify-theme/templates/list-collections.liquid
create mode 100644 shopify-theme/templates/page.contact.liquid
create mode 100644 shopify-theme/templates/page.liquid
create mode 100644 shopify-theme/templates/product.liquid
create mode 100644 shopify-theme/templates/search.liquid
create mode 100644 supabase/migrations/202605130001_storefront_core.sql
create mode 100644 turbo.json
create mode 100644 vercel.json
diff --git a/.github/ISSUE_TEMPLATE/ai-agent-task.md b/.github/ISSUE_TEMPLATE/ai-agent-task.md
new file mode 100644
index 0000000..4563853
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/ai-agent-task.md
@@ -0,0 +1,24 @@
+---
+name: AI agent task
+about: Scope work for Claude/Copilot/GitHub coding agents
+labels: ai-agent, needs-plan
+---
+
+## Goal
+
+Describe the customer/business outcome.
+
+## Context
+
+- Storefront area:
+- Shopify/Supabase/Chatbase/Vercel impact:
+- Design references:
+- Plain `.patch`/`.diff` URL, if reviewing GitHub changes:
+
+## Acceptance criteria
+
+- [ ] Implementation matches the requested UX.
+- [ ] `npm run check` passes.
+- [ ] Vercel preview link is attached for UI changes.
+- [ ] No secrets are committed.
+- [ ] PR summary explains Shopify/Supabase/Chatbase impact if relevant.
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644
index 0000000..97b4b16
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,20 @@
+## Summary
+
+-
+
+## Integration impact
+
+- Shopify:
+- Supabase:
+- Chatbase:
+- Vercel:
+
+## Review links
+
+- Plain diff/patch URL:
+
+## Testing
+
+- [ ] `npm run check`
+- [ ] Vercel preview reviewed, if UI changed
+- [ ] No secrets committed
diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml
new file mode 100644
index 0000000..987fe24
--- /dev/null
+++ b/.github/workflows/check.yml
@@ -0,0 +1,22 @@
+name: Storefront checks
+
+on:
+ pull_request:
+ push:
+ branches: [main, master, work]
+
+jobs:
+ static-check:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ - name: Setup Node
+ uses: actions/setup-node@v4
+ with:
+ node-version: 22
+ cache: npm
+ - name: Install dependencies
+ run: npm ci
+ - name: Run static checks
+ run: npm run check
diff --git a/.gitignore b/.gitignore
index 713d500..74072f3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
node_modules/
.env
+.turbo/
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..97aad93
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,9 @@
+# Claude/GitHub agent instructions for FORMA
+
+- Keep the storefront deployable as a static Vercel site unless a task explicitly adds a backend.
+- Do not commit production secrets. Use `assets/config.js` only with blank/demo values in git.
+- Prefer adding integration hooks in `assets/integrations.js` and UI behavior in `assets/store.js`.
+- Run `npm run check` before every PR.
+- For visual changes, attach a Vercel preview link or screenshot evidence in the PR.
+- Keep Shopify as the commerce source of truth, Supabase as the CRM/event store, and Chatbase as the support assistant.
+- When reviewing GitHub changes, prefer plain-text `.patch` or `.diff` URLs and see `docs/GITHUB_PLAIN_DIFFS.md`.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..2a1cb59
--- /dev/null
+++ b/README.md
@@ -0,0 +1,11 @@
+# FORMA storefront
+
+See `docs/GO_LIVE_CHECKLIST.md` to deploy and connect integrations.
+
+## Turborepo
+
+See `docs/TURBOREPO.md` for installing and running Turbo with this repo.
+
+## Monorepo apps and packages
+
+This repo now includes two deployable apps (`apps/storefront`, `apps/docs`) and three shared packages (`packages/ui`, `packages/config`, `packages/storefront-data`) to match the Turborepo starter shape.
diff --git a/apps/docs/index.html b/apps/docs/index.html
new file mode 100644
index 0000000..0cc19da
--- /dev/null
+++ b/apps/docs/index.html
@@ -0,0 +1,23 @@
+
+
+
+
+
+ FORMA Docs App
+
+
+
+
+ Deployable app 2 · apps/docs
+ FORMA Docs
+ Documentación deployable para instalación, go-live, Shopify, Supabase, Chatbase, Vercel y Turborepo.
+
+
+
+
diff --git a/apps/docs/package.json b/apps/docs/package.json
new file mode 100644
index 0000000..37ee4bd
--- /dev/null
+++ b/apps/docs/package.json
@@ -0,0 +1,15 @@
+{
+ "name": "@forma/app-docs",
+ "version": "1.0.0",
+ "private": true,
+ "scripts": {
+ "build": "node ../../scripts/smoke-app.js .",
+ "check": "node ../../scripts/smoke-app.js .",
+ "dev": "python3 -m http.server 4175 --directory apps/docs",
+ "start": "python3 -m http.server ${PORT:-4175} --directory apps/docs"
+ },
+ "dependencies": {
+ "@forma/config": "*",
+ "@forma/ui": "*"
+ }
+}
diff --git a/apps/storefront/index.html b/apps/storefront/index.html
new file mode 100644
index 0000000..999c10c
--- /dev/null
+++ b/apps/storefront/index.html
@@ -0,0 +1,24 @@
+
+
+
+
+
+ FORMA Storefront App
+
+
+
+
+ Deployable app 1 · apps/storefront
+ FORMA Storefront
+ App estática deployable dentro del monorepo Turborepo. La tienda principal completa sigue disponible en la raíz del repo.
+
+
Trench Lino Arena Prendas · $148
+
Bolso Arc Cognac Bolsos · $96
+
Tote Weekend Bolsos · $112
+
+ Abrir storefront completo
+
+
+
diff --git a/apps/storefront/package.json b/apps/storefront/package.json
new file mode 100644
index 0000000..5ae48cd
--- /dev/null
+++ b/apps/storefront/package.json
@@ -0,0 +1,16 @@
+{
+ "name": "@forma/app-storefront",
+ "version": "1.0.0",
+ "private": true,
+ "scripts": {
+ "build": "node ../../scripts/smoke-app.js .",
+ "check": "node ../../scripts/smoke-app.js .",
+ "dev": "python3 -m http.server 4174 --directory apps/storefront",
+ "start": "python3 -m http.server ${PORT:-4174} --directory apps/storefront"
+ },
+ "dependencies": {
+ "@forma/config": "*",
+ "@forma/storefront-data": "*",
+ "@forma/ui": "*"
+ }
+}
diff --git a/assets/config.js b/assets/config.js
new file mode 100644
index 0000000..eef72a0
--- /dev/null
+++ b/assets/config.js
@@ -0,0 +1,26 @@
+// FORMA runtime configuration.
+// Keep real credentials in Vercel Environment Variables or generate this file during deploy.
+// Values left blank keep the storefront in safe local-demo mode.
+window.FORMA_CONFIG = {
+ shopify: {
+ domain: '',
+ storefrontToken: '',
+ apiVersion: '2026-01',
+ enableRemoteProducts: false,
+ },
+ supabase: {
+ url: '',
+ anonKey: '',
+ eventsTable: 'store_events',
+ newsletterTable: 'newsletter_signups',
+ },
+ chatbase: {
+ botId: '',
+ enabled: false,
+ },
+ githubAgent: {
+ provider: 'Claude',
+ repo: 'FEISHTML',
+ workflow: 'Plan → branch → PR → Vercel preview → review → merge',
+ },
+};
diff --git a/assets/integrations.js b/assets/integrations.js
new file mode 100644
index 0000000..477d5ff
--- /dev/null
+++ b/assets/integrations.js
@@ -0,0 +1,189 @@
+(function attachFormaIntegrations() {
+ const emptyConfig = {
+ shopify: {},
+ supabase: {},
+ chatbase: {},
+ githubAgent: {},
+ };
+
+ function getConfig() {
+ return { ...emptyConfig, ...(window.FORMA_CONFIG || {}) };
+ }
+
+ function hasSupabase() {
+ const { supabase } = getConfig();
+ return Boolean(supabase?.url && supabase?.anonKey);
+ }
+
+ async function supabaseInsert(tableName, payload) {
+ const { supabase } = getConfig();
+ if (!hasSupabase() || !tableName) return { skipped: true };
+
+ const response = await fetch(`${supabase.url.replace(/\/$/, '')}/rest/v1/${tableName}`, {
+ method: 'POST',
+ headers: {
+ apikey: supabase.anonKey,
+ Authorization: `Bearer ${supabase.anonKey}`,
+ 'Content-Type': 'application/json',
+ Prefer: 'return=minimal',
+ },
+ body: JSON.stringify(payload),
+ });
+
+ if (!response.ok) {
+ throw new Error(`Supabase insert failed: ${response.status}`);
+ }
+
+ return { ok: true };
+ }
+
+ async function trackEvent(eventName, payload = {}) {
+ const { supabase } = getConfig();
+ try {
+ return await supabaseInsert(supabase.eventsTable, {
+ event_name: eventName,
+ payload,
+ page_path: window.location.pathname,
+ user_agent: window.navigator.userAgent,
+ created_at: new Date().toISOString(),
+ });
+ } catch (error) {
+ console.warn('[FORMA] Event tracking skipped:', error.message);
+ return { skipped: true, error };
+ }
+ }
+
+ async function saveNewsletter(email) {
+ const { supabase } = getConfig();
+ try {
+ return await supabaseInsert(supabase.newsletterTable, {
+ email,
+ source: 'forma-storefront',
+ created_at: new Date().toISOString(),
+ });
+ } catch (error) {
+ console.warn('[FORMA] Newsletter sync skipped:', error.message);
+ return { skipped: true, error };
+ }
+ }
+
+ function loadChatbase() {
+ const { chatbase } = getConfig();
+ if (!chatbase?.enabled || !chatbase?.botId || document.querySelector('[data-chatbase-loader]')) {
+ return false;
+ }
+
+ window.embeddedChatbotConfig = { chatbotId: chatbase.botId, domain: 'www.chatbase.co' };
+ const script = document.createElement('script');
+ script.src = 'https://www.chatbase.co/embed.min.js';
+ script.id = chatbase.botId;
+ script.setAttribute('chatbotId', chatbase.botId);
+ script.setAttribute('domain', 'www.chatbase.co');
+ script.dataset.chatbaseLoader = 'true';
+ script.defer = true;
+ document.body.appendChild(script);
+ return true;
+ }
+
+ function hasShopify() {
+ const { shopify } = getConfig();
+ return Boolean(shopify?.domain && shopify?.storefrontToken && shopify?.enableRemoteProducts);
+ }
+
+ async function fetchShopifyProducts() {
+ const { shopify } = getConfig();
+ if (!hasShopify()) return [];
+
+ const endpoint = `https://${shopify.domain}/api/${shopify.apiVersion || '2026-01'}/graphql.json`;
+ const query = `
+ query FormaProducts($first: Int!) {
+ products(first: $first, sortKey: CREATED_AT, reverse: true) {
+ edges {
+ node {
+ id
+ title
+ productType
+ createdAt
+ description
+ tags
+ featuredImage { url altText }
+ variants(first: 20) {
+ edges {
+ node {
+ id
+ title
+ price { amount currencyCode }
+ compareAtPrice { amount currencyCode }
+ selectedOptions { name value }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ `;
+
+ const response = await fetch(endpoint, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-Shopify-Storefront-Access-Token': shopify.storefrontToken,
+ },
+ body: JSON.stringify({ query, variables: { first: 12 } }),
+ });
+
+ if (!response.ok) {
+ throw new Error(`Shopify request failed: ${response.status}`);
+ }
+
+ const data = await response.json();
+ if (data.errors) {
+ throw new Error(data.errors.map((error) => error.message).join(', '));
+ }
+
+ return data.data.products.edges.map(({ node }, index) => {
+ const firstVariant = node.variants.edges[0]?.node;
+ const price = Number(firstVariant?.price?.amount || 0);
+ const oldPrice = Number(firstVariant?.compareAtPrice?.amount || 0) || null;
+ const colorValues = new Set();
+ const sizeValues = new Set();
+
+ node.variants.edges.forEach(({ node: variant }) => {
+ variant.selectedOptions.forEach((option) => {
+ if (/color/i.test(option.name)) colorValues.add(option.value);
+ if (/size|talla/i.test(option.name)) sizeValues.add(option.value);
+ });
+ });
+
+ return {
+ id: node.id,
+ shopifyVariantId: firstVariant?.id,
+ name: node.title,
+ category: node.productType || 'Shopify',
+ price,
+ oldPrice,
+ rating: 4.8,
+ badge: node.tags[0] || 'Shopify',
+ createdAt: 100 + index,
+ description: node.description || 'Producto sincronizado desde Shopify.',
+ colors: [...colorValues].length ? [...colorValues] : ['Default'],
+ sizes: [...sizeValues].length ? [...sizeValues] : ['Única'],
+ bg: '#eadfd0',
+ gradient: 'linear-gradient(145deg, #fff8ec, #c89b77)',
+ shape: '1.2rem',
+ image: node.featuredImage?.url,
+ };
+ });
+ }
+
+ window.FormaIntegrations = {
+ fetchShopifyProducts,
+ getConfig,
+ hasShopify,
+ hasSupabase,
+ loadChatbase,
+ saveNewsletter,
+ trackEvent,
+ };
+})();
diff --git a/assets/store.css b/assets/store.css
new file mode 100644
index 0000000..a9e0052
--- /dev/null
+++ b/assets/store.css
@@ -0,0 +1,570 @@
+:root {
+ --ink: #16130f;
+ --muted: #766f66;
+ --line: #e8dfd3;
+ --paper: #fbf7f0;
+ --sand: #efe4d4;
+ --clay: #b96f4f;
+ --olive: #6f7758;
+ --cream: #fffaf2;
+ --shadow: 0 28px 90px rgba(52, 39, 25, 0.16);
+}
+
+* { box-sizing: border-box; }
+html { scroll-behavior: smooth; }
+body {
+ margin: 0;
+ color: var(--ink);
+ background: linear-gradient(180deg, #fffaf2, #f3eadc 45%, #fffaf2);
+ font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
+}
+body.is-locked { overflow: hidden; }
+a { color: inherit; text-decoration: none; }
+button, input, select { font: inherit; }
+button { cursor: pointer; }
+
+.navbar {
+ position: sticky;
+ top: 0;
+ z-index: 50;
+ display: grid;
+ grid-template-columns: auto 1fr auto;
+ align-items: center;
+ gap: 2rem;
+ padding: 1rem clamp(1rem, 4vw, 4rem);
+ background: rgba(255, 250, 242, 0.84);
+ border-bottom: 1px solid rgba(22, 19, 15, 0.08);
+ backdrop-filter: blur(18px);
+}
+.brand {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.7rem;
+ font-weight: 900;
+ letter-spacing: 0.22em;
+}
+.brand-mark {
+ display: grid;
+ place-items: center;
+ width: 2.35rem;
+ height: 2.35rem;
+ color: var(--cream);
+ background: var(--ink);
+ border-radius: 50%;
+ letter-spacing: 0;
+}
+.nav-links {
+ display: flex;
+ justify-content: center;
+ gap: clamp(1rem, 3vw, 2.5rem);
+ color: var(--muted);
+ font-size: 0.9rem;
+ font-weight: 800;
+}
+.nav-links a:hover { color: var(--ink); }
+.nav-actions { display: flex; align-items: center; gap: 0.65rem; }
+.search-box {
+ display: flex;
+ align-items: center;
+ gap: 0.45rem;
+ width: min(18vw, 13rem);
+ padding: 0.65rem 0.85rem;
+ background: #fff;
+ border: 1px solid var(--line);
+ border-radius: 999px;
+}
+.search-box input {
+ min-width: 0;
+ width: 100%;
+ border: 0;
+ outline: 0;
+ background: transparent;
+}
+.nav-icon, .cart-trigger, .view-toggle, .tab, .close-btn {
+ border: 1px solid var(--line);
+ color: var(--ink);
+ background: #fff;
+}
+.nav-icon {
+ position: relative;
+ display: grid;
+ place-items: center;
+ width: 2.75rem;
+ height: 2.75rem;
+ border-radius: 50%;
+ font-weight: 900;
+}
+.nav-icon strong, .cart-trigger strong {
+ display: grid;
+ place-items: center;
+ min-width: 1.35rem;
+ height: 1.35rem;
+ color: #fff;
+ background: var(--clay);
+ border-radius: 999px;
+ font-size: 0.72rem;
+}
+.nav-icon strong {
+ position: absolute;
+ top: -0.35rem;
+ right: -0.25rem;
+}
+.cart-trigger {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.55rem;
+ min-height: 2.75rem;
+ padding: 0 1rem;
+ border-radius: 999px;
+ font-weight: 900;
+}
+
+.hero {
+ display: grid;
+ grid-template-columns: minmax(0, 0.95fr) minmax(330px, 1.05fr);
+ gap: clamp(2rem, 6vw, 6rem);
+ align-items: center;
+ min-height: calc(100vh - 5rem);
+ padding: clamp(3rem, 6vw, 6rem) clamp(1rem, 5vw, 5rem);
+}
+.eyebrow {
+ margin: 0 0 0.85rem;
+ color: var(--clay);
+ font-size: 0.76rem;
+ font-weight: 900;
+ letter-spacing: 0.2em;
+ text-transform: uppercase;
+}
+h1, h2, p { margin-top: 0; }
+h1, h2 {
+ font-family: "Playfair Display", Georgia, serif;
+ line-height: 0.94;
+ letter-spacing: -0.055em;
+}
+h1 { max-width: 11ch; margin-bottom: 1.35rem; font-size: clamp(4rem, 9vw, 9rem); }
+h2 { margin-bottom: 1rem; font-size: clamp(2.5rem, 6vw, 5.4rem); }
+.hero-copy > p:not(.eyebrow), .section-head p, .editorial p, .footer p {
+ max-width: 42rem;
+ color: var(--muted);
+ line-height: 1.75;
+ font-size: 1.05rem;
+}
+.hero-actions { display: flex; flex-wrap: wrap; gap: 1rem; margin: 2rem 0; }
+.btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-height: 3.15rem;
+ padding: 0 1.25rem;
+ border: 1px solid transparent;
+ border-radius: 999px;
+ font-weight: 900;
+ transition: transform 180ms ease, box-shadow 180ms ease, background 180ms ease;
+}
+.btn:hover { transform: translateY(-2px); box-shadow: 0 16px 35px rgba(22, 19, 15, 0.13); }
+.btn-dark { color: #fff; background: var(--ink); }
+.btn-light { color: var(--ink); background: #fff; border-color: var(--line); }
+.stats {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 0.8rem;
+ max-width: 35rem;
+ margin: 0;
+}
+.stats div, .feature-bar article, .product-card, .drawer-panel, .product-modal, .footer {
+ background: rgba(255, 255, 255, 0.72);
+ border: 1px solid rgba(22, 19, 15, 0.08);
+ box-shadow: var(--shadow);
+}
+.stats div { padding: 1rem; border-radius: 1.25rem; }
+.stats dt { font-size: 2rem; font-weight: 900; }
+.stats dd { margin: 0; color: var(--muted); font-size: 0.86rem; }
+
+.campaign-card { position: relative; min-height: 42rem; }
+.campaign-image {
+ position: absolute;
+ inset: 0;
+ overflow: hidden;
+ background:
+ radial-gradient(circle at 68% 38%, rgba(185, 111, 79, 0.3), transparent 18rem),
+ linear-gradient(140deg, #d6c1a7, #f8eee0 46%, #8a765f 46.5%, #c08a67 65%, #efe4d4);
+ border: 1px solid rgba(22, 19, 15, 0.1);
+ border-radius: 2.5rem;
+ box-shadow: var(--shadow);
+}
+.campaign-image::before {
+ content: "";
+ position: absolute;
+ right: 12%;
+ bottom: 10%;
+ width: 42%;
+ height: 58%;
+ background: linear-gradient(160deg, #251f19, #8e6048 58%, #e7d1b7 59%);
+ border-radius: 48% 48% 1.4rem 1.4rem;
+ box-shadow: 0 30px 60px rgba(22, 19, 15, 0.25);
+}
+.campaign-image::after {
+ content: "FORMA";
+ position: absolute;
+ left: -0.4rem;
+ bottom: 2rem;
+ color: rgba(255, 255, 255, 0.54);
+ font-size: clamp(5rem, 10vw, 9rem);
+ font-weight: 900;
+ letter-spacing: 0.08em;
+}
+.campaign-note {
+ position: absolute;
+ left: 1.5rem;
+ top: 1.5rem;
+ display: grid;
+ gap: 0.25rem;
+ padding: 1rem 1.1rem;
+ background: rgba(255, 250, 242, 0.78);
+ border: 1px solid rgba(255,255,255,0.4);
+ border-radius: 1rem;
+ backdrop-filter: blur(12px);
+}
+.campaign-note span { color: var(--clay); font-size: 0.75rem; font-weight: 900; text-transform: uppercase; }
+
+.feature-bar, .catalog, .editorial, .footer {
+ width: min(1180px, calc(100% - 2rem));
+ margin: 0 auto;
+}
+.feature-bar {
+ display: grid;
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+ gap: 1rem;
+ padding: 1rem 0 5rem;
+}
+.feature-bar article { padding: 1.2rem; border-radius: 1.25rem; }
+.feature-bar span { font-size: 1.7rem; }
+.feature-bar strong { display: block; margin: 0.7rem 0 0.25rem; }
+.feature-bar p { margin-bottom: 0; color: var(--muted); }
+
+.catalog, .editorial { padding: 5rem 0; }
+.section-head { max-width: 46rem; }
+.catalog-toolbar {
+ display: flex;
+ align-items: end;
+ justify-content: space-between;
+ gap: 1rem;
+ margin: 2rem 0 1.2rem;
+}
+.tabs, .toolbar-actions { display: flex; flex-wrap: wrap; gap: 0.65rem; }
+.tab, .view-toggle {
+ min-height: 2.75rem;
+ padding: 0 1rem;
+ border-radius: 999px;
+ font-weight: 900;
+}
+.tab.is-active, .tab:hover, .view-toggle[aria-pressed="true"] { color: #fff; background: var(--ink); }
+.toolbar-actions label {
+ display: grid;
+ gap: 0.35rem;
+ color: var(--muted);
+ font-size: 0.75rem;
+ font-weight: 900;
+ text-transform: uppercase;
+}
+select {
+ min-height: 2.75rem;
+ padding: 0 0.9rem;
+ border: 1px solid var(--line);
+ border-radius: 999px;
+ background: #fff;
+}
+.product-grid {
+ display: grid;
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+ gap: 1rem;
+}
+.product-grid.is-compact { grid-template-columns: repeat(6, minmax(0, 1fr)); }
+.product-card {
+ position: relative;
+ overflow: hidden;
+ border-radius: 1.4rem;
+ transition: transform 180ms ease, box-shadow 180ms ease;
+}
+.product-card:hover { transform: translateY(-4px); }
+.product-media {
+ position: relative;
+ display: grid;
+ place-items: center;
+ min-height: 17rem;
+ overflow: hidden;
+ background: var(--product-bg, var(--sand));
+}
+.product-visual {
+ width: 62%;
+ aspect-ratio: 0.82;
+ border-radius: var(--shape, 42% 42% 1.2rem 1.2rem);
+ background: var(--product-gradient);
+ box-shadow: 0 20px 45px rgba(22, 19, 15, 0.18);
+ transition: transform 250ms ease;
+}
+.product-card:hover .product-visual { transform: scale(1.06) rotate(-2deg); }
+.badge {
+ position: absolute;
+ top: 0.8rem;
+ left: 0.8rem;
+ padding: 0.45rem 0.65rem;
+ color: #fff;
+ background: var(--clay);
+ border-radius: 999px;
+ font-size: 0.72rem;
+ font-weight: 900;
+ text-transform: uppercase;
+}
+.favorite-btn {
+ position: absolute;
+ top: 0.65rem;
+ right: 0.65rem;
+ display: grid;
+ place-items: center;
+ width: 2.35rem;
+ height: 2.35rem;
+ border: 0;
+ background: rgba(255,255,255,0.8);
+ border-radius: 50%;
+ font-size: 1.2rem;
+}
+.favorite-btn.is-active { color: #fff; background: var(--clay); }
+.quick-add {
+ position: absolute;
+ right: 0.8rem;
+ bottom: 0.8rem;
+ left: 0.8rem;
+ opacity: 0;
+ transform: translateY(0.5rem);
+}
+.product-card:hover .quick-add, .product-card:focus-within .quick-add { opacity: 1; transform: translateY(0); }
+.product-info { padding: 1rem; }
+.product-top { display: flex; justify-content: space-between; gap: 1rem; margin-bottom: 0.55rem; }
+.product-top h3 { margin: 0; font-size: 1rem; }
+.rating { color: var(--clay); font-weight: 900; }
+.product-info p { min-height: 3.2rem; color: var(--muted); line-height: 1.55; }
+.price-line { display: flex; align-items: center; justify-content: space-between; gap: 1rem; }
+.price-line strong { font-size: 1.18rem; }
+.price-line s { color: var(--muted); font-size: 0.86rem; }
+.detail-link { border: 0; background: transparent; color: var(--clay); font-weight: 900; }
+.product-grid.is-compact .product-media { min-height: 11rem; }
+.product-grid.is-compact .product-info p, .product-grid.is-compact .quick-add { display: none; }
+.empty-state { padding: 3rem; text-align: center; color: var(--muted); border: 1px dashed var(--line); border-radius: 1.2rem; }
+
+.editorial {
+ display: grid;
+ grid-template-columns: minmax(0, 0.85fr) minmax(320px, 1.15fr);
+ gap: clamp(2rem, 5vw, 4rem);
+ align-items: center;
+}
+.editorial-grid {
+ display: grid;
+ grid-template-columns: 1fr 0.8fr;
+ grid-template-rows: repeat(2, 14rem);
+ gap: 1rem;
+}
+.art { border-radius: 1.6rem; box-shadow: var(--shadow); }
+.art-one { grid-row: 1 / 3; background: linear-gradient(145deg, #806b56, #e9d4ba); }
+.art-two { background: linear-gradient(145deg, #2b2923, #9a7d5f); }
+.art-three { background: linear-gradient(145deg, #dcbf9e, #fff8eb); }
+
+.drawer {
+ position: fixed;
+ inset: 0;
+ z-index: 70;
+ pointer-events: none;
+ background: rgba(22, 19, 15, 0);
+ transition: background 180ms ease;
+}
+.drawer.is-open { pointer-events: auto; background: rgba(22, 19, 15, 0.34); }
+.drawer-panel {
+ position: absolute;
+ top: 0;
+ right: 0;
+ display: grid;
+ grid-template-rows: auto auto 1fr auto;
+ width: min(100%, 30rem);
+ height: 100%;
+ padding: 1.2rem;
+ background: var(--cream);
+ transform: translateX(105%);
+ transition: transform 220ms ease;
+}
+.drawer.is-open .drawer-panel { transform: translateX(0); }
+.small-panel { grid-template-rows: auto 1fr; }
+.drawer-head { display: flex; justify-content: space-between; align-items: start; border-bottom: 1px solid var(--line); }
+.drawer-head h2 { margin-bottom: 1rem; font-size: 2.9rem; }
+.close-btn {
+ display: grid;
+ place-items: center;
+ width: 2.6rem;
+ height: 2.6rem;
+ border-radius: 50%;
+ font-size: 1.5rem;
+}
+.shipping-meter { display: grid; gap: 0.7rem; padding: 1rem 0; color: var(--muted); font-size: 0.92rem; }
+.shipping-meter span { overflow: hidden; height: 0.5rem; background: var(--sand); border-radius: 999px; }
+.shipping-meter i { display: block; width: 0; height: 100%; background: var(--olive); border-radius: inherit; transition: width 180ms ease; }
+.cart-items, .wishlist-items { overflow: auto; }
+.cart-empty { color: var(--muted); text-align: center; padding: 4rem 1rem; }
+.line-item {
+ display: grid;
+ grid-template-columns: 4.5rem 1fr auto;
+ gap: 0.9rem;
+ align-items: center;
+ padding: 1rem 0;
+ border-bottom: 1px solid var(--line);
+}
+.line-thumb { height: 4.5rem; background: var(--thumb-bg); border-radius: 1rem; }
+.line-item h3 { margin: 0 0 0.2rem; font-size: 0.95rem; }
+.line-item p { margin: 0; color: var(--muted); font-size: 0.85rem; }
+.qty { display: inline-flex; align-items: center; gap: 0.55rem; margin-top: 0.55rem; }
+.qty button { width: 1.7rem; height: 1.7rem; border: 1px solid var(--line); background: #fff; border-radius: 50%; }
+.cart-footer { display: grid; gap: 1rem; padding-top: 1rem; border-top: 1px solid var(--line); }
+.cart-footer dl { display: grid; gap: 0.65rem; margin: 0; }
+.cart-footer div { display: flex; justify-content: space-between; }
+.cart-footer dt { color: var(--muted); }
+.cart-footer dd { margin: 0; font-weight: 900; }
+
+.product-modal {
+ width: min(960px, calc(100% - 2rem));
+ padding: 0;
+ border: 0;
+ border-radius: 1.6rem;
+ background: var(--cream);
+}
+.product-modal::backdrop { background: rgba(22, 19, 15, 0.45); backdrop-filter: blur(3px); }
+.modal-close { position: absolute; top: 1rem; right: 1rem; z-index: 2; }
+.modal-grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(320px, 0.92fr); gap: 1.4rem; padding: 1.2rem; }
+.modal-gallery { display: grid; gap: 0.8rem; }
+.modal-hero { min-height: 28rem; display: grid; place-items: center; background: var(--modal-bg); border-radius: 1.2rem; }
+.modal-hero .product-visual { width: 42%; }
+.thumbs { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.65rem; }
+.thumbs button { min-height: 5rem; border: 1px solid var(--line); border-radius: 0.9rem; background: var(--modal-bg); }
+.modal-copy { padding: 1rem 1rem 1rem 0; }
+.modal-copy h2 { font-size: clamp(2.4rem, 5vw, 4.2rem); }
+.option-group { margin: 1.2rem 0; }
+.option-group strong { display: block; margin-bottom: 0.6rem; }
+.option-list { display: flex; flex-wrap: wrap; gap: 0.55rem; }
+.option-list button { min-height: 2.35rem; padding: 0 0.8rem; border: 1px solid var(--line); background: #fff; border-radius: 999px; font-weight: 800; }
+.option-list button.is-selected { color: #fff; background: var(--ink); }
+.guarantees { display: grid; gap: 0.55rem; margin: 1rem 0; padding: 0; list-style: none; color: var(--muted); }
+.guarantees li::before { content: "✓"; margin-right: 0.55rem; color: var(--olive); font-weight: 900; }
+
+.footer {
+ display: grid;
+ grid-template-columns: 1fr 1.25fr 0.55fr;
+ gap: 2rem;
+ margin-top: 4rem;
+ margin-bottom: 1rem;
+ padding: 2rem;
+ border-radius: 1.6rem;
+}
+.newsletter label { display: block; margin-bottom: 0.75rem; font-weight: 900; }
+.newsletter div { display: flex; gap: 0.65rem; }
+.newsletter input {
+ width: 100%;
+ min-height: 3.15rem;
+ padding: 0 1rem;
+ border: 1px solid var(--line);
+ border-radius: 999px;
+}
+.newsletter p { min-height: 1.4rem; margin: 0.6rem 0 0; color: var(--olive); font-weight: 800; }
+.footer nav { display: grid; gap: 0.7rem; align-content: start; color: var(--muted); font-weight: 800; }
+.footer nav a:hover { color: var(--ink); }
+
+@media (max-width: 1020px) {
+ .navbar { grid-template-columns: 1fr auto; }
+ .nav-links { display: none; }
+ .search-box { width: 10rem; }
+ .hero, .editorial, .modal-grid { grid-template-columns: 1fr; }
+ .campaign-card { min-height: 32rem; }
+ .feature-bar, .product-grid, .product-grid.is-compact { grid-template-columns: repeat(2, minmax(0, 1fr)); }
+ .catalog-toolbar { align-items: stretch; flex-direction: column; }
+ .footer { grid-template-columns: 1fr; }
+}
+
+@media (max-width: 640px) {
+ .navbar { gap: 0.8rem; padding-inline: 0.8rem; }
+ .brand { letter-spacing: 0.12em; }
+ .search-box, .nav-icon[aria-label="Cuenta de usuario"] { display: none; }
+ .hero { padding-top: 2rem; }
+ h1 { font-size: clamp(3.4rem, 18vw, 5rem); }
+ .stats, .feature-bar, .product-grid, .product-grid.is-compact, .editorial-grid { grid-template-columns: 1fr; }
+ .editorial-grid { grid-template-rows: repeat(3, 12rem); }
+ .art-one { grid-row: auto; }
+ .newsletter div { flex-direction: column; }
+}
+
+.ops-stack {
+ display: grid;
+ grid-template-columns: minmax(0, 0.9fr) minmax(320px, 1.1fr);
+ gap: clamp(2rem, 5vw, 4rem);
+ align-items: start;
+ width: min(1180px, calc(100% - 2rem));
+ margin: 0 auto;
+ padding: 5rem 0;
+}
+.ops-stack p { color: var(--muted); line-height: 1.75; }
+.ops-grid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 1rem;
+}
+.ops-grid article {
+ min-height: 11rem;
+ padding: 1.2rem;
+ background: rgba(255, 255, 255, 0.72);
+ border: 1px solid rgba(22, 19, 15, 0.08);
+ border-radius: 1.25rem;
+ box-shadow: var(--shadow);
+}
+.ops-grid article:last-child { grid-column: 1 / -1; }
+.ops-grid strong { display: block; margin-bottom: 0.65rem; font-size: 1.05rem; }
+.ops-grid span {
+ display: inline-flex;
+ margin-bottom: 0.7rem;
+ padding: 0.38rem 0.6rem;
+ color: #fff;
+ background: var(--ink);
+ border-radius: 999px;
+ font-size: 0.72rem;
+ font-weight: 900;
+ text-transform: uppercase;
+}
+.ops-grid span.is-live { background: var(--olive); }
+.product-image {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ transition: transform 250ms ease;
+}
+.product-card:hover .product-image { transform: scale(1.06); }
+.modal-hero .product-image { border-radius: 1.2rem; }
+
+@media (max-width: 1020px) {
+ .ops-stack { grid-template-columns: 1fr; }
+}
+
+@media (max-width: 640px) {
+ .ops-grid { grid-template-columns: 1fr; }
+ .ops-grid article:last-child { grid-column: auto; }
+}
+
+.runtime-panel {
+ margin-top: 1rem;
+ padding: 1rem;
+ color: var(--muted);
+ background: rgba(255, 255, 255, 0.72);
+ border: 1px solid rgba(22, 19, 15, 0.08);
+ border-radius: 1rem;
+ box-shadow: var(--shadow);
+}
+html[data-runtime-error] .runtime-panel::after {
+ content: " Error detectado: " attr(data-runtime-error);
+ display: block;
+ margin-top: 0.5rem;
+ color: #b00020;
+ font-weight: 900;
+}
diff --git a/assets/store.js b/assets/store.js
new file mode 100644
index 0000000..4933536
--- /dev/null
+++ b/assets/store.js
@@ -0,0 +1,507 @@
+const defaultProducts = [
+ {
+ id: 'linen-trench',
+ name: 'Trench Lino Arena',
+ category: 'Prendas',
+ price: 148,
+ oldPrice: 188,
+ rating: 4.9,
+ badge: '-20%',
+ createdAt: 8,
+ description: 'Trench liviano de lino premium con caída relajada, bolsillos amplios y acabado repelente al agua.',
+ colors: ['Arena', 'Oliva', 'Negro'],
+ sizes: ['XS', 'S', 'M', 'L'],
+ bg: '#ead9c3',
+ gradient: 'linear-gradient(145deg, #d8b894, #f8ecdc 48%, #77624b 49%, #b96f4f)',
+ shape: '44% 44% 1.2rem 1.2rem',
+ },
+ {
+ id: 'arc-bag',
+ name: 'Bolso Arc Cognac',
+ category: 'Bolsos',
+ price: 96,
+ oldPrice: null,
+ rating: 4.8,
+ badge: 'Nuevo',
+ createdAt: 10,
+ description: 'Bolso estructurado con asa curva, cierre magnético y compartimento interior para básicos diarios.',
+ colors: ['Cognac', 'Crema', 'Chocolate'],
+ sizes: ['Única'],
+ bg: '#e8c9aa',
+ gradient: 'radial-gradient(circle at 50% 18%, #fff1df 0 18%, transparent 19%), linear-gradient(160deg, #8a5136, #c48763)',
+ shape: '48% 48% 18% 18%',
+ },
+ {
+ id: 'ribbed-set',
+ name: 'Set Rib Studio',
+ category: 'Prendas',
+ price: 82,
+ oldPrice: 104,
+ rating: 4.7,
+ badge: '-21%',
+ createdAt: 6,
+ description: 'Conjunto ribbed de dos piezas con textura suave, cintura alta y fit cómodo para todo el día.',
+ colors: ['Marfil', 'Topo', 'Grafito'],
+ sizes: ['S', 'M', 'L'],
+ bg: '#eee4d6',
+ gradient: 'repeating-linear-gradient(90deg, #f7efe4 0 8px, #ddcbb7 8px 13px)',
+ shape: '1.1rem',
+ },
+ {
+ id: 'ceramic-vase',
+ name: 'Jarrón Forma 02',
+ category: 'Hogar',
+ price: 58,
+ oldPrice: null,
+ rating: 4.9,
+ badge: 'Nuevo',
+ createdAt: 9,
+ description: 'Pieza cerámica hecha a mano con silueta orgánica y esmalte mate para espacios minimalistas.',
+ colors: ['Crudo', 'Terracota'],
+ sizes: ['S', 'M'],
+ bg: '#e7ddcd',
+ gradient: 'linear-gradient(145deg, #fff8ec, #c89b77)',
+ shape: '50% 50% 34% 34%',
+ },
+ {
+ id: 'silk-scarf',
+ name: 'Pañuelo Seda Grid',
+ category: 'Accesorios',
+ price: 44,
+ oldPrice: null,
+ rating: 4.6,
+ badge: 'Limited',
+ createdAt: 7,
+ description: 'Pañuelo de seda con patrón geométrico, bordes enrollados y acabado satinado.',
+ colors: ['Marfil', 'Azul', 'Caramelo'],
+ sizes: ['Única'],
+ bg: '#dde1d2',
+ gradient: 'linear-gradient(45deg, #fff 0 25%, #6f7758 25% 50%, #d7a075 50% 75%, #fff 75%)',
+ shape: '0.8rem',
+ },
+ {
+ id: 'leather-belt',
+ name: 'Cinturón Nudo',
+ category: 'Accesorios',
+ price: 52,
+ oldPrice: 68,
+ rating: 4.7,
+ badge: '-24%',
+ createdAt: 5,
+ description: 'Cinturón en cuero vegetal con hebilla escultural y costuras tono sobre tono.',
+ colors: ['Negro', 'Cognac'],
+ sizes: ['S', 'M', 'L'],
+ bg: '#dcc7ae',
+ gradient: 'linear-gradient(90deg, #2a211b 0 22%, #b96f4f 22% 78%, #2a211b 78%)',
+ shape: '999px',
+ },
+ {
+ id: 'weekend-tote',
+ name: 'Tote Weekend',
+ category: 'Bolsos',
+ price: 112,
+ oldPrice: null,
+ rating: 4.8,
+ badge: 'Nuevo',
+ createdAt: 11,
+ description: 'Tote amplio de canvas encerado con correas de cuero y bolsillo para laptop de 14 pulgadas.',
+ colors: ['Canvas', 'Oliva', 'Negro'],
+ sizes: ['Única'],
+ bg: '#e2d3bf',
+ gradient: 'linear-gradient(160deg, #efe4d4 0 45%, #6f7758 46% 75%, #2b2923 76%)',
+ shape: '1rem 1rem 2rem 2rem',
+ },
+ {
+ id: 'scent-candle',
+ name: 'Vela Santal 300g',
+ category: 'Hogar',
+ price: 38,
+ oldPrice: 48,
+ rating: 4.5,
+ badge: '-20%',
+ createdAt: 4,
+ description: 'Vela aromática de santal, ámbar y cedro en vaso reusable de vidrio ahumado.',
+ colors: ['Ámbar', 'Humo'],
+ sizes: ['300g'],
+ bg: '#eadfd0',
+ gradient: 'linear-gradient(180deg, #fff1c7 0 12%, #37312c 13% 100%)',
+ shape: '0.5rem 0.5rem 1.2rem 1.2rem',
+ },
+];
+
+let products = [...defaultProducts];
+
+const store = {
+ state: {
+ cart: [],
+ wishlist: [],
+ filter: 'all',
+ sort: 'newest',
+ compact: false,
+ search: '',
+ selectedProduct: null,
+ selectedSize: null,
+ selectedColor: null,
+ },
+ set(partial) {
+ this.state = { ...this.state, ...partial };
+ render();
+ },
+};
+
+const money = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' });
+const escapeHTML = (value = '') => String(value).replace(/[&<>'"]/g, (char) => ({
+ '&': '&',
+ '<': '<',
+ '>': '>',
+ "'": ''',
+ '"': '"',
+}[char]));
+const productGrid = document.querySelector('[data-product-grid]');
+const emptyState = document.querySelector('[data-empty-state]');
+const cartDrawer = document.querySelector('[data-cart-drawer]');
+const wishlistDrawer = document.querySelector('[data-wishlist-drawer]');
+const modal = document.querySelector('[data-product-modal]');
+const modalContent = document.querySelector('[data-modal-content]');
+const runtimeStatus = document.querySelector('[data-runtime-status]');
+
+function setRuntimeStatus(message) {
+ if (runtimeStatus) runtimeStatus.textContent = message;
+}
+
+function visibleProducts() {
+ const query = store.state.search.trim().toLowerCase();
+ const filtered = products.filter((product) => {
+ const categoryMatch = store.state.filter === 'all' || product.category === store.state.filter;
+ const queryMatch = !query || `${product.name} ${product.category} ${product.description}`.toLowerCase().includes(query);
+ return categoryMatch && queryMatch;
+ });
+
+ return filtered.sort((a, b) => {
+ if (store.state.sort === 'price-asc') return a.price - b.price;
+ if (store.state.sort === 'price-desc') return b.price - a.price;
+ if (store.state.sort === 'rating') return b.rating - a.rating;
+ return b.createdAt - a.createdAt;
+ });
+}
+
+function productStyle(product) {
+ return `--product-bg:${product.bg};--thumb-bg:${product.bg};--modal-bg:${product.bg};--product-gradient:${product.gradient};--shape:${product.shape};`;
+}
+
+function productMedia(product) {
+ return product.image
+ ? ` `
+ : '
';
+}
+
+function renderProducts() {
+ const list = visibleProducts();
+ productGrid.dataset.enhanced = 'true';
+ productGrid.classList.toggle('is-compact', store.state.compact);
+ emptyState.hidden = list.length > 0;
+
+ productGrid.innerHTML = list.map((product) => {
+ const isFavorite = store.state.wishlist.includes(product.id);
+ return `
+
+
+ ${escapeHTML(product.badge)}
+ ${isFavorite ? '♥' : '♡'}
+ ${productMedia(product)}
+ Quick Add
+
+
+
+
${escapeHTML(product.name)}
+ ★ ${product.rating}
+
+
${escapeHTML(product.description)}
+
+ ${money.format(product.price)} ${product.oldPrice ? `${money.format(product.oldPrice)} ` : ''}
+ Ver detalle
+
+
+
+ `;
+ }).join('');
+}
+
+function addToCart(productId, options = {}) {
+ const product = products.find((item) => item.id === productId);
+ if (!product) return;
+ const size = options.size || product.sizes[0];
+ const color = options.color || product.colors[0];
+ const key = `${productId}-${size}-${color}`;
+ const existing = store.state.cart.find((item) => item.key === key);
+
+ if (existing) {
+ existing.quantity += 1;
+ } else {
+ store.state.cart.push({ ...product, key, size, color, quantity: 1 });
+ }
+
+ render();
+ window.FormaIntegrations?.trackEvent('cart_add', { productId, size, color, price: product.price });
+ openDrawer(cartDrawer);
+}
+
+function changeQuantity(key, amount) {
+ const item = store.state.cart.find((cartItem) => cartItem.key === key);
+ if (!item) return;
+ item.quantity += amount;
+ if (item.quantity <= 0) {
+ store.state.cart = store.state.cart.filter((cartItem) => cartItem.key !== key);
+ }
+ render();
+}
+
+function toggleFavorite(productId) {
+ const exists = store.state.wishlist.includes(productId);
+ store.state.wishlist = exists
+ ? store.state.wishlist.filter((id) => id !== productId)
+ : [...store.state.wishlist, productId];
+ render();
+}
+
+function renderCart() {
+ const cartItems = document.querySelector('[data-cart-items]');
+ const count = store.state.cart.reduce((sum, item) => sum + item.quantity, 0);
+ const subtotal = store.state.cart.reduce((sum, item) => sum + item.price * item.quantity, 0);
+ const freeShippingTarget = 100;
+ const remaining = Math.max(freeShippingTarget - subtotal, 0);
+ const shipping = subtotal === 0 ? 8 : remaining > 0 ? 8 : 0;
+ const progress = Math.min((subtotal / freeShippingTarget) * 100, 100);
+
+ document.querySelector('[data-cart-count]').textContent = count;
+ document.querySelector('[data-subtotal]').textContent = money.format(subtotal);
+ document.querySelector('[data-shipping-cost]').textContent = shipping === 0 ? 'Gratis' : money.format(shipping);
+ document.querySelector('[data-total]').textContent = money.format(subtotal + shipping);
+ document.querySelector('[data-shipping-progress]').style.width = `${progress}%`;
+ document.querySelector('[data-shipping-copy]').textContent = subtotal === 0
+ ? 'Agrega productos para desbloquear envío gratis.'
+ : remaining > 0
+ ? `Te faltan ${money.format(remaining)} para envío gratis.`
+ : '¡Desbloqueaste envío gratis!';
+
+ if (!store.state.cart.length) {
+ cartItems.innerHTML = 'Tu bolsa está vacía. Agrega tus favoritos para empezar.
';
+ return;
+ }
+
+ cartItems.innerHTML = store.state.cart.map((item) => `
+
+
+
+
${escapeHTML(item.name)}
+
${escapeHTML(item.color)} · ${escapeHTML(item.size)} · ${money.format(item.price)}
+
+ −
+ ${item.quantity}
+ +
+
+
+ ${money.format(item.price * item.quantity)}
+
+ `).join('');
+}
+
+function renderWishlist() {
+ document.querySelector('[data-wishlist-count]').textContent = store.state.wishlist.length;
+ const wishlistItems = document.querySelector('[data-wishlist-items]');
+ const list = store.state.wishlist.map((id) => products.find((product) => product.id === id)).filter(Boolean);
+
+ wishlistItems.innerHTML = list.length
+ ? list.map((product) => `
+
+
+ ${escapeHTML(product.name)} ${escapeHTML(product.category)} · ${money.format(product.price)}
+ Ver
+
+ `).join('')
+ : 'Aún no tienes favoritos.
';
+}
+
+function openProduct(productId) {
+ const product = products.find((item) => item.id === productId);
+ if (!product) return;
+ store.state.selectedProduct = product.id;
+ store.state.selectedSize = product.sizes[0];
+ store.state.selectedColor = product.colors[0];
+ renderModal();
+ if (typeof modal.showModal === 'function') {
+ modal.showModal();
+ } else {
+ modal.setAttribute('open', '');
+ }
+ document.body.classList.add('is-locked');
+}
+
+function renderModal() {
+ const product = products.find((item) => item.id === store.state.selectedProduct);
+ if (!product) return;
+
+ modalContent.innerHTML = `
+
+
${productMedia(product)}
+
+
+
+
+
+
+
+
${escapeHTML(product.category)}
+
${escapeHTML(product.name)}
+
${escapeHTML(product.description)}
+
${money.format(product.price)} ${product.oldPrice ? `${money.format(product.oldPrice)} ` : ''}
+
+
Talla
+
+ ${product.sizes.map((size) => `${escapeHTML(size)} `).join('')}
+
+
+
+
Color
+
+ ${product.colors.map((color) => `${escapeHTML(color)} `).join('')}
+
+
+
+ Garantía de calidad por 12 meses
+ Cambios fáciles durante 30 días
+ Empaque premium reciclable
+
+
Agregar al carrito
+
+ `;
+}
+
+function openDrawer(drawer) {
+ drawer.classList.add('is-open');
+ drawer.setAttribute('aria-hidden', 'false');
+ document.body.classList.add('is-locked');
+}
+
+function closeDrawer(drawer) {
+ drawer.classList.remove('is-open');
+ drawer.setAttribute('aria-hidden', 'true');
+ if (!cartDrawer.classList.contains('is-open') && !wishlistDrawer.classList.contains('is-open') && !modal.open) {
+ document.body.classList.remove('is-locked');
+ }
+}
+
+function closeModal() {
+ if (typeof modal.close === 'function') {
+ modal.close();
+ } else {
+ modal.removeAttribute('open');
+ }
+ document.body.classList.remove('is-locked');
+}
+
+function renderIntegrationStatus() {
+ const config = window.FormaIntegrations?.getConfig?.() || {};
+ const statuses = {
+ shopify: config.shopify?.enableRemoteProducts && config.shopify?.domain ? 'Conectado' : 'Modo demo',
+ supabase: config.supabase?.url && config.supabase?.anonKey ? 'Conectado' : 'Modo demo',
+ chatbase: config.chatbase?.enabled && config.chatbase?.botId ? 'Activo' : 'Modo demo',
+ };
+
+ Object.entries(statuses).forEach(([key, label]) => {
+ const node = document.querySelector(`[data-integration-status="${key}"]`);
+ if (!node) return;
+ node.textContent = label;
+ node.classList.toggle('is-live', label !== 'Modo demo');
+ });
+}
+
+function render() {
+ renderProducts();
+ renderCart();
+ renderWishlist();
+ renderIntegrationStatus();
+ setRuntimeStatus(`Tienda activa: ${products.length} registros de producto cargados. Filtros, carrito, wishlist y modal listos.`);
+}
+
+async function hydrateCatalog() {
+ try {
+ const remoteProducts = await window.FormaIntegrations?.fetchShopifyProducts?.();
+ if (remoteProducts?.length) {
+ products = remoteProducts;
+ render();
+ window.FormaIntegrations?.trackEvent('shopify_catalog_loaded', { count: remoteProducts.length });
+ } else {
+ setRuntimeStatus(`Tienda activa en modo demo: ${products.length} registros locales cargados. Configura Shopify para productos remotos.`);
+ }
+ } catch (error) {
+ console.warn('[FORMA] Shopify catalog fallback enabled:', error.message);
+ }
+}
+
+document.addEventListener('click', (event) => {
+ const filterButton = event.target.closest('[data-filter]');
+ const quickAddButton = event.target.closest('[data-quick-add]');
+ const productButton = event.target.closest('[data-open-product]');
+ const favoriteButton = event.target.closest('[data-favorite]');
+ const quantityButton = event.target.closest('[data-qty]');
+ const sizeButton = event.target.closest('[data-size]');
+ const colorButton = event.target.closest('[data-color]');
+ const modalAddButton = event.target.closest('[data-modal-add]');
+
+ if (filterButton) {
+ store.set({ filter: filterButton.dataset.filter });
+ document.querySelectorAll('[data-filter]').forEach((button) => button.classList.toggle('is-active', button === filterButton));
+ }
+ if (quickAddButton) addToCart(quickAddButton.dataset.quickAdd);
+ if (productButton) openProduct(productButton.dataset.openProduct);
+ if (favoriteButton) toggleFavorite(favoriteButton.dataset.favorite);
+ if (quantityButton) changeQuantity(quantityButton.dataset.qty, Number(quantityButton.dataset.amount));
+ if (sizeButton) { store.state.selectedSize = sizeButton.dataset.size; renderModal(); }
+ if (colorButton) { store.state.selectedColor = colorButton.dataset.color; renderModal(); }
+ if (modalAddButton) {
+ closeModal();
+ addToCart(modalAddButton.dataset.modalAdd, { size: store.state.selectedSize, color: store.state.selectedColor });
+ }
+
+ if (event.target.closest('[data-open-cart]')) openDrawer(cartDrawer);
+ if (event.target.closest('[data-close-cart]')) closeDrawer(cartDrawer);
+ if (event.target.closest('[data-open-wishlist]')) openDrawer(wishlistDrawer);
+ if (event.target.closest('[data-close-wishlist]')) closeDrawer(wishlistDrawer);
+ if (event.target.closest('[data-close-modal]')) closeModal();
+ if (event.target === cartDrawer) closeDrawer(cartDrawer);
+ if (event.target === wishlistDrawer) closeDrawer(wishlistDrawer);
+ if (event.target.closest('[data-open-featured]')) openProduct('linen-trench');
+});
+
+document.querySelector('[data-sort]').addEventListener('change', (event) => store.set({ sort: event.target.value }));
+document.querySelector('[data-search]').addEventListener('input', (event) => store.set({ search: event.target.value }));
+document.querySelector('[data-view-toggle]').addEventListener('click', (event) => {
+ store.state.compact = !store.state.compact;
+ event.currentTarget.setAttribute('aria-pressed', String(store.state.compact));
+ renderProducts();
+});
+
+document.querySelector('[data-newsletter-form]').addEventListener('submit', async (event) => {
+ event.preventDefault();
+ const email = event.currentTarget.querySelector('input[type="email"]').value;
+ await window.FormaIntegrations?.saveNewsletter?.(email);
+ window.FormaIntegrations?.trackEvent('newsletter_signup', { email });
+ document.querySelector('[data-newsletter-message]').textContent = 'Listo. Te avisaremos del próximo drop privado.';
+});
+
+modal.addEventListener('click', (event) => {
+ const dialogBounds = modal.getBoundingClientRect();
+ const isBackdropClick = event.clientX < dialogBounds.left || event.clientX > dialogBounds.right || event.clientY < dialogBounds.top || event.clientY > dialogBounds.bottom;
+ if (isBackdropClick) closeModal();
+});
+
+try {
+ render();
+ window.FormaIntegrations?.loadChatbase?.();
+ hydrateCatalog();
+} catch (error) {
+ console.error('[FORMA] Runtime error:', error);
+ document.documentElement.dataset.runtimeError = error.message;
+ setRuntimeStatus('La tienda cargó HTML, pero el runtime JS necesita revisión. Revisa la consola.');
+}
diff --git a/docs/GITHUB_PLAIN_DIFFS.md b/docs/GITHUB_PLAIN_DIFFS.md
new file mode 100644
index 0000000..d4aa953
--- /dev/null
+++ b/docs/GITHUB_PLAIN_DIFFS.md
@@ -0,0 +1,34 @@
+# GitHub plain-text patch and diff URLs
+
+Professional review tip: add `.patch` or `.diff` to the end of many GitHub PR, commit and compare URLs to get plain-text output that is easier to inspect, archive, quote and feed into AI agents.
+
+## Examples
+
+```text
+https://github.com/Shopify/Timber/pull/1.patch
+https://github.com/Shopify/Timber/pull/1.diff
+https://github.com/Shopify/Timber/commit/.patch
+https://github.com/Shopify/Timber/compare/main...branch.diff
+```
+
+## Helper script
+
+Use the local helper to normalize a GitHub URL into a plain patch or diff URL:
+
+```bash
+scripts/github-plain-diff.js https://github.com/Shopify/Timber/pull/1 patch
+scripts/github-plain-diff.js https://github.com/Shopify/Timber/pull/1 diff
+```
+
+## Why this helps agents
+
+- Plain text avoids heavy GitHub UI markup.
+- `.patch` includes commit metadata and mail-style patches.
+- `.diff` is compact when you only need file changes.
+- Reviewers can paste exact plain-text URLs into issues, PR comments or AI prompts.
+
+## Suggested workflow
+
+1. Use `.diff` for quick code review.
+2. Use `.patch` when commit metadata matters.
+3. Include one of these URLs in AI-agent tasks when asking for review or implementation follow-up.
diff --git a/docs/GO_LIVE_CHECKLIST.md b/docs/GO_LIVE_CHECKLIST.md
new file mode 100644
index 0000000..87d3fca
--- /dev/null
+++ b/docs/GO_LIVE_CHECKLIST.md
@@ -0,0 +1,93 @@
+# FORMA go-live checklist
+
+If the page exists on GitHub but you do not see the storefront working, the missing step is usually deployment/configuration rather than more files.
+
+## What works immediately
+
+The static demo storefront works with local demo data when served from the repo root:
+
+```bash
+npm ci
+npm run check
+npm run dev
+# open http://127.0.0.1:4173/
+```
+
+You should see:
+
+- Product cards rendered by `assets/store.js`.
+- Category filters and sort controls.
+- Wishlist drawer.
+- Product detail modal.
+- Quick Add and cart drawer.
+- Newsletter confirmation message.
+
+## What does not work just by pushing a branch
+
+Pushing the branch to GitHub does **not** automatically activate these systems:
+
+| Area | Why it still looks inactive | Required next step |
+| --- | --- | --- |
+| Public website | GitHub branch is code storage, not necessarily hosting. | Connect the repo/branch to Vercel or enable GitHub Pages. |
+| Shopify product sync | `assets/config.js` ships blank credentials and `enableRemoteProducts: false`. | Generate runtime config from Vercel env vars or edit demo config locally. |
+| Shopify checkout | The current static demo cart is client-side only. | Connect Storefront Cart API/checkout URL or use the Liquid theme in Shopify. |
+| Supabase events/newsletter | Supabase URL and anon key are blank. | Create project, run migration, add URL/key to runtime config. |
+| Chatbase widget | Chatbase bot ID is blank and `enabled: false`. | Create bot, add bot ID and set enabled true. |
+| Shopify Liquid theme | Files in `shopify-theme/` are not active until uploaded to Shopify. | Upload with Shopify CLI/Theme Kit or copy into a Shopify theme repo. |
+| Ruby Shopify adapter | It needs gems and Shopify Admin env vars. | `cd shopify-ruby`, `bundle install`, fill `.env`, run smoke script. |
+
+## Vercel deployment path
+
+1. Push branch to GitHub.
+2. Import the repo in Vercel.
+3. Select the branch you pushed.
+4. Framework preset: **Other** / static.
+5. Build command: optional `npm run check`.
+6. Output directory: repo root (`.`).
+7. Deploy and open the Vercel preview URL.
+
+## Runtime config path
+
+For production, do not commit secrets to `assets/config.js`. Instead generate the file during deploy or replace placeholders using Vercel environment variables.
+
+Minimum values to turn integrations from demo to connected:
+
+```js
+window.FORMA_CONFIG = {
+ shopify: {
+ domain: 'your-store.myshopify.com',
+ storefrontToken: 'public-storefront-token',
+ apiVersion: '2026-01',
+ enableRemoteProducts: true,
+ },
+ supabase: {
+ url: 'https://your-project.supabase.co',
+ anonKey: 'public-anon-key',
+ eventsTable: 'store_events',
+ newsletterTable: 'newsletter_signups',
+ },
+ chatbase: {
+ botId: 'your-chatbase-bot-id',
+ enabled: true,
+ },
+};
+```
+
+## Shopify theme path
+
+The static Vercel store and the Shopify Liquid theme are separate deliverables:
+
+- Static storefront: repo root `index.html` + `assets/`.
+- Shopify theme: `shopify-theme/`.
+
+To make the Shopify theme visible inside Shopify, upload or connect `shopify-theme/` with Shopify tooling, then preview it from Shopify Admin.
+
+## Quick diagnosis
+
+Run:
+
+```bash
+npm run smoke:static
+```
+
+This checks that `index.html` references the required storefront scripts/styles and that all referenced local assets exist.
diff --git a/docs/INTEGRATIONS.md b/docs/INTEGRATIONS.md
new file mode 100644
index 0000000..6fd7291
--- /dev/null
+++ b/docs/INTEGRATIONS.md
@@ -0,0 +1,106 @@
+# FORMA integration map
+
+This storefront is intentionally static-first so it can ship quickly to Vercel, while still exposing clean hooks for commerce, CRM, support and AI-assisted maintenance.
+
+## Runtime configuration
+
+Edit `assets/config.js` locally for demos, or generate it during Vercel builds from environment variables. Never commit real production tokens.
+
+| Area | Config keys | Purpose |
+| --- | --- | --- |
+| Shopify | `shopify.domain`, `shopify.storefrontToken`, `shopify.apiVersion`, `shopify.enableRemoteProducts` | Reads products and variants through the Shopify Storefront API. |
+| Supabase | `supabase.url`, `supabase.anonKey`, `supabase.eventsTable`, `supabase.newsletterTable` | Stores storefront events and newsletter signups. |
+| Chatbase | `chatbase.botId`, `chatbase.enabled` | Loads the Chatbase support widget only when enabled. |
+| GitHub AI | `githubAgent.provider`, `githubAgent.workflow` | Documents the expected Claude-style GitHub agent workflow. |
+
+## Shopify path
+
+1. Create a Shopify custom app with Storefront API access.
+2. Enable product read permissions and copy the Storefront access token.
+3. Set `shopify.domain` to the shop domain, for example `forma.myshopify.com`.
+4. Set `shopify.enableRemoteProducts` to `true`.
+5. The storefront falls back to the local demo catalog if Shopify is not configured or fails.
+
+
+
+## Shopify Liquid theme scaffold
+
+The `shopify-theme/` directory mirrors the classic Timber-style theme structure requested for Shopify theme work:
+
+- `assets/` for JavaScript, CSS and theme images.
+- `layout/theme.liquid` as the base HTML document.
+- `snippets/` for reusable Liquid components.
+- `templates/` for Shopify page types, including cart, collection, product, search, blog and customer templates.
+- `spec/` for a structure smoke test plus Timber-style i18n/HTML helper specs using Nokogiri, HTMLEntities and RSpec.
+- `config.yml` as a legacy Theme Kit style placeholder.
+
+Validate it with:
+
+```bash
+ruby shopify-theme/spec/theme_structure_spec.rb
+```
+
+Optional full theme specs after installing Ruby gems:
+
+```bash
+cd shopify-theme
+bundle install
+bundle exec rspec
+```
+
+This is a migration scaffold inspired by Timber, not a vendored copy of Timber. It includes Timber-style files such as `timber.js.liquid`, `ajax-cart.js.liquid`, `breadcrumb`, `collection-sorting`, onboarding snippets, `comment`, `oldIE-js`, and `gift_card.liquid`.
+
+## Shopify Ruby Admin adapter
+
+Reference repository: https://github.com/Shopify/shopify-api-ruby
+
+If you want to contribute changes back to the upstream Shopify Ruby API gem, use `docs/UPSTREAM_SHOPIFY_RUBY.md` and `scripts/prepare-shopify-upstream.sh` to prepare a clean fork-based workspace.
+
+The `shopify-ruby/` folder contains the FORMA-side Ruby adapter for secure Shopify Admin API operations. Use it when you need server-side tasks such as product syncs, order operations, inventory workflows or private Admin GraphQL calls that must not run in the browser.
+
+Quick start:
+
+```bash
+cd shopify-ruby
+cp .env.example .env
+bundle install
+SHOPIFY_PRODUCT_LIMIT=5 bundle exec ruby scripts/list_products.rb
+```
+
+The static Vercel storefront can keep using `assets/integrations.js` for safe Storefront/demo behavior, while this Ruby adapter handles private Admin API work.
+
+## Supabase path
+
+1. Create a Supabase project.
+2. Run `supabase/migrations/202605130001_storefront_core.sql`.
+3. Put the project URL and anon key into runtime config.
+4. The storefront will insert:
+ - `cart_add` events into `store_events`.
+ - `newsletter_signup` rows into `newsletter_signups`.
+
+## Chatbase path
+
+Reference: https://www.chatbase.co/docs/developer-guides/javascript-embed
+
+1. Create a Chatbase bot trained with product, shipping, return and brand FAQs.
+2. Copy the bot ID into `chatbase.botId`.
+3. Set `chatbase.enabled` to `true`.
+4. The widget script is injected at runtime by `assets/integrations.js`.
+
+## GitHub AI agent workflow
+
+Use Claude, Copilot Workspace or another GitHub agent with this loop:
+
+1. Create an issue using `.github/ISSUE_TEMPLATE/ai-agent-task.md`.
+2. Ask the agent to create a branch from the issue.
+3. Require a PR with screenshots or Vercel preview evidence for UI changes.
+4. Review the PR using the checklist in `.github/pull_request_template.md`.
+5. Merge only after `npm run check` and Vercel preview pass.
+
+## Vercel path
+
+1. Import the GitHub repo into Vercel.
+2. Use no build command for the static storefront, or set `npm run check` as an optional preflight in CI.
+3. Output directory should be the repo root.
+4. Add production env vars if generating `assets/config.js` during deploy.
+5. Vercel previews should be used for every GitHub PR.
diff --git a/docs/TURBOREPO.md b/docs/TURBOREPO.md
new file mode 100644
index 0000000..b6378ad
--- /dev/null
+++ b/docs/TURBOREPO.md
@@ -0,0 +1,82 @@
+# Turborepo setup for FORMA
+
+This repo now includes a minimal `turbo.json` so the existing storefront checks can be run through Turborepo when `turbo` is installed.
+
+Official installation reference reviewed:
+
+## Quick start options
+
+Create a new Turborepo starter elsewhere:
+
+```bash
+pnpm dlx create-turbo@latest
+yarn dlx create-turbo@latest
+npx create-turbo@latest
+bunx create-turbo@latest
+```
+
+Install Turbo globally for this existing repo:
+
+```bash
+pnpm add turbo --global
+yarn global add turbo
+npm install turbo --global
+bun install turbo --global
+```
+
+The official docs recommend installing `turbo` globally for convenient terminal usage and locally in a repo for stable team workflows. In this environment, installing the npm package returned HTTP 403, so this repo keeps the Turborepo config committed while leaving package installation to your machine/CI network.
+
+## Commands in this repo
+
+Without Turbo:
+
+```bash
+npm run check
+npm run smoke:static
+npm run dev
+```
+
+With Turbo installed:
+
+```bash
+turbo run check
+turbo run smoke:static
+turbo run verify:files
+```
+
+Check whether Turbo is available:
+
+```bash
+npm run turbo:doctor
+```
+
+## Why this is not a full create-turbo rewrite
+
+`create-turbo` creates a new monorepo with multiple apps and packages. FORMA already has a static storefront, Shopify Liquid theme scaffold, Shopify Ruby adapter and deployment docs, so this change adds Turborepo orchestration without moving files or breaking the current Vercel/static workflow.
+
+
+## Starter-compatible monorepo shape
+
+The official starter describes two deployable applications and three shared libraries. FORMA now mirrors that shape while preserving the existing static root storefront:
+
+```text
+apps/
+ storefront/ # deployable static storefront app
+ docs/ # deployable static docs app
+packages/
+ ui/ # shared HTML/UI helpers
+ config/ # shared brand/deployment config
+ storefront-data/ # shared product/category records
+```
+
+Useful commands:
+
+```bash
+npm run check:workspaces
+npm run build
+turbo run build
+turbo build --filter=@forma/app-docs --dry
+cd apps/docs && turbo build
+```
+
+`turbo` is an alias for `turbo run`, so `turbo build` and `turbo run build` are equivalent.
diff --git a/docs/UPSTREAM_SHOPIFY_RUBY.md b/docs/UPSTREAM_SHOPIFY_RUBY.md
new file mode 100644
index 0000000..7c6fd14
--- /dev/null
+++ b/docs/UPSTREAM_SHOPIFY_RUBY.md
@@ -0,0 +1,76 @@
+# Contributing upstream to Shopify/shopify-api-ruby
+
+Yes — this project is prepared to help create focused upstream contributions for `Shopify/shopify-api-ruby`.
+
+Upstream repository:
+
+## What I can do from this workspace
+
+- Inspect the FORMA Shopify adapter and identify issues that may belong upstream.
+- Create a clean reproduction script or failing test against `shopify_api`.
+- Prepare a patch branch in a local clone of `Shopify/shopify-api-ruby` or your fork.
+- Draft the upstream PR title, body, test evidence and migration notes.
+
+## What I need from you before opening an upstream PR
+
+1. Your GitHub fork URL, for example:
+ - `git@github.com:/shopify-api-ruby.git`
+ - or `https://github.com//shopify-api-ruby.git`
+2. The issue or improvement you want to contribute.
+3. Confirmation that I can work in a sibling directory outside this storefront repo.
+4. Any Shopify app/test-store context needed to reproduce the issue, without sharing secrets in git.
+
+## Recommended upstream workflow
+
+```bash
+# From this repo root
+scripts/prepare-shopify-upstream.sh git@github.com:/shopify-api-ruby.git
+
+cd ../shopify-api-ruby-upstream
+git checkout -b forma/
+bundle install
+bundle exec rake test
+```
+
+Then implement the smallest possible change, add/update tests, run the project checks and push to your fork:
+
+```bash
+git push -u fork forma/
+```
+
+Open the PR against `Shopify/shopify-api-ruby:main`.
+
+
+## Plain-text GitHub review URLs
+
+When reviewing upstream Shopify changes, append `.patch` or `.diff` to GitHub PR, commit or compare URLs. See `docs/GITHUB_PLAIN_DIFFS.md` or run:
+
+```bash
+scripts/github-plain-diff.js https://github.com/Shopify/shopify-api-ruby/pull/ patch
+```
+
+## PR quality checklist
+
+- [ ] The PR solves one clear upstream problem.
+- [ ] The PR includes a failing test or regression coverage when possible.
+- [ ] The PR does not include FORMA-specific product/business code.
+- [ ] The PR does not include secrets, store domains, tokens or private payloads.
+- [ ] The PR body includes reproduction steps and test output.
+- [ ] The PR follows the upstream repository style instead of this storefront style.
+
+## Good contribution candidates
+
+- Documentation improvements discovered while wiring `ShopifyAPI::Context`.
+- Clear examples for Admin GraphQL or Storefront GraphQL clients.
+- Better error messages or guards around configuration.
+- Test coverage for client behavior that is currently unclear.
+
+## Not good upstream candidates
+
+- FORMA storefront UI, catalog data, Supabase integration or Chatbase integration.
+- Private Shopify app credentials or store-specific behavior.
+- Large refactors without an issue or maintainer alignment.
+
+## Current environment note
+
+This container could not read the upstream Git remote directly because the network tunnel returned HTTP 403. If you provide a fork and the environment allows SSH/HTTPS Git access, the helper script can prepare the workspace. If not, I can still draft patches and PR text for you to apply locally.
diff --git a/index.html b/index.html
index ddfc503..e0149b1 100644
--- a/index.html
+++ b/index.html
@@ -1,18 +1,248 @@
-
-
-
-
-
-
- FEISS Gaming — Perifericos Premium
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+ FORMA — Tienda premium de esenciales modernos
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Nueva campaña · Primavera urbana
+
Esenciales con forma, intención y carácter.
+
+ Compra una selección curada de moda, accesorios y objetos lifestyle con estética editorial, materiales premium y experiencia de compra sin fricción.
+
+
+
+
24h despacho
+
+18k clientes
+
4.9 rating
+
+
+
+
+
+ Drop 04
+ Minimal Utility
+
+
+
+
+
+ 🚚 Envío gratis En pedidos desde $100.
+ ↩ Devoluciones fáciles 30 días para cambios.
+ ✦ Garantía premium Materiales verificados.
+ ◌ Soporte humano Asesoría de estilo real.
+
+
+
+
+
Catálogo
+
Compra por categoría
+
Filtros rápidos, ordenamiento inteligente, vista compacta y tarjetas listas para conversión.
+
+
+
+
+
+
+
+
Trench Lino Arena ★ 4.9 Trench liviano de lino premium con caída relajada.
$148.00 Prendas
+
+
+
+
Bolso Arc Cognac ★ 4.8 Bolso estructurado con asa curva y cierre magnético.
$96.00 Bolsos
+
+
+
+
Set Rib Studio ★ 4.7 Conjunto ribbed de dos piezas con textura suave.
$82.00 Prendas
+
+
+
+
Jarrón Forma 02 ★ 4.9 Pieza cerámica hecha a mano con silueta orgánica.
$58.00 Hogar
+
+
+
+
Pañuelo Seda Grid ★ 4.6 Pañuelo de seda con patrón geométrico satinado.
$44.00 Accesorios
+
+
+
+
Cinturón Nudo ★ 4.7 Cinturón en cuero vegetal con hebilla escultural.
$52.00 Accesorios
+
+
+
+
Tote Weekend ★ 4.8 Tote amplio de canvas encerado con correas de cuero.
$112.00 Bolsos
+
+
+
+
Vela Santal 300g ★ 4.5 Vela aromática de santal, ámbar y cedro.
$38.00 Hogar
+
+
+ No encontramos productos con esa búsqueda.
+ Catálogo HTML cargado. El JavaScript activará filtros, carrito, wishlist y modal al terminar de cargar.
+
+
+
+
+
Colección cápsula
+
Diseñada para combinar sin pensar.
+
+ Piezas neutras, siluetas limpias y acentos de textura para elevar outfits diarios, viajes cortos y espacios personales.
+
+
+
+
+
+
+
+
+
+
+
+
Operación conectada
+
Lista para Shopify, Supabase, Chatbase, GitHub AI y Vercel.
+
+ La tienda queda organizada para usar Shopify como catálogo y checkout, Supabase como CRM/eventos, Chatbase como asistente, GitHub con agente IA tipo Claude para PRs y Vercel para previews y producción.
+
+
+
+
Shopify Modo demo Storefront API para productos y variantes.
+
Supabase Modo demo Eventos, newsletter y base operativa.
+
Chatbase Modo demo Chatbot de soporte cargado por configuración.
+
GitHub AI Claude-ready Flujo de issues, ramas, PRs y revisión.
+
Vercel Preview-ready Deploy estático con headers y rewrites.
+
+
+
+
+
+
+
+
+
Agrega productos para desbloquear envío gratis.
+
+
+
+
+
+
+
+
+
+
+ ×
+
+
+
+
+
+
diff --git a/package-lock.json b/package-lock.json
index af2503e..a6af0e6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,65 @@
{
- "name": "project",
+ "name": "forma-storefront",
+ "version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
- "packages": {}
+ "packages": {
+ "": {
+ "name": "forma-storefront",
+ "version": "1.0.0",
+ "workspaces": [
+ "apps/*",
+ "packages/*"
+ ]
+ },
+ "apps/docs": {
+ "name": "@forma/app-docs",
+ "version": "1.0.0",
+ "dependencies": {
+ "@forma/config": "*",
+ "@forma/ui": "*"
+ }
+ },
+ "apps/storefront": {
+ "name": "@forma/app-storefront",
+ "version": "1.0.0",
+ "dependencies": {
+ "@forma/config": "*",
+ "@forma/storefront-data": "*",
+ "@forma/ui": "*"
+ }
+ },
+ "node_modules/@forma/app-docs": {
+ "resolved": "apps/docs",
+ "link": true
+ },
+ "node_modules/@forma/app-storefront": {
+ "resolved": "apps/storefront",
+ "link": true
+ },
+ "node_modules/@forma/config": {
+ "resolved": "packages/config",
+ "link": true
+ },
+ "node_modules/@forma/storefront-data": {
+ "resolved": "packages/storefront-data",
+ "link": true
+ },
+ "node_modules/@forma/ui": {
+ "resolved": "packages/ui",
+ "link": true
+ },
+ "packages/config": {
+ "name": "@forma/config",
+ "version": "1.0.0"
+ },
+ "packages/storefront-data": {
+ "name": "@forma/storefront-data",
+ "version": "1.0.0"
+ },
+ "packages/ui": {
+ "name": "@forma/ui",
+ "version": "1.0.0"
+ }
+ }
}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..ceb4ac1
--- /dev/null
+++ b/package.json
@@ -0,0 +1,31 @@
+{
+ "name": "forma-storefront",
+ "version": "1.0.0",
+ "private": true,
+ "description": "FORMA editorial ecommerce storefront with Shopify, Supabase, Chatbase and Vercel integration hooks.",
+ "scripts": {
+ "dev": "python3 -m http.server 4173",
+ "check": "npm run verify:files && npm run smoke:static && npm run check:workspaces && node --check assets/integrations.js && node --check assets/store.js && node --check scripts/verify-files.js && node --check scripts/smoke-static.js && node --check scripts/smoke-app.js && ruby shopify-theme/spec/theme_structure_spec.rb",
+ "start": "python3 -m http.server ${PORT:-4173}",
+ "verify:files": "node scripts/verify-files.js",
+ "theme:spec": "cd shopify-theme && bundle exec rspec",
+ "smoke:static": "node scripts/smoke-static.js",
+ "turbo:doctor": "node scripts/turbo-doctor.js",
+ "turbo:check": "turbo run check",
+ "turbo:smoke": "turbo run smoke:static",
+ "build": "npm run --workspaces --if-present build",
+ "check:workspaces": "npm run --workspaces --if-present check"
+ },
+ "keywords": [
+ "ecommerce",
+ "shopify",
+ "supabase",
+ "chatbase",
+ "vercel"
+ ],
+ "packageManager": "npm@11.4.2",
+ "workspaces": [
+ "apps/*",
+ "packages/*"
+ ]
+}
diff --git a/packages/config/index.js b/packages/config/index.js
new file mode 100644
index 0000000..3c45576
--- /dev/null
+++ b/packages/config/index.js
@@ -0,0 +1,11 @@
+export const brand = {
+ name: 'FORMA',
+ tagline: 'Esenciales con forma, intención y carácter.',
+ supportEmail: 'help@forma.store',
+};
+
+export const deployment = {
+ staticRoot: '.',
+ storefrontApp: 'apps/storefront',
+ docsApp: 'apps/docs',
+};
diff --git a/packages/config/package.json b/packages/config/package.json
new file mode 100644
index 0000000..e47099c
--- /dev/null
+++ b/packages/config/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "@forma/config",
+ "version": "1.0.0",
+ "private": true,
+ "type": "module",
+ "main": "index.js",
+ "scripts": {
+ "build": "node --check index.js",
+ "check": "node --check index.js"
+ }
+}
diff --git a/packages/storefront-data/index.js b/packages/storefront-data/index.js
new file mode 100644
index 0000000..d59c37c
--- /dev/null
+++ b/packages/storefront-data/index.js
@@ -0,0 +1,9 @@
+export const products = [
+ { id: 'linen-trench', name: 'Trench Lino Arena', category: 'Prendas', price: 148 },
+ { id: 'arc-bag', name: 'Bolso Arc Cognac', category: 'Bolsos', price: 96 },
+ { id: 'silk-scarf', name: 'Pañuelo Seda Grid', category: 'Accesorios', price: 44 },
+ { id: 'weekend-tote', name: 'Tote Weekend', category: 'Bolsos', price: 112 },
+ { id: 'scent-candle', name: 'Vela Santal 300g', category: 'Hogar', price: 38 },
+];
+
+export const categories = [...new Set(products.map((product) => product.category))];
diff --git a/packages/storefront-data/package.json b/packages/storefront-data/package.json
new file mode 100644
index 0000000..73e69f9
--- /dev/null
+++ b/packages/storefront-data/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "@forma/storefront-data",
+ "version": "1.0.0",
+ "private": true,
+ "type": "module",
+ "main": "index.js",
+ "scripts": {
+ "build": "node --check index.js",
+ "check": "node --check index.js"
+ }
+}
diff --git a/packages/ui/index.js b/packages/ui/index.js
new file mode 100644
index 0000000..e048d65
--- /dev/null
+++ b/packages/ui/index.js
@@ -0,0 +1,7 @@
+export function productCard(product) {
+ return `${product.name} ${product.category} $${product.price} `;
+}
+
+export function pageShell({ title, body }) {
+ return `${title} ${body}`;
+}
diff --git a/packages/ui/package.json b/packages/ui/package.json
new file mode 100644
index 0000000..3aaa07c
--- /dev/null
+++ b/packages/ui/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "@forma/ui",
+ "version": "1.0.0",
+ "private": true,
+ "type": "module",
+ "main": "index.js",
+ "scripts": {
+ "build": "node --check index.js",
+ "check": "node --check index.js"
+ }
+}
diff --git a/scripts/github-plain-diff.js b/scripts/github-plain-diff.js
new file mode 100755
index 0000000..bd53aa8
--- /dev/null
+++ b/scripts/github-plain-diff.js
@@ -0,0 +1,30 @@
+#!/usr/bin/env node
+
+const input = process.argv[2];
+const format = process.argv[3] || 'patch';
+
+if (!input || !['patch', 'diff'].includes(format)) {
+ console.error('Usage: scripts/github-plain-diff.js [patch|diff]');
+ console.error('Example: scripts/github-plain-diff.js https://github.com/Shopify/Timber/pull/1 patch');
+ process.exit(64);
+}
+
+let url;
+try {
+ url = new URL(input);
+} catch (error) {
+ console.error(`Invalid URL: ${input}`);
+ process.exit(65);
+}
+
+if (url.hostname !== 'github.com') {
+ console.error('Only github.com URLs are supported.');
+ process.exit(66);
+}
+
+url.hash = '';
+url.search = '';
+url.pathname = url.pathname.replace(/\.(patch|diff)$/i, '');
+url.pathname = `${url.pathname}.${format}`;
+
+console.log(url.toString());
diff --git a/scripts/prepare-shopify-upstream.sh b/scripts/prepare-shopify-upstream.sh
new file mode 100755
index 0000000..e5b6288
--- /dev/null
+++ b/scripts/prepare-shopify-upstream.sh
@@ -0,0 +1,53 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+FORK_URL="${1:-}"
+TARGET_DIR="${2:-../shopify-api-ruby-upstream}"
+UPSTREAM_URL="git@github.com:Shopify/shopify-api-ruby.git"
+
+if [[ -z "${FORK_URL}" ]]; then
+ cat >&2 <<'USAGE'
+Usage:
+ scripts/prepare-shopify-upstream.sh [target-dir]
+
+Example:
+ scripts/prepare-shopify-upstream.sh git@github.com:your-user/shopify-api-ruby.git
+USAGE
+ exit 64
+fi
+
+if [[ -e "${TARGET_DIR}" && ! -d "${TARGET_DIR}/.git" ]]; then
+ echo "Target exists but is not a git repository: ${TARGET_DIR}" >&2
+ exit 65
+fi
+
+if [[ ! -d "${TARGET_DIR}/.git" ]]; then
+ git clone "${FORK_URL}" "${TARGET_DIR}"
+fi
+
+cd "${TARGET_DIR}"
+
+if ! git remote get-url upstream >/dev/null 2>&1; then
+ git remote add upstream "${UPSTREAM_URL}"
+fi
+
+if ! git remote get-url fork >/dev/null 2>&1; then
+ git remote add fork "${FORK_URL}"
+fi
+
+git fetch upstream main || git fetch origin main
+git checkout main
+git pull --ff-only upstream main || git pull --ff-only origin main
+
+cat </dev/null || true)
+ Fork: $(git remote get-url fork 2>/dev/null || true)
+
+Next steps:
+ cd ${TARGET_DIR}
+ git checkout -b forma/
+ bundle install
+ bundle exec rake test
+SUMMARY
diff --git a/scripts/smoke-app.js b/scripts/smoke-app.js
new file mode 100755
index 0000000..29d7e6d
--- /dev/null
+++ b/scripts/smoke-app.js
@@ -0,0 +1,27 @@
+const fs = require('fs');
+const path = require('path');
+
+const appDir = process.argv[2];
+if (!appDir) {
+ console.error('Usage: node scripts/smoke-app.js ');
+ process.exit(64);
+}
+
+const indexPath = path.join(process.cwd(), appDir, 'index.html');
+const pkgPath = path.join(process.cwd(), appDir, 'package.json');
+
+for (const file of [indexPath, pkgPath]) {
+ if (!fs.existsSync(file) || fs.readFileSync(file, 'utf8').trim().length === 0) {
+ console.error(`Missing or empty app file: ${file}`);
+ process.exit(1);
+ }
+}
+
+const html = fs.readFileSync(indexPath, 'utf8');
+const missing = ['', ' !html.includes(marker));
+if (missing.length) {
+ console.error(`App ${appDir} is missing markers: ${missing.join(', ')}`);
+ process.exit(1);
+}
+
+console.log(`Verified deployable app: ${appDir}`);
diff --git a/scripts/smoke-static.js b/scripts/smoke-static.js
new file mode 100644
index 0000000..0a886b7
--- /dev/null
+++ b/scripts/smoke-static.js
@@ -0,0 +1,55 @@
+const fs = require('fs');
+const path = require('path');
+
+const root = process.cwd();
+const htmlPath = path.join(root, 'index.html');
+const html = fs.readFileSync(htmlPath, 'utf8');
+
+const requiredMarkers = [
+ 'assets/config.js',
+ 'assets/integrations.js',
+ 'assets/store.js',
+ 'assets/store.css',
+ 'data-product-grid',
+ 'data-cart-drawer',
+ 'data-wishlist-drawer',
+ 'data-product-modal',
+ 'data-integration-status="shopify"',
+ 'data-integration-status="supabase"',
+ 'data-integration-status="chatbase"',
+ 'Bolso Arc Cognac',
+ 'Vela Santal 300g',
+ 'Tote Weekend',
+ 'Pañuelo Seda Grid',
+ 'Trench Lino Arena',
+ 'data-runtime-status',
+];
+
+const missingMarkers = requiredMarkers.filter((marker) => !html.includes(marker));
+if (missingMarkers.length) {
+ console.error(`Missing required storefront markers:\n${missingMarkers.map((marker) => `- ${marker}`).join('\n')}`);
+ process.exit(1);
+}
+
+const assetRefs = [...html.matchAll(/(?:src|href)="(assets\/[^"]+)"/g)].map((match) => match[1]);
+const missingAssets = assetRefs.filter((asset) => !fs.existsSync(path.join(root, asset)));
+if (missingAssets.length) {
+ console.error(`Missing referenced assets:\n${missingAssets.map((asset) => `- ${asset}`).join('\n')}`);
+ process.exit(1);
+}
+
+const config = fs.readFileSync(path.join(root, 'assets/config.js'), 'utf8');
+const demoMode = [
+ "domain: ''",
+ "storefrontToken: ''",
+ 'enableRemoteProducts: false',
+ "url: ''",
+ "anonKey: ''",
+ "botId: ''",
+ 'enabled: false',
+].every((marker) => config.includes(marker));
+
+console.log(`Verified ${assetRefs.length} local asset references and ${requiredMarkers.length} storefront markers.`);
+console.log(demoMode
+ ? 'Runtime integrations are intentionally in demo mode. Configure assets/config.js or deploy-time env generation for Shopify/Supabase/Chatbase.'
+ : 'Runtime integration config appears customized.');
diff --git a/scripts/turbo-doctor.js b/scripts/turbo-doctor.js
new file mode 100755
index 0000000..ebe79cd
--- /dev/null
+++ b/scripts/turbo-doctor.js
@@ -0,0 +1,27 @@
+#!/usr/bin/env node
+const { spawnSync } = require('child_process');
+
+const result = spawnSync('turbo', ['--version'], { encoding: 'utf8' });
+
+if (result.error) {
+ console.log('Turbo is not installed in this environment.');
+ console.log('Install globally with one of:');
+ console.log(' pnpm add turbo --global');
+ console.log(' yarn global add turbo');
+ console.log(' npm install turbo --global');
+ console.log(' bun install turbo --global');
+ console.log('Or scaffold a new starter with:');
+ console.log(' pnpm dlx create-turbo@latest');
+ console.log(' yarn dlx create-turbo@latest');
+ console.log(' npx create-turbo@latest');
+ console.log(' bunx create-turbo@latest');
+ process.exit(0);
+}
+
+if (result.status !== 0) {
+ process.stderr.write(result.stderr || result.stdout);
+ process.exit(result.status || 1);
+}
+
+console.log(`Turbo installed: ${result.stdout.trim()}`);
+console.log('Try: turbo run check');
diff --git a/scripts/verify-files.js b/scripts/verify-files.js
new file mode 100644
index 0000000..85d8873
--- /dev/null
+++ b/scripts/verify-files.js
@@ -0,0 +1,106 @@
+const fs = require('fs');
+const path = require('path');
+
+const requiredFiles = [
+ 'README.md',
+ 'package.json',
+ 'scripts/turbo-doctor.js',
+ 'docs/TURBOREPO.md',
+ 'turbo.json',
+ 'index.html',
+ 'assets/config.js',
+ 'assets/integrations.js',
+ 'assets/store.css',
+ 'assets/store.js',
+ 'docs/INTEGRATIONS.md',
+ 'supabase/migrations/202605130001_storefront_core.sql',
+ 'vercel.json',
+ 'CLAUDE.md',
+ '.github/workflows/check.yml',
+ '.github/pull_request_template.md',
+ '.github/ISSUE_TEMPLATE/ai-agent-task.md',
+ 'shopify-ruby/Gemfile',
+ 'shopify-ruby/.env.example',
+ 'shopify-ruby/config/shopify_context.rb',
+ 'shopify-ruby/lib/forma_shopify/admin_client.rb',
+ 'shopify-ruby/lib/forma_shopify/storefront_client.rb',
+ 'shopify-ruby/scripts/list_products.rb',
+ 'shopify-ruby/README.md',
+ 'docs/UPSTREAM_SHOPIFY_RUBY.md',
+ 'scripts/prepare-shopify-upstream.sh',
+ 'scripts/github-plain-diff.js',
+ 'docs/GITHUB_PLAIN_DIFFS.md',
+ 'scripts/smoke-static.js',
+ 'scripts/smoke-app.js',
+ 'packages/storefront-data/package.json',
+ 'packages/storefront-data/index.js',
+ 'packages/config/package.json',
+ 'packages/config/index.js',
+ 'packages/ui/package.json',
+ 'packages/ui/index.js',
+ 'apps/docs/package.json',
+ 'apps/docs/index.html',
+ 'apps/storefront/package.json',
+ 'apps/storefront/index.html',
+ 'docs/GO_LIVE_CHECKLIST.md',
+
+ 'shopify-theme/README.md',
+ 'shopify-theme/Gemfile',
+ 'shopify-theme/spec/spec_helper.rb',
+ 'shopify-theme/spec/helpers/html_helper.rb',
+ 'shopify-theme/spec/helpers/i18n_helper.rb',
+ 'shopify-theme/spec/i18n_validity_spec.rb',
+ 'shopify-theme/spec/html_validity_spec.rb',
+ 'shopify-theme/locales/en.default.json',
+ 'shopify-theme/locales/es.default.json',
+ 'shopify-theme/assets/forma-theme.css.liquid',
+ 'shopify-theme/assets/forma-theme.js',
+ 'shopify-theme/assets/timber.js.liquid',
+ 'shopify-theme/assets/ajax-cart.js.liquid',
+ 'shopify-theme/assets/gift-card.scss.liquid',
+ 'shopify-theme/snippets/breadcrumb.liquid',
+ 'shopify-theme/snippets/collection-sorting.liquid',
+ 'shopify-theme/snippets/onboarding-empty-collection.liquid',
+ 'shopify-theme/snippets/onboarding-featured-collections.liquid',
+ 'shopify-theme/snippets/comment.liquid',
+ 'shopify-theme/snippets/oldIE-js.liquid',
+ 'shopify-theme/snippets/ajax-cart-template.liquid',
+ 'shopify-theme/templates/gift_card.liquid',
+ 'shopify-theme/layout/theme.liquid',
+ 'shopify-theme/snippets/site-header.liquid',
+ 'shopify-theme/snippets/site-footer.liquid',
+ 'shopify-theme/snippets/product-card.liquid',
+ 'shopify-theme/spec/theme_structure_spec.rb',
+ 'shopify-theme/templates/404.liquid',
+ 'shopify-theme/templates/article.liquid',
+ 'shopify-theme/templates/blog.liquid',
+ 'shopify-theme/templates/cart.liquid',
+ 'shopify-theme/templates/collection.liquid',
+ 'shopify-theme/templates/collection.list.liquid',
+ 'shopify-theme/templates/index.liquid',
+ 'shopify-theme/templates/list-collections.liquid',
+ 'shopify-theme/templates/page.contact.liquid',
+ 'shopify-theme/templates/page.liquid',
+ 'shopify-theme/templates/product.liquid',
+ 'shopify-theme/templates/search.liquid',
+ 'shopify-theme/templates/customers/account.liquid',
+ 'shopify-theme/templates/customers/addresses.liquid',
+ 'shopify-theme/templates/customers/login.liquid',
+ 'shopify-theme/templates/customers/order.liquid',
+ 'shopify-theme/templates/customers/register.liquid',
+ 'shopify-theme/config.yml',
+ 'shopify-theme/config/settings_schema.json',
+ 'shopify-theme/config/settings_data.json',
+];
+
+const emptyFiles = requiredFiles.filter((file) => {
+ const fullPath = path.join(process.cwd(), file);
+ return !fs.existsSync(fullPath) || fs.readFileSync(fullPath, 'utf8').trim().length === 0;
+});
+
+if (emptyFiles.length) {
+ console.error(`Empty or missing required files:\n${emptyFiles.map((file) => `- ${file}`).join('\n')}`);
+ process.exit(1);
+}
+
+console.log(`Verified ${requiredFiles.length} required files contain content.`);
diff --git a/shopify-ruby/.env.example b/shopify-ruby/.env.example
new file mode 100644
index 0000000..a53f87f
--- /dev/null
+++ b/shopify-ruby/.env.example
@@ -0,0 +1,11 @@
+# Shopify Admin API custom app credentials.
+SHOPIFY_API_KEY=
+SHOPIFY_API_SECRET=
+SHOPIFY_ADMIN_ACCESS_TOKEN=
+SHOPIFY_SHOP_DOMAIN=your-store.myshopify.com
+SHOPIFY_API_VERSION=2026-04
+SHOPIFY_SCOPES=read_products,write_products,read_orders
+
+# Optional Storefront API token for public catalog queries.
+SHOPIFY_STOREFRONT_PUBLIC_TOKEN=
+SHOPIFY_STOREFRONT_PRIVATE_TOKEN=
diff --git a/shopify-ruby/Gemfile b/shopify-ruby/Gemfile
new file mode 100644
index 0000000..8e2ca87
--- /dev/null
+++ b/shopify-ruby/Gemfile
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+source "https://rubygems.org"
+
+gem "shopify_api"
+gem "dotenv"
diff --git a/shopify-ruby/README.md b/shopify-ruby/README.md
new file mode 100644
index 0000000..335011f
--- /dev/null
+++ b/shopify-ruby/README.md
@@ -0,0 +1,40 @@
+# FORMA Shopify Ruby scaffold
+
+This folder is a small adapter for the official Shopify Ruby API gem from `git@github.com:Shopify/shopify-api-ruby.git` / `https://github.com/Shopify/shopify-api-ruby`.
+
+It is not a vendored copy of Shopify's repository. It is the app-side code FORMA needs to organize Shopify Admin and Storefront API access while the storefront remains deployable on Vercel.
+
+## What it includes
+
+- `Gemfile` with `shopify_api` and `dotenv`.
+- `config/shopify_context.rb` to initialize `ShopifyAPI::Context` with environment variables.
+- `lib/forma_shopify/admin_client.rb` for GraphQL Admin API product reads.
+- `lib/forma_shopify/storefront_client.rb` for Storefront API product reads.
+- `scripts/list_products.rb` as a smoke test for Admin API credentials.
+
+## Setup
+
+```bash
+cd shopify-ruby
+cp .env.example .env
+bundle install
+SHOPIFY_PRODUCT_LIMIT=5 bundle exec ruby scripts/list_products.rb
+```
+
+## Required environment variables
+
+| Variable | Purpose |
+| --- | --- |
+| `SHOPIFY_API_KEY` | Custom app API key. |
+| `SHOPIFY_API_SECRET` | Custom app API secret. |
+| `SHOPIFY_ADMIN_ACCESS_TOKEN` | Admin API access token for GraphQL Admin calls. |
+| `SHOPIFY_SHOP_DOMAIN` | Shop domain, for example `your-store.myshopify.com`. |
+| `SHOPIFY_API_VERSION` | Admin API version, default `2026-04`. |
+| `SHOPIFY_SCOPES` | Comma-separated app scopes. |
+| `SHOPIFY_STOREFRONT_PUBLIC_TOKEN` / `SHOPIFY_STOREFRONT_PRIVATE_TOKEN` | Optional Storefront API tokens. |
+
+## Notes
+
+- New Shopify apps should prefer GraphQL Admin API over REST.
+- Do not commit `.env` or production tokens.
+- Use this Ruby adapter for secure server-side Admin API work, and keep `assets/integrations.js` limited to safe browser-side Storefront API/demo behavior.
diff --git a/shopify-ruby/config/shopify_context.rb b/shopify-ruby/config/shopify_context.rb
new file mode 100644
index 0000000..3814d75
--- /dev/null
+++ b/shopify-ruby/config/shopify_context.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require "shopify_api"
+
+module FormaShopify
+ module Context
+ module_function
+
+ def setup!
+ ShopifyAPI::Context.setup(
+ api_key: env!("SHOPIFY_API_KEY"),
+ api_secret_key: env!("SHOPIFY_API_SECRET"),
+ host_name: env!("SHOPIFY_SHOP_DOMAIN"),
+ scope: ENV.fetch("SHOPIFY_SCOPES", "read_products"),
+ is_embedded: false,
+ is_private: false,
+ api_version: ENV.fetch("SHOPIFY_API_VERSION", "2026-04"),
+ rest_disabled: true
+ )
+ end
+
+ def env!(key)
+ value = ENV[key]
+ return value unless value.nil? || value.strip.empty?
+
+ raise KeyError, "Missing required environment variable: #{key}"
+ end
+ end
+end
diff --git a/shopify-ruby/lib/forma_shopify/admin_client.rb b/shopify-ruby/lib/forma_shopify/admin_client.rb
new file mode 100644
index 0000000..dad5f61
--- /dev/null
+++ b/shopify-ruby/lib/forma_shopify/admin_client.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+require "shopify_api"
+
+module FormaShopify
+ class AdminClient
+ PRODUCTS_QUERY = <<~GRAPHQL
+ query FormaProducts($first: Int!) {
+ products(first: $first, sortKey: CREATED_AT, reverse: true) {
+ edges {
+ node {
+ id
+ title
+ handle
+ productType
+ status
+ createdAt
+ onlineStoreUrl
+ featuredImage { url altText }
+ variants(first: 10) {
+ edges {
+ node {
+ id
+ title
+ sku
+ price
+ compareAtPrice
+ selectedOptions { name value }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ GRAPHQL
+
+ def initialize(shop: ENV.fetch("SHOPIFY_SHOP_DOMAIN"), access_token: ENV.fetch("SHOPIFY_ADMIN_ACCESS_TOKEN"))
+ @session = ShopifyAPI::Auth::Session.new(shop: shop, access_token: access_token)
+ @client = ShopifyAPI::Clients::Graphql::Admin.new(session: @session)
+ end
+
+ def products(first: 10)
+ response = @client.query(query: PRODUCTS_QUERY, variables: { first: first })
+ response.body.fetch("data").fetch("products").fetch("edges").map { |edge| edge.fetch("node") }
+ end
+ end
+end
diff --git a/shopify-ruby/lib/forma_shopify/storefront_client.rb b/shopify-ruby/lib/forma_shopify/storefront_client.rb
new file mode 100644
index 0000000..8bc3e71
--- /dev/null
+++ b/shopify-ruby/lib/forma_shopify/storefront_client.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require "shopify_api"
+
+module FormaShopify
+ class StorefrontClient
+ PRODUCTS_QUERY = <<~GRAPHQL
+ query FormaStorefrontProducts($first: Int!) {
+ products(first: $first) {
+ edges {
+ node {
+ id
+ title
+ handle
+ description
+ featuredImage { url altText }
+ priceRange {
+ minVariantPrice { amount currencyCode }
+ }
+ }
+ }
+ }
+ }
+ GRAPHQL
+
+ def initialize(
+ shop_url: ENV.fetch("SHOPIFY_SHOP_DOMAIN"),
+ public_token: ENV["SHOPIFY_STOREFRONT_PUBLIC_TOKEN"],
+ private_token: ENV["SHOPIFY_STOREFRONT_PRIVATE_TOKEN"]
+ )
+ token_options = private_token.to_s.empty? ? { public_token: public_token } : { private_token: private_token }
+ @client = ShopifyAPI::Clients::Graphql::Storefront.new(shop_url, **token_options)
+ end
+
+ def products(first: 10)
+ response = @client.query(query: PRODUCTS_QUERY, variables: { first: first })
+ response.body.fetch("data").fetch("products").fetch("edges").map { |edge| edge.fetch("node") }
+ end
+ end
+end
diff --git a/shopify-ruby/scripts/list_products.rb b/shopify-ruby/scripts/list_products.rb
new file mode 100755
index 0000000..152e9c3
--- /dev/null
+++ b/shopify-ruby/scripts/list_products.rb
@@ -0,0 +1,14 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+require "bundler/setup"
+require "dotenv/load"
+require_relative "../config/shopify_context"
+require_relative "../lib/forma_shopify/admin_client"
+
+FormaShopify::Context.setup!
+client = FormaShopify::AdminClient.new
+
+client.products(first: Integer(ENV.fetch("SHOPIFY_PRODUCT_LIMIT", "5"))).each do |product|
+ puts "#{product.fetch("title")} — #{product.fetch("id")}"
+end
diff --git a/shopify-theme/Gemfile b/shopify-theme/Gemfile
new file mode 100644
index 0000000..bb205e8
--- /dev/null
+++ b/shopify-theme/Gemfile
@@ -0,0 +1,9 @@
+source 'https://rubygems.org'
+
+gem 'nokogiri'
+gem 'htmlentities'
+gem 'rspec'
+
+group :development do
+ gem 'pry'
+end
diff --git a/shopify-theme/README.md b/shopify-theme/README.md
new file mode 100644
index 0000000..065e1da
--- /dev/null
+++ b/shopify-theme/README.md
@@ -0,0 +1,42 @@
+# FORMA Shopify Liquid theme scaffold
+
+This folder mirrors the classic Shopify/Timber theme layout so the static FORMA storefront can be ported into a Shopify Liquid theme.
+
+Reference reviewed:
+
+- `Shopify/Timber`:
+- Shopify theme architecture:
+
+## Structure
+
+```text
+shopify-theme/
+├── assets/ # JavaScript, CSS, and theme images
+├── layout/ # theme.liquid and optional alternate layouts
+├── snippets/ # reusable Liquid snippets
+├── spec/ # structure tests and helpers
+├── templates/ # page, collection, product, cart, blog, search, customer templates
+├── config/ # Shopify theme settings schema/data
+└── config.yml # legacy Theme Kit style config placeholder
+```
+
+## Validate
+
+```bash
+ruby shopify-theme/spec/theme_structure_spec.rb
+```
+
+Optional Timber-style specs require Bundler gems:
+
+```bash
+cd shopify-theme
+bundle install
+bundle exec rspec
+```
+
+## Notes
+
+- This is a minimal migration scaffold, not a full Timber copy.
+- Keep static Vercel assets in the repo root and Shopify Liquid assets under `shopify-theme/`.
+- Shopify theme upload tools expect the theme root to contain supported directories such as `assets`, `layout`, `snippets`, `templates`, `config`, `locales`, and related theme folders.
+- Timber-inspired additions include `timber.js.liquid`, `ajax-cart.js.liquid`, `breadcrumb`, `collection-sorting`, onboarding snippets, comment snippet, `oldIE-js`, and `gift_card.liquid`.
diff --git a/shopify-theme/assets/ajax-cart.js.liquid b/shopify-theme/assets/ajax-cart.js.liquid
new file mode 100644
index 0000000..698c4c2
--- /dev/null
+++ b/shopify-theme/assets/ajax-cart.js.liquid
@@ -0,0 +1,33 @@
+/*============================================================================
+ Lightweight Ajax cart helper inspired by Timber ajax-cart.js.liquid.
+ Uses Shopify Ajax API endpoints when fetch is available and falls back to forms.
+==============================================================================*/
+(function formaAjaxCart(window, document) {
+ 'use strict';
+
+ window.FORMA = window.FORMA || {};
+
+ window.FORMA.getCart = function getCart() {
+ return fetch('/cart.js', { headers: { Accept: 'application/json' } }).then(function parse(response) {
+ return response.json();
+ });
+ };
+
+ window.FORMA.changeCart = function changeCart(line, quantity) {
+ return fetch('/cart/change.js', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
+ body: JSON.stringify({ line: line, quantity: quantity }),
+ }).then(function parse(response) { return response.json(); });
+ };
+
+ document.addEventListener('click', function onCartChange(event) {
+ var button = event.target.closest('[data-cart-change]');
+ if (!button || !window.fetch) return;
+ event.preventDefault();
+ window.FORMA.changeCart(Number(button.dataset.line), Number(button.dataset.quantity)).then(function notify(cart) {
+ document.dispatchEvent(new CustomEvent('forma:cart:change', { detail: cart }));
+ window.location.reload();
+ });
+ });
+})(window, document);
diff --git a/shopify-theme/assets/forma-theme.css.liquid b/shopify-theme/assets/forma-theme.css.liquid
new file mode 100644
index 0000000..e4b704d
--- /dev/null
+++ b/shopify-theme/assets/forma-theme.css.liquid
@@ -0,0 +1,152 @@
+:root {
+ --forma-ink: {{ settings.color_ink | default: '#16130f' }};
+ --forma-paper: {{ settings.color_paper | default: '#fffaf2' }};
+ --forma-accent: {{ settings.color_accent | default: '#b96f4f' }};
+}
+
+body {
+ margin: 0;
+ color: var(--forma-ink);
+ background: var(--forma-paper);
+ font-family: {{ settings.body_font.family | default: 'Inter' }}, system-ui, sans-serif;
+}
+
+.forma-shell {
+ width: min(1180px, calc(100% - 2rem));
+ margin: 0 auto;
+}
+
+.forma-header,
+.forma-footer {
+ border-bottom: 1px solid rgba(22, 19, 15, 0.1);
+ padding: 1rem 0;
+}
+
+.forma-header__inner,
+.forma-footer__inner {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 1rem;
+}
+
+.forma-logo {
+ color: var(--forma-ink);
+ font-weight: 900;
+ letter-spacing: 0.22em;
+ text-decoration: none;
+}
+
+.forma-nav {
+ display: flex;
+ gap: 1rem;
+}
+
+.forma-nav a,
+.forma-link {
+ color: inherit;
+ font-weight: 800;
+ text-decoration: none;
+}
+
+.forma-hero {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) minmax(280px, 0.8fr);
+ gap: clamp(2rem, 6vw, 5rem);
+ align-items: center;
+ padding: clamp(3rem, 7vw, 7rem) 0;
+}
+
+.forma-hero h1,
+.forma-title {
+ margin: 0 0 1rem;
+ font-size: clamp(2.7rem, 7vw, 6rem);
+ line-height: 0.95;
+ letter-spacing: -0.055em;
+}
+
+.forma-hero p,
+.forma-muted {
+ color: rgba(22, 19, 15, 0.68);
+ line-height: 1.7;
+}
+
+.forma-button {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-height: 3rem;
+ padding: 0 1.1rem;
+ color: #fff;
+ background: var(--forma-ink);
+ border: 0;
+ border-radius: 999px;
+ font-weight: 900;
+ text-decoration: none;
+}
+
+.forma-grid {
+ display: grid;
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+ gap: 1rem;
+}
+
+.forma-card {
+ overflow: hidden;
+ background: rgba(255, 255, 255, 0.72);
+ border: 1px solid rgba(22, 19, 15, 0.08);
+ border-radius: 1.2rem;
+}
+
+.forma-card__media {
+ aspect-ratio: 1 / 1.1;
+ background: #efe4d4;
+}
+
+.forma-card__media img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.forma-card__body {
+ padding: 1rem;
+}
+
+.forma-price {
+ font-weight: 900;
+}
+
+.forma-form {
+ display: grid;
+ gap: 0.8rem;
+ max-width: 36rem;
+}
+
+.forma-form input,
+.forma-form textarea {
+ width: 100%;
+ padding: 0.9rem 1rem;
+ border: 1px solid rgba(22, 19, 15, 0.18);
+ border-radius: 0.8rem;
+}
+
+@media (max-width: 860px) {
+ .forma-header__inner,
+ .forma-footer__inner,
+ .forma-hero {
+ align-items: flex-start;
+ flex-direction: column;
+ grid-template-columns: 1fr;
+ }
+
+ .forma-grid {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+}
+
+@media (max-width: 560px) {
+ .forma-grid {
+ grid-template-columns: 1fr;
+ }
+}
diff --git a/shopify-theme/assets/forma-theme.js b/shopify-theme/assets/forma-theme.js
new file mode 100644
index 0000000..ab0e201
--- /dev/null
+++ b/shopify-theme/assets/forma-theme.js
@@ -0,0 +1,22 @@
+(function formaTheme() {
+ document.documentElement.classList.add('forma-js');
+
+ document.addEventListener('submit', function handleAjaxCart(event) {
+ var form = event.target.closest('form[action*="/cart/add"]');
+ if (!form || !window.fetch) return;
+
+ event.preventDefault();
+
+ fetch('/cart/add.js', {
+ method: 'POST',
+ headers: { Accept: 'application/json' },
+ body: new FormData(form),
+ })
+ .then(function parse(response) { return response.json(); })
+ .then(function notify(item) {
+ document.dispatchEvent(new CustomEvent('forma:cart:add', { detail: item }));
+ form.querySelector('[data-add-label]').textContent = 'Agregado';
+ })
+ .catch(function fallback() { form.submit(); });
+ });
+})();
diff --git a/shopify-theme/assets/gift-card.scss.liquid b/shopify-theme/assets/gift-card.scss.liquid
new file mode 100644
index 0000000..bb76f8f
--- /dev/null
+++ b/shopify-theme/assets/gift-card.scss.liquid
@@ -0,0 +1,29 @@
+/*============================================================================
+ Gift card styles inspired by Timber's dedicated gift card stylesheet.
+==============================================================================*/
+.gift-card-template {
+ min-height: 100vh;
+ color: #16130f;
+ background: #fffaf2;
+ font-family: system-ui, sans-serif;
+}
+
+.gift-card {
+ width: min(38rem, calc(100% - 2rem));
+ margin: 4rem auto;
+ padding: 2rem;
+ background: #ffffff;
+ border: 1px solid rgba(22, 19, 15, 0.1);
+ border-radius: 1.5rem;
+ text-align: center;
+}
+
+.gift-card__code {
+ display: inline-block;
+ margin: 1rem 0;
+ padding: 0.9rem 1rem;
+ background: #efe4d4;
+ border-radius: 0.75rem;
+ font-weight: 900;
+ letter-spacing: 0.12em;
+}
diff --git a/shopify-theme/assets/timber.js.liquid b/shopify-theme/assets/timber.js.liquid
new file mode 100644
index 0000000..57f77e2
--- /dev/null
+++ b/shopify-theme/assets/timber.js.liquid
@@ -0,0 +1,33 @@
+/*============================================================================
+ Timber-inspired utility helpers for FORMA.
+ Includes transition prep and URL parameter replacement patterns commonly used
+ by vintage Shopify themes.
+==============================================================================*/
+(function formaTimber(window, document) {
+ 'use strict';
+
+ window.FORMA = window.FORMA || {};
+
+ window.FORMA.prepareTransition = function prepareTransition(element) {
+ if (!element) return;
+ element.classList.add('is-transitioning');
+ element.addEventListener('transitionend', function onTransitionEnd() {
+ element.classList.remove('is-transitioning');
+ element.removeEventListener('transitionend', onTransitionEnd);
+ });
+ element.offsetWidth;
+ };
+
+ window.FORMA.replaceUrlParam = function replaceUrlParam(url, paramName, paramValue) {
+ var pattern = new RegExp('(' + paramName + '=).*?(&|$)');
+ if (url.search(pattern) >= 0) {
+ return url.replace(pattern, '$1' + paramValue + '$2');
+ }
+ return url + (url.indexOf('?') > 0 ? '&' : '?') + paramName + '=' + paramValue;
+ };
+
+ document.addEventListener('change', function onSortChange(event) {
+ if (!event.target.matches('[data-collection-sort]')) return;
+ window.location.href = window.FORMA.replaceUrlParam(window.location.href, 'sort_by', event.target.value);
+ });
+})(window, document);
diff --git a/shopify-theme/config.yml b/shopify-theme/config.yml
new file mode 100644
index 0000000..1d47a32
--- /dev/null
+++ b/shopify-theme/config.yml
@@ -0,0 +1,5 @@
+development:
+ password: ${SHOPIFY_THEME_PASSWORD}
+ theme_id: ${SHOPIFY_THEME_ID}
+ store: ${SHOPIFY_SHOP_DOMAIN}
+ directory: shopify-theme
diff --git a/shopify-theme/config/settings_data.json b/shopify-theme/config/settings_data.json
new file mode 100644
index 0000000..e0faa7d
--- /dev/null
+++ b/shopify-theme/config/settings_data.json
@@ -0,0 +1,7 @@
+{
+ "current": {
+ "color_ink": "#16130f",
+ "color_paper": "#fffaf2",
+ "color_accent": "#b96f4f"
+ }
+}
diff --git a/shopify-theme/config/settings_schema.json b/shopify-theme/config/settings_schema.json
new file mode 100644
index 0000000..272a26d
--- /dev/null
+++ b/shopify-theme/config/settings_schema.json
@@ -0,0 +1,30 @@
+[
+ {
+ "name": "Theme settings",
+ "settings": [
+ {
+ "type": "color",
+ "id": "color_ink",
+ "label": "Ink color",
+ "default": "#16130f"
+ },
+ {
+ "type": "color",
+ "id": "color_paper",
+ "label": "Paper color",
+ "default": "#fffaf2"
+ },
+ {
+ "type": "color",
+ "id": "color_accent",
+ "label": "Accent color",
+ "default": "#b96f4f"
+ },
+ {
+ "type": "collection",
+ "id": "featured_collection",
+ "label": "Featured collection"
+ }
+ ]
+ }
+]
diff --git a/shopify-theme/layout/theme.liquid b/shopify-theme/layout/theme.liquid
new file mode 100644
index 0000000..57d886d
--- /dev/null
+++ b/shopify-theme/layout/theme.liquid
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+ {{ page_title }}{% if current_tags %} – {{ current_tags | join: ', ' }}{% endif %}{% unless page_title contains shop.name %} – {{ shop.name }}{% endunless %}
+ {{ content_for_header }}
+ {{ 'forma-theme.css.liquid' | asset_url | stylesheet_tag }}
+
+
+
+ {% render 'oldIE-js' %}
+
+
+ {% render 'site-header' %}
+ {% render 'breadcrumb' %}
+
+
+ {{ content_for_layout }}
+
+
+ {% render 'site-footer' %}
+
+
diff --git a/shopify-theme/locales/en.default.json b/shopify-theme/locales/en.default.json
new file mode 100644
index 0000000..15e3a7d
--- /dev/null
+++ b/shopify-theme/locales/en.default.json
@@ -0,0 +1,96 @@
+{
+ "general": {
+ "cart": "Cart",
+ "shop": "Shop",
+ "returns": "Returns",
+ "privacy": "Privacy",
+ "breadcrumbs": {
+ "aria_label": "Breadcrumbs",
+ "home": "Home",
+ "home_link_title": "Back to the frontpage"
+ },
+ "404": {
+ "title": "404",
+ "subtext_html": "The page you requested does not exist."
+ }
+ },
+ "home": {
+ "eyebrow": "New campaign",
+ "heading": "Essentials with form, intention and character.",
+ "text": "A Shopify experience inspired by Timber, ready for products, collections and cart.",
+ "cta": "Shop now",
+ "theme_card_title": "FORMA Shopify Theme",
+ "theme_card_text": "Layout, snippets, assets and templates organized."
+ },
+ "homepage": {
+ "onboarding": {
+ "modal_title": "Almost there",
+ "no_collections_html": "Create a featured collection to populate this section."
+ }
+ },
+ "collections": {
+ "title": "Collections",
+ "sorting": {
+ "title": "Sort by",
+ "featured": "Featured",
+ "best_selling": "Best selling",
+ "az": "Alphabetically, A-Z",
+ "za": "Alphabetically, Z-A",
+ "price_ascending": "Price, low to high",
+ "price_descending": "Price, high to low",
+ "date_descending": "Date, new to old",
+ "date_ascending": "Date, old to new"
+ },
+ "onboarding": {
+ "modal_title": "Add products",
+ "no_products_html": "This collection is ready for products from Shopify Admin."
+ }
+ },
+ "product": {
+ "add_to_cart": "Add to cart",
+ "sold_out": "Sold out"
+ },
+ "cart": {
+ "title": "Cart",
+ "checkout": "Checkout",
+ "empty": "Your cart is empty.",
+ "total": "Total"
+ },
+ "search": {
+ "title": "Search",
+ "placeholder": "Search products",
+ "submit": "Search",
+ "empty": "No results."
+ },
+ "customer": {
+ "account": "Account",
+ "create_account": "Create account",
+ "addresses": "Addresses",
+ "no_addresses": "No saved addresses.",
+ "logout": "Log out",
+ "password": "Password",
+ "login": "Log in",
+ "last_name": "Last name",
+ "order": "Order"
+ },
+ "contact": {
+ "title": "Contact",
+ "success": "Thank you. We will reply soon.",
+ "name": "Name",
+ "email": "Email",
+ "message": "Message",
+ "submit": "Send"
+ },
+ "blogs": {
+ "comments": {
+ "title": "Comments"
+ }
+ },
+ "gift_cards": {
+ "issued": {
+ "title": "Gift card",
+ "subtext": "Here is your gift card.",
+ "redeem": "Use this code at checkout."
+ }
+ }
+}
diff --git a/shopify-theme/locales/es.default.json b/shopify-theme/locales/es.default.json
new file mode 100644
index 0000000..75d2103
--- /dev/null
+++ b/shopify-theme/locales/es.default.json
@@ -0,0 +1,96 @@
+{
+ "general": {
+ "cart": "Carrito",
+ "shop": "Tienda",
+ "returns": "Devoluciones",
+ "privacy": "Privacidad",
+ "breadcrumbs": {
+ "aria_label": "Breadcrumbs",
+ "home": "Inicio",
+ "home_link_title": "Volver al inicio"
+ },
+ "404": {
+ "title": "404",
+ "subtext_html": "La página solicitada no existe."
+ }
+ },
+ "home": {
+ "eyebrow": "Nueva campaña",
+ "heading": "Esenciales con forma, intención y carácter.",
+ "text": "Una experiencia Shopify inspirada en Timber, lista para productos, colecciones y carrito.",
+ "cta": "Comprar ahora",
+ "theme_card_title": "FORMA Shopify Theme",
+ "theme_card_text": "Layout, snippets, assets y templates organizados."
+ },
+ "homepage": {
+ "onboarding": {
+ "modal_title": "Casi listo",
+ "no_collections_html": "Crea una colección destacada para poblar esta sección."
+ }
+ },
+ "collections": {
+ "title": "Colecciones",
+ "sorting": {
+ "title": "Ordenar por",
+ "featured": "Destacados",
+ "best_selling": "Más vendidos",
+ "az": "Alfabéticamente, A-Z",
+ "za": "Alfabéticamente, Z-A",
+ "price_ascending": "Precio, menor a mayor",
+ "price_descending": "Precio, mayor a menor",
+ "date_descending": "Fecha, nuevo a antiguo",
+ "date_ascending": "Fecha, antiguo a nuevo"
+ },
+ "onboarding": {
+ "modal_title": "Agrega productos",
+ "no_products_html": "Esta colección está lista para productos desde Shopify Admin."
+ }
+ },
+ "product": {
+ "add_to_cart": "Agregar al carrito",
+ "sold_out": "Agotado"
+ },
+ "cart": {
+ "title": "Carrito",
+ "checkout": "Checkout",
+ "empty": "Tu carrito está vacío.",
+ "total": "Total"
+ },
+ "search": {
+ "title": "Buscar",
+ "placeholder": "Buscar productos",
+ "submit": "Buscar",
+ "empty": "No hay resultados."
+ },
+ "customer": {
+ "account": "Cuenta",
+ "create_account": "Crear cuenta",
+ "addresses": "Direcciones",
+ "no_addresses": "No hay direcciones guardadas.",
+ "logout": "Cerrar sesión",
+ "password": "Contraseña",
+ "login": "Entrar",
+ "last_name": "Apellido",
+ "order": "Pedido"
+ },
+ "contact": {
+ "title": "Contacto",
+ "success": "Gracias. Te responderemos pronto.",
+ "name": "Nombre",
+ "email": "Email",
+ "message": "Mensaje",
+ "submit": "Enviar"
+ },
+ "blogs": {
+ "comments": {
+ "title": "Comentarios"
+ }
+ },
+ "gift_cards": {
+ "issued": {
+ "title": "Tarjeta de regalo",
+ "subtext": "Aquí está tu tarjeta de regalo.",
+ "redeem": "Usa este código en checkout."
+ }
+ }
+}
diff --git a/shopify-theme/snippets/ajax-cart-template.liquid b/shopify-theme/snippets/ajax-cart-template.liquid
new file mode 100644
index 0000000..247743d
--- /dev/null
+++ b/shopify-theme/snippets/ajax-cart-template.liquid
@@ -0,0 +1,12 @@
+
+
+
{{ 'cart.title' | t }}
+ {% for item in cart.items %}
+
+ {% else %}
+
{{ 'cart.empty' | t }}
+ {% endfor %}
+
diff --git a/shopify-theme/snippets/breadcrumb.liquid b/shopify-theme/snippets/breadcrumb.liquid
new file mode 100644
index 0000000..b5d6112
--- /dev/null
+++ b/shopify-theme/snippets/breadcrumb.liquid
@@ -0,0 +1,28 @@
+
+{% unless template == 'index' or template == 'cart' %}
+
+ {{ 'general.breadcrumbs.home' | t }}
+ {% if template contains 'product' %}
+ {% if collection %}
+ /
+ {{ collection.title }}
+ {% endif %}
+ /
+ {{ product.title }}
+ {% elsif template contains 'collection' and collection.handle %}
+ /
+ {{ collection.title }}
+ {% elsif template contains 'page' %}
+ /
+ {{ page.title }}
+ {% elsif template contains 'blog' %}
+ /
+ {{ blog.title }}
+ {% elsif template contains 'article' %}
+ /
+ {{ blog.title }}
+ /
+ {{ article.title }}
+ {% endif %}
+
+{% endunless %}
diff --git a/shopify-theme/snippets/collection-sorting.liquid b/shopify-theme/snippets/collection-sorting.liquid
new file mode 100644
index 0000000..efa756c
--- /dev/null
+++ b/shopify-theme/snippets/collection-sorting.liquid
@@ -0,0 +1,14 @@
+
+
+ {{ 'collections.sorting.title' | t }}
+
+ {{ 'collections.sorting.featured' | t }}
+ {{ 'collections.sorting.best_selling' | t }}
+ {{ 'collections.sorting.az' | t }}
+ {{ 'collections.sorting.za' | t }}
+ {{ 'collections.sorting.price_ascending' | t }}
+ {{ 'collections.sorting.price_descending' | t }}
+ {{ 'collections.sorting.date_descending' | t }}
+ {{ 'collections.sorting.date_ascending' | t }}
+
+
diff --git a/shopify-theme/snippets/comment.liquid b/shopify-theme/snippets/comment.liquid
new file mode 100644
index 0000000..c609d8f
--- /dev/null
+++ b/shopify-theme/snippets/comment.liquid
@@ -0,0 +1,6 @@
+
+
+ {{ comment.author }}
+ {{ comment.created_at | date: format: 'date' }}
+ {{ comment.content }}
+
diff --git a/shopify-theme/snippets/oldIE-js.liquid b/shopify-theme/snippets/oldIE-js.liquid
new file mode 100644
index 0000000..3e9083c
--- /dev/null
+++ b/shopify-theme/snippets/oldIE-js.liquid
@@ -0,0 +1,7 @@
+
+{% comment %}
+ oldIE fixes/shivs inspired by Timber. Kept conditional so modern browsers skip it.
+{% endcomment %}
+
diff --git a/shopify-theme/snippets/onboarding-empty-collection.liquid b/shopify-theme/snippets/onboarding-empty-collection.liquid
new file mode 100644
index 0000000..8f7d22c
--- /dev/null
+++ b/shopify-theme/snippets/onboarding-empty-collection.liquid
@@ -0,0 +1,10 @@
+
+
+
+
+
+
{{ 'collections.onboarding.modal_title' | t }}
+
{{ 'collections.onboarding.no_products_html' | t }}
+
+
+
diff --git a/shopify-theme/snippets/onboarding-featured-collections.liquid b/shopify-theme/snippets/onboarding-featured-collections.liquid
new file mode 100644
index 0000000..5176805
--- /dev/null
+++ b/shopify-theme/snippets/onboarding-featured-collections.liquid
@@ -0,0 +1,10 @@
+
+
+
+
+
+
{{ 'homepage.onboarding.modal_title' | t }}
+
{{ 'homepage.onboarding.no_collections_html' | t }}
+
+
+
diff --git a/shopify-theme/snippets/product-card.liquid b/shopify-theme/snippets/product-card.liquid
new file mode 100644
index 0000000..737e0c5
--- /dev/null
+++ b/shopify-theme/snippets/product-card.liquid
@@ -0,0 +1,16 @@
+
+
+ {% if product.featured_image %}
+ {{ product.featured_image | image_url: width: 720 | image_tag: loading: 'lazy', alt: product.featured_image.alt | escape }}
+ {% endif %}
+
+
+
+
+ {% if product.compare_at_price > product.price %}
+ {{ product.compare_at_price | money }}
+ {% endif %}
+ {{ product.price | money }}
+
+
+
diff --git a/shopify-theme/snippets/site-footer.liquid b/shopify-theme/snippets/site-footer.liquid
new file mode 100644
index 0000000..e93876d
--- /dev/null
+++ b/shopify-theme/snippets/site-footer.liquid
@@ -0,0 +1,13 @@
+
diff --git a/shopify-theme/snippets/site-header.liquid b/shopify-theme/snippets/site-header.liquid
new file mode 100644
index 0000000..b5fe6ba
--- /dev/null
+++ b/shopify-theme/snippets/site-header.liquid
@@ -0,0 +1,14 @@
+
diff --git a/shopify-theme/spec/helpers/html_helper.rb b/shopify-theme/spec/helpers/html_helper.rb
new file mode 100644
index 0000000..fbd1753
--- /dev/null
+++ b/shopify-theme/spec/helpers/html_helper.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module HTMLValidator
+ HTML5_TAGS = [
+ 'a', 'abbr', 'address', 'area', 'article', 'aside', 'audio', 'b', 'base', 'bdi', 'bdo',
+ 'blockquote', 'body', 'br', 'button', 'canvas', 'caption', 'cite', 'code', 'col', 'colgroup',
+ 'data', 'datalist', 'dd', 'del', 'details', 'dfn', 'div', 'dl', 'dt', 'em', 'embed', 'fieldset',
+ 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header',
+ 'hr', 'html', 'i', 'iframe', 'img', 'input', 'ins', 'kbd', 'keygen', 'label', 'legend', 'li',
+ 'link', 'main', 'map', 'mark', 'menu', 'meta', 'meter', 'nav', 'noscript', 'object', 'ol',
+ 'optgroup', 'option', 'output', 'p', 'param', 'picture', 'pre', 'progress', 'q', 'rp', 'rt',
+ 'ruby', 's', 'samp', 'script', 'section', 'select', 'small', 'source', 'span', 'strong', 'style',
+ 'sub', 'summary', 'sup', 'svg', 'table', 'tbody', 'td', 'template', 'textarea', 'tfoot', 'th',
+ 'thead', 'time', 'title', 'tr', 'track', 'u', 'ul', 'var', 'video', 'wbr'
+ ].freeze
+
+ LIQUID_TAG_PATTERN = /\{[%{].*?[%}]\}/m
+
+ module_function
+
+ def html_fragment_for(path)
+ source = File.read(path)
+ without_liquid = HTMLEntities.new.decode(source.gsub(LIQUID_TAG_PATTERN, ''))
+ Nokogiri::HTML5.fragment(without_liquid)
+ rescue NameError
+ Nokogiri::HTML.fragment(without_liquid)
+ end
+
+ def invalid_html_tags(path)
+ html_fragment_for(path).css('*').map(&:name).uniq.reject { |tag| HTML5_TAGS.include?(tag) }
+ end
+end
diff --git a/shopify-theme/spec/helpers/i18n_helper.rb b/shopify-theme/spec/helpers/i18n_helper.rb
new file mode 100644
index 0000000..10892dc
--- /dev/null
+++ b/shopify-theme/spec/helpers/i18n_helper.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+EXCLUDED_KEYWORDS = ['date_formats'].freeze
+
+module I18nHelper
+ module_function
+
+ def match_tags(path)
+ File.open(path).read.scan(/\{\{\s*(?:'|")([a-z0-9._]+?)(?:'|")(?:\s*\|\s*t)/).flatten.map do |key|
+ next if include_excluded_keywords?(key)
+
+ truncate_plural_key(key.split('.'))
+ end.compact.uniq
+ end
+
+ def include_excluded_keywords?(key)
+ EXCLUDED_KEYWORDS.any? { |keyword| key.include?(keyword) }
+ end
+
+ def truncate_plural_key(parts)
+ return parts.join('.') unless parts.last&.match?(/^(one|other|many|few|zero|two)$/)
+
+ parts[0...-1].join('.')
+ end
+
+ def flatten_locale_keys(hash, prefix = nil)
+ hash.each_with_object([]) do |(key, value), keys|
+ next if include_excluded_keywords?(key)
+
+ full_key = [prefix, key].compact.join('.')
+ if value.is_a?(Hash)
+ keys.concat(flatten_locale_keys(value, full_key))
+ else
+ keys << truncate_plural_key(full_key.split('.'))
+ end
+ end.uniq
+ end
+end
diff --git a/shopify-theme/spec/html_validity_spec.rb b/shopify-theme/spec/html_validity_spec.rb
new file mode 100644
index 0000000..8d33d33
--- /dev/null
+++ b/shopify-theme/spec/html_validity_spec.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Theme HTML' do
+ Dir[THEME_ROOT.join('{layout,snippets,templates}/**/*.liquid')].each do |path|
+ it "uses known HTML tags in #{Pathname.new(path).relative_path_from(THEME_ROOT)}" do
+ expect(HTMLValidator.invalid_html_tags(path)).to eq([])
+ end
+ end
+end
diff --git a/shopify-theme/spec/i18n_validity_spec.rb b/shopify-theme/spec/i18n_validity_spec.rb
new file mode 100644
index 0000000..d63bd2c
--- /dev/null
+++ b/shopify-theme/spec/i18n_validity_spec.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Theme i18n' do
+ liquid_files = Dir[THEME_ROOT.join('**/*.liquid')]
+ locale_files = Dir[THEME_ROOT.join('locales/*.json')]
+
+ file_keys = liquid_files.flat_map { |path| I18nHelper.match_tags(path) }.compact.sort.uniq
+ locale_keys = locale_files.each_with_object({}) do |path, result|
+ result[File.basename(path)] = I18nHelper.flatten_locale_keys(JSON.parse(File.read(path))).sort
+ end
+
+ it 'has locale files' do
+ expect(locale_files).not_to be_empty
+ end
+
+ it 'defines every translation key used by Liquid files' do
+ missing_by_locale = locale_keys.transform_values { |keys| file_keys - keys }.reject { |_locale, missing| missing.empty? }
+ expect(missing_by_locale).to eq({})
+ end
+
+ it 'does not contain completely unused locale keys' do
+ unused_by_locale = locale_keys.transform_values { |keys| keys - file_keys }.reject { |_locale, unused| unused.empty? }
+ expect(unused_by_locale).to eq({})
+ end
+end
diff --git a/shopify-theme/spec/spec_helper.rb b/shopify-theme/spec/spec_helper.rb
new file mode 100644
index 0000000..0f01063
--- /dev/null
+++ b/shopify-theme/spec/spec_helper.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'rubygems'
+require 'bundler'
+require 'nokogiri'
+require 'htmlentities'
+require 'pry'
+require 'json'
+require 'pathname'
+
+Dir[File.join(__dir__, 'helpers', '*.rb')].sort.each { |helper| require helper }
+
+THEME_ROOT = Pathname.new(__dir__).join('..').expand_path
diff --git a/shopify-theme/spec/theme_structure_spec.rb b/shopify-theme/spec/theme_structure_spec.rb
new file mode 100644
index 0000000..4191ff0
--- /dev/null
+++ b/shopify-theme/spec/theme_structure_spec.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+require 'pathname'
+
+ROOT = Pathname.new(__dir__).join('..')
+REQUIRED_FILES = %w[
+ Gemfile
+ assets/forma-theme.css.liquid
+ assets/forma-theme.js
+ assets/timber.js.liquid
+ assets/ajax-cart.js.liquid
+ assets/gift-card.scss.liquid
+ layout/theme.liquid
+ snippets/site-header.liquid
+ snippets/site-footer.liquid
+ snippets/product-card.liquid
+ snippets/breadcrumb.liquid
+ snippets/collection-sorting.liquid
+ snippets/onboarding-empty-collection.liquid
+ snippets/onboarding-featured-collections.liquid
+ snippets/comment.liquid
+ snippets/oldIE-js.liquid
+ snippets/ajax-cart-template.liquid
+ templates/404.liquid
+ templates/article.liquid
+ templates/blog.liquid
+ templates/cart.liquid
+ templates/collection.liquid
+ templates/collection.list.liquid
+ templates/index.liquid
+ templates/list-collections.liquid
+ templates/page.contact.liquid
+ templates/page.liquid
+ templates/product.liquid
+ templates/search.liquid
+ templates/gift_card.liquid
+ templates/customers/account.liquid
+ templates/customers/addresses.liquid
+ templates/customers/login.liquid
+ templates/customers/order.liquid
+ templates/customers/register.liquid
+ config.yml
+ config/settings_schema.json
+ config/settings_data.json
+].freeze
+
+missing = REQUIRED_FILES.reject { |file| ROOT.join(file).file? && ROOT.join(file).read.strip.length.positive? }
+abort "Missing or empty theme files:\n#{missing.join("\n")}" if missing.any?
+
+puts "Verified #{REQUIRED_FILES.length} Shopify theme files."
diff --git a/shopify-theme/templates/404.liquid b/shopify-theme/templates/404.liquid
new file mode 100644
index 0000000..069d2c3
--- /dev/null
+++ b/shopify-theme/templates/404.liquid
@@ -0,0 +1,7 @@
+
diff --git a/shopify-theme/templates/article.liquid b/shopify-theme/templates/article.liquid
new file mode 100644
index 0000000..c9b32a2
--- /dev/null
+++ b/shopify-theme/templates/article.liquid
@@ -0,0 +1,14 @@
+
+
+ {{ article.published_at | date: format: 'date' }}
+ {{ article.content }}
+
+
+{% if blog.comments_enabled? %}
+
+{% endif %}
diff --git a/shopify-theme/templates/blog.liquid b/shopify-theme/templates/blog.liquid
new file mode 100644
index 0000000..6f53924
--- /dev/null
+++ b/shopify-theme/templates/blog.liquid
@@ -0,0 +1,9 @@
+
diff --git a/shopify-theme/templates/cart.liquid b/shopify-theme/templates/cart.liquid
new file mode 100644
index 0000000..8b48717
--- /dev/null
+++ b/shopify-theme/templates/cart.liquid
@@ -0,0 +1,18 @@
+
diff --git a/shopify-theme/templates/collection.liquid b/shopify-theme/templates/collection.liquid
new file mode 100644
index 0000000..7e78381
--- /dev/null
+++ b/shopify-theme/templates/collection.liquid
@@ -0,0 +1,15 @@
+
diff --git a/shopify-theme/templates/collection.list.liquid b/shopify-theme/templates/collection.list.liquid
new file mode 100644
index 0000000..6fdcd38
--- /dev/null
+++ b/shopify-theme/templates/collection.list.liquid
@@ -0,0 +1,9 @@
+
diff --git a/shopify-theme/templates/customers/account.liquid b/shopify-theme/templates/customers/account.liquid
new file mode 100644
index 0000000..844d914
--- /dev/null
+++ b/shopify-theme/templates/customers/account.liquid
@@ -0,0 +1,5 @@
+
diff --git a/shopify-theme/templates/customers/addresses.liquid b/shopify-theme/templates/customers/addresses.liquid
new file mode 100644
index 0000000..45d04f2
--- /dev/null
+++ b/shopify-theme/templates/customers/addresses.liquid
@@ -0,0 +1,8 @@
+
diff --git a/shopify-theme/templates/customers/login.liquid b/shopify-theme/templates/customers/login.liquid
new file mode 100644
index 0000000..15c9cfe
--- /dev/null
+++ b/shopify-theme/templates/customers/login.liquid
@@ -0,0 +1,9 @@
+
diff --git a/shopify-theme/templates/customers/order.liquid b/shopify-theme/templates/customers/order.liquid
new file mode 100644
index 0000000..b9cbcd4
--- /dev/null
+++ b/shopify-theme/templates/customers/order.liquid
@@ -0,0 +1,5 @@
+
diff --git a/shopify-theme/templates/customers/register.liquid b/shopify-theme/templates/customers/register.liquid
new file mode 100644
index 0000000..a92bb14
--- /dev/null
+++ b/shopify-theme/templates/customers/register.liquid
@@ -0,0 +1,11 @@
+
diff --git a/shopify-theme/templates/gift_card.liquid b/shopify-theme/templates/gift_card.liquid
new file mode 100644
index 0000000..afa2173
--- /dev/null
+++ b/shopify-theme/templates/gift_card.liquid
@@ -0,0 +1,23 @@
+
+{% layout none %}
+{% assign formatted_initial_value = gift_card.initial_value | money_without_trailing_zeros: gift_card.currency %}
+
+
+
+
+
+
+
+ {{ 'gift_cards.issued.title' | t }} – {{ shop.name }}
+ {{ 'gift-card.scss.liquid' | asset_url | stylesheet_tag }}
+
+
+
+ {{ 'gift_cards.issued.title' | t }}
+ {{ 'gift_cards.issued.subtext' | t }}
+ {{ formatted_initial_value }}
+ {{ gift_card.code | format_code }}
+ {{ 'gift_cards.issued.redeem' | t }}
+
+
+
diff --git a/shopify-theme/templates/index.liquid b/shopify-theme/templates/index.liquid
new file mode 100644
index 0000000..2765512
--- /dev/null
+++ b/shopify-theme/templates/index.liquid
@@ -0,0 +1,31 @@
+
+
+{% assign featured_collection = collections[settings.featured_collection] | default: collections.all %}
+{% if featured_collection.products_count > 0 %}
+
+{% else %}
+
+{% endif %}
diff --git a/shopify-theme/templates/list-collections.liquid b/shopify-theme/templates/list-collections.liquid
new file mode 100644
index 0000000..efbe91b
--- /dev/null
+++ b/shopify-theme/templates/list-collections.liquid
@@ -0,0 +1,10 @@
+
diff --git a/shopify-theme/templates/page.contact.liquid b/shopify-theme/templates/page.contact.liquid
new file mode 100644
index 0000000..d2a2869
--- /dev/null
+++ b/shopify-theme/templates/page.contact.liquid
@@ -0,0 +1,13 @@
+
diff --git a/shopify-theme/templates/page.liquid b/shopify-theme/templates/page.liquid
new file mode 100644
index 0000000..87b3b1f
--- /dev/null
+++ b/shopify-theme/templates/page.liquid
@@ -0,0 +1,4 @@
+
diff --git a/shopify-theme/templates/product.liquid b/shopify-theme/templates/product.liquid
new file mode 100644
index 0000000..7c5b8d1
--- /dev/null
+++ b/shopify-theme/templates/product.liquid
@@ -0,0 +1,19 @@
+
diff --git a/shopify-theme/templates/search.liquid b/shopify-theme/templates/search.liquid
new file mode 100644
index 0000000..a70ea3d
--- /dev/null
+++ b/shopify-theme/templates/search.liquid
@@ -0,0 +1,18 @@
+
diff --git a/supabase/migrations/202605130001_storefront_core.sql b/supabase/migrations/202605130001_storefront_core.sql
new file mode 100644
index 0000000..568f2af
--- /dev/null
+++ b/supabase/migrations/202605130001_storefront_core.sql
@@ -0,0 +1,32 @@
+create table if not exists public.store_events (
+ id bigint generated by default as identity primary key,
+ event_name text not null,
+ payload jsonb not null default '{}'::jsonb,
+ page_path text,
+ user_agent text,
+ created_at timestamptz not null default now()
+);
+
+create table if not exists public.newsletter_signups (
+ id bigint generated by default as identity primary key,
+ email text not null unique,
+ source text not null default 'forma-storefront',
+ created_at timestamptz not null default now()
+);
+
+alter table public.store_events enable row level security;
+alter table public.newsletter_signups enable row level security;
+
+drop policy if exists "Allow anonymous storefront event inserts" on public.store_events;
+create policy "Allow anonymous storefront event inserts"
+ on public.store_events
+ for insert
+ to anon
+ with check (true);
+
+drop policy if exists "Allow anonymous newsletter inserts" on public.newsletter_signups;
+create policy "Allow anonymous newsletter inserts"
+ on public.newsletter_signups
+ for insert
+ to anon
+ with check (source = 'forma-storefront');
diff --git a/turbo.json b/turbo.json
new file mode 100644
index 0000000..80b91be
--- /dev/null
+++ b/turbo.json
@@ -0,0 +1,42 @@
+{
+ "$schema": "https://turbo.build/schema.json",
+ "ui": "tui",
+ "tasks": {
+ "check": {
+ "dependsOn": [
+ "^check",
+ "verify:files",
+ "smoke:static"
+ ],
+ "outputs": []
+ },
+ "verify:files": {
+ "outputs": []
+ },
+ "smoke:static": {
+ "outputs": []
+ },
+ "theme:spec": {
+ "outputs": []
+ },
+ "dev": {
+ "cache": false,
+ "persistent": true
+ },
+ "start": {
+ "cache": false,
+ "persistent": true
+ },
+ "build": {
+ "dependsOn": [
+ "^build"
+ ],
+ "outputs": [
+ "dist/**",
+ "build/**",
+ ".next/**",
+ "!.next/cache/**"
+ ]
+ }
+ }
+}
diff --git a/vercel.json b/vercel.json
new file mode 100644
index 0000000..9d263c9
--- /dev/null
+++ b/vercel.json
@@ -0,0 +1,24 @@
+{
+ "version": 2,
+ "cleanUrls": true,
+ "trailingSlash": false,
+ "headers": [
+ {
+ "source": "/(.*)",
+ "headers": [
+ { "key": "X-Content-Type-Options", "value": "nosniff" },
+ { "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" },
+ { "key": "Permissions-Policy", "value": "camera=(), microphone=(), geolocation=()" }
+ ]
+ },
+ {
+ "source": "/assets/(.*)",
+ "headers": [
+ { "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }
+ ]
+ }
+ ],
+ "rewrites": [
+ { "source": "/", "destination": "/index.html" }
+ ]
+}