diff --git a/.changeset/bright-variant-details.md b/.changeset/bright-variant-details.md new file mode 100644 index 00000000..2de20178 --- /dev/null +++ b/.changeset/bright-variant-details.md @@ -0,0 +1,5 @@ +--- +'@godaddy/gd-commerce-server': patch +--- + +Remove the unsupported SKUGroup status selection from product details requests so variant products load through the Commerce API. diff --git a/.changeset/steady-storefront-surfaces.md b/.changeset/steady-storefront-surfaces.md new file mode 100644 index 00000000..2c76be3b --- /dev/null +++ b/.changeset/steady-storefront-surfaces.md @@ -0,0 +1,5 @@ +--- +'@godaddy/gd-commerce-storefront': patch +--- + +Isolate storefront text and surface colors from host styles and support paired dark theme colors. diff --git a/packages/commerce-server/src/configuration-integration.test.ts b/packages/commerce-server/src/configuration-integration.test.ts index 14d2d2d5..af7d7b0e 100644 --- a/packages/commerce-server/src/configuration-integration.test.ts +++ b/packages/commerce-server/src/configuration-integration.test.ts @@ -9,6 +9,94 @@ afterEach((): void => { vi.unstubAllGlobals(); }); +it('serves variant product details without querying SKUGroup.status', async (): Promise => { + const upstream = vi.fn(async (_input: RequestInfo | URL, init?: RequestInit): Promise => { + const { query, variables } = JSON.parse(String(init?.body)) as { + query: string; + variables: { id: string; attributeValues: string[] }; + }; + if (/^\s+status\s*$/m.test(query)) { + return Response.json({ errors: [{ message: 'Cannot query field "status" on type "SKUGroup".' }] }); + } + expect(variables.id).toBe('shirt'); + const skus = [ + { id: 'shirt-red', label: 'Red shirt' }, + { id: 'shirt-blue', label: 'Blue shirt' }, + ]; + return Response.json({ + data: { + skuGroup: { + id: 'shirt', + label: 'Shirt', + attributes: { + edges: [ + { + node: { + name: 'color', + label: 'Color', + values: { + edges: [ + { node: { name: 'red', label: 'Red' } }, + { node: { name: 'blue', label: 'Blue' } }, + ], + }, + }, + }, + ], + }, + skus: { + edges: skus + .filter( + (sku) => + variables.attributeValues.length === 0 || + variables.attributeValues.includes(sku.id.slice(6)), + ) + .map((node) => ({ node })), + }, + }, + }, + }); + }); + vi.stubGlobal('fetch', upstream); + const app = express(); + app.use( + '/api/commerce', + createCommerceRouter({ + configuration: createRuntimeCommerceConfiguration({ + environment: { + GODADDY_OAUTH_CLIENT_ID: 'client-1', + GODADDY_OAUTH_CLIENT_SECRET: 'secret-1', + GODADDY_STORE_ID: 'store-1', + GODADDY_CHANNEL_ID: 'channel-1', + GODADDY_CURRENCY_CODE: 'USD', + }, + }), + }), + ); + const server = app.listen(0, '127.0.0.1'); + await once(server, 'listening'); + try { + const address = server.address(); + if (!address || typeof address === 'string') throw new Error('Expected a listening TCP server'); + const url = `http://127.0.0.1:${address.port}/api/commerce/products/shirt`; + const product = await clientFetch(url); + expect(product.status).toBe(200); + expect( + (await product.json()).skuGroup.skus.edges.map(({ node }: { node: { id: string } }) => node.id), + ).toEqual(['shirt-red', 'shirt-blue']); + const selected = await clientFetch(`${url}?attributeValues=blue`); + expect(selected.status).toBe(200); + expect((await selected.json()).skuGroup.skus.edges).toEqual([ + { node: { id: 'shirt-blue', label: 'Blue shirt' } }, + ]); + expect(upstream).toHaveBeenCalledTimes(2); + } finally { + await new Promise((resolve, reject) => + server.close((error) => (error ? reject(error) : resolve())), + ); + } +}); + it.each([undefined, 'https://api.example.com', 'https://api.example.com:8443'])( 'uses host configuration throughout the router with API override %s', async (apiBaseUrl): Promise => { diff --git a/packages/commerce-server/src/router.test.ts b/packages/commerce-server/src/router.test.ts index c02f4bab..d93d1171 100644 --- a/packages/commerce-server/src/router.test.ts +++ b/packages/commerce-server/src/router.test.ts @@ -105,7 +105,8 @@ describe('Commerce scoped routes', () => { expect(query).toContain('prices(first: 10)'); expect(query).toContain('inventoryCounts'); expect(query).toContain('pageInfo { hasNextPage }'); - expect(query).toMatch(/skuGroup\(id: \$id\) \{\s+id\s+status/); + expect(query).toMatch(/skuGroup\(id: \$id\) \{\s+id\s+name/); + expect(query).not.toMatch(/^\s+status\s*$/m); expect(query).toContain('attributes(first: 50, orderBy: { position: ASC })'); expect(query).toContain('values(first: 50, orderBy: { position: ASC })'); expect(query).toContain('status: { eq: "ACTIVE" }'); diff --git a/packages/commerce-server/src/server/api/commerce/products/[id]/GET.ts b/packages/commerce-server/src/server/api/commerce/products/[id]/GET.ts index 4b925ba3..5e96325a 100644 --- a/packages/commerce-server/src/server/api/commerce/products/[id]/GET.ts +++ b/packages/commerce-server/src/server/api/commerce/products/[id]/GET.ts @@ -29,7 +29,6 @@ const skuGroupQuery = ` query SkuGroup($id: String!, $first: Int, $attributeValues: [String!] = []) { skuGroup(id: $id) { id - status name label description diff --git a/packages/commerce-storefront/README.md b/packages/commerce-storefront/README.md index ab2dad6f..912aa544 100644 --- a/packages/commerce-storefront/README.md +++ b/packages/commerce-storefront/README.md @@ -82,7 +82,7 @@ A connection failure leaves the surrounding application and its state mounted. C ``` -The stylesheet includes all required utilities and scopes them to the package's surfaces. The build removes CSS layer wrappers in their declared order, so the exported CSS can pass through a host Tailwind v3 PostCSS pipeline without `@tailwind` directives. Import it directly; consumers do not need to copy or rewrite the CSS. It does not add a global reset or require dependency scanning by a host Tailwind build. The `theme` prop reaches the drawer even though it is portalled into `document.body`. Keep text, controls and focus indicators accessible when changing colors. Utility class names and internal markup are not a customization API. +The stylesheet includes all required utilities and scopes them to the package's surfaces. The build removes CSS layer wrappers in their declared order, so the exported CSS can pass through a host Tailwind v3 PostCSS pipeline without `@tailwind` directives. Import it directly; consumers do not need to copy or rewrite the CSS. It does not add a global reset or require dependency scanning by a host Tailwind build. The `theme` prop reaches the drawer even though it is portalled into `document.body`. Surfaces pair `--commerce-surface` with `--commerce-text`; muted text and borders adapt to that pair. Descendants inherit the surface text and body font instead of global heading styles. Keep text, controls and focus indicators accessible when changing colors. Utility class names and internal markup are not a customization API. The first release uses English UI text and `en-US` currency formatting. Catalog title and description are configurable. Full localization and arbitrary component slots are outside this initial API. diff --git a/packages/commerce-storefront/src/artifacts.test.ts b/packages/commerce-storefront/src/artifacts.test.ts index a712925b..73d6cf71 100644 --- a/packages/commerce-storefront/src/artifacts.test.ts +++ b/packages/commerce-storefront/src/artifacts.test.ts @@ -28,7 +28,7 @@ it('ships CSS that scopes every document selector to a commerce surface', async it('ships a client package with framework peers external and no server dependencies', async () => { const js = await readFile(new URL('../dist/index.js', import.meta.url), 'utf8'); - expect(js).not.toMatch(/node:(?:fs|crypto)|GODADDY_OAUTH_CLIENT_SECRET|@godaddy\/commerce-server/); + expect(js).not.toMatch(/node:(?:fs|crypto)|GODADDY_OAUTH_CLIENT_SECRET|@godaddy\/gd-commerce-server/); expect(js).toContain('react/jsx-runtime'); expect(js).toContain('from "react"'); expect(js).toContain('from "@tanstack/react-query"'); @@ -38,6 +38,28 @@ it('ships a client package with framework peers external and no server dependenc expect(pkg.exports['./styles.css']).toBe('./dist/styles.css'); }); +it('pairs surface colors and resets host descendant typography before applying utilities', async () => { + const css = postcss.parse(await readFile(new URL('../dist/styles.css', import.meta.url), 'utf8')); + const declarations: Record = {}; + css.walkRules('.commerce-storefront', (rule) => { + rule.walkDecls((declaration) => { + declarations[declaration.prop] = declaration.value; + }); + }); + expect(declarations.color).toBe('var(--commerce-text, #171717)'); + expect(declarations['background-color']).toBe('var(--commerce-surface, #fff)'); + const resets: Record = {}; + css.walkRules('.commerce-storefront :where(*)', (rule) => { + rule.walkDecls((declaration) => { + resets[declaration.prop] = declaration.value; + }); + }); + expect(resets).toMatchObject({ color: 'inherit', 'font-family': 'inherit' }); + expect(css.toString().indexOf('.commerce-storefront :where(*)')).toBeLessThan( + css.toString().indexOf('.commerce-storefront .text-commerce-on-accent'), + ); +}); + // Vite processes dependency CSS separately through the host's PostCSS plugins. // No @tailwind directives or scanned package classes should be needed in the host. it('passes shipped CSS through Tailwind v3 without losing package styles', async () => { diff --git a/packages/commerce-storefront/src/styles.css b/packages/commerce-storefront/src/styles.css index b5202ce5..12fb5ef2 100644 --- a/packages/commerce-storefront/src/styles.css +++ b/packages/commerce-storefront/src/styles.css @@ -9,11 +9,18 @@ --color-commerce-on-accent: var(--commerce-on-accent, #fff); --color-white: var(--commerce-surface, #fff); --color-neutral-900: var(--commerce-text, #171717); + --color-neutral-100: color-mix(in srgb, var(--commerce-text, #171717) 5%, var(--commerce-surface, #fff)); + --color-neutral-200: color-mix(in srgb, var(--commerce-text, #171717) 15%, var(--commerce-surface, #fff)); + --color-neutral-300: color-mix(in srgb, var(--commerce-text, #171717) 25%, var(--commerce-surface, #fff)); + --color-neutral-400: color-mix(in srgb, var(--commerce-text, #171717) 55%, var(--commerce-surface, #fff)); + --color-neutral-600: color-mix(in srgb, var(--commerce-text, #171717) 75%, var(--commerce-surface, #fff)); + --color-neutral-700: color-mix(in srgb, var(--commerce-text, #171717) 85%, var(--commerce-surface, #fff)); --radius-lg: var(--commerce-radius, 0.5rem); } @layer base { - .commerce-storefront { color: var(--commerce-text, #171717); font-family: inherit; line-height: 1.5; } + .commerce-storefront { color: var(--commerce-text, #171717); background-color: var(--commerce-surface, #fff); font-family: inherit; line-height: 1.5; } + .commerce-storefront :where(*) { color: inherit; font-family: inherit; } .commerce-storefront *, .commerce-storefront *::before, .commerce-storefront *::after { box-sizing: border-box; border-width: 0; border-style: solid; } @@ -24,6 +31,6 @@ .commerce-storefront :where(a) { color: inherit; text-decoration: inherit; } .commerce-storefront :where(img) { display: block; max-width: 100%; } .commerce-storefront :where(ul) { list-style: none; } - .commerce-inline { display: inline-block; } + .commerce-inline { display: inline-block; background-color: transparent; } .commerce-sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border-width: 0; } }