CursorPool
← 返回首页

Shopify App Development

Develop Shopify App Extensions using TypeScript with best practices

cursor.directory·11
规则

Shopify App Extension Typescript Conversion

# Converting Shopify Extensions from JavaScript to TypeScript

## Overview

Shopify Checkout UI Extensions can be written in TypeScript (`.tsx`) instead of JavaScript (`.jsx`). This provides better type safety and IDE support.

## Conversion Steps

### 1. Rename the file

- Change `src/Checkout.jsx` → `src/Checkout.tsx`

### 2. Update `shopify.extension.toml`

```toml
[[extensions.targeting]]
module = "./src/Checkout.tsx"  # Changed from .jsx
target = "purchase.checkout.block.render"
```

### 3. Update `shopify.d.ts`

```typescript
import "@shopify/ui-extensions";

declare module "./src/Checkout.tsx" {
  // Changed from .jsx
  const shopify: import("@shopify/ui-extensions/purchase.checkout.block.render").Api;
  const globalThis: { shopify: typeof shopify };
}
```

### 4. Update `tsconfig.json`

Remove `checkJs` and `allowJs`, add `strict`:

```json
{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "preact",
    "target": "ES2020",
    "strict": true, // Add this
    "moduleResolution": "bundler",
    "esModuleInterop": true,
    "skipLibCheck": true,
    "baseUrl": "../..",
    "paths": {
      "@lib/utils": ["./lib/utils.ts"],
      "@lib/*": ["./lib/*"]
    }
  },
  "include": ["./src", "./shopify.d.ts", "../../lib/**/*"]
}
```

### 5. Add TypeScript Types

Add type definitions for your settings and component props:

```typescript
// Common types
type PaddingKeyword = "none" | "small" | "small-100" | "base" | "large" | "large-100";
type JustifyContent = "start" | "center" | "end";
type Tone = "auto" | "neutral" | "info" | "success" | "warning" | "critical" | "custom";

// Example usage
const paddingMap: Record<string, PaddingKeyword> = {
  none: "none",
  extraTight: "small"
  // ...
};
const safePadding: PaddingKeyword = paddingMap[paddingValue] || "none";
```

### 6. Type Your Variables

```typescript
// Before (JSX)
const text = String(settings?.text || "");
const padding = String(settings?.padding || "none");

// After (TSX)
const text: string = String(settings?.text || "");
const padding: string = String(settings?.padding || "none");
const safePadding: PaddingKeyword = paddingMap[padding] || "none";
```

### 7. Type JSX Elements

```typescript
// Before
let content;
if (condition) {
  content = <s-text>Hello</s-text>;
}

// After
let content: JSX.Element | null;
if (condition) {
  content = <s-text>Hello</s-text>;
} else {
  content = null;
}
```

## Benefits

1. **Type Safety**: Catch errors at compile time
2. **Better IDE Support**: Autocomplete, refactoring, navigation
3. **Self-Documenting**: Types serve as documentation
4. **Easier Refactoring**: TypeScript helps ensure changes are consistent

## Common Type Patterns

### Settings Values

```typescript
// Settings from shopify.settings.value can be: string | number | boolean | null | undefined
const text = String(settings?.text || "");
const number = typeof settings?.number === "number" ? settings.number : 0;
const enabled = Boolean(settings?.enabled);
```

### Padding/Spacing Values

```typescript
type PaddingKeyword = "none" | "small" | "small-100" | "base" | "large" | "large-100";
const paddingMap: Record<string, PaddingKeyword> = {
  none: "none",
  extraTight: "small",
  tight: "small-100",
  base: "base",
  loose: "large",
  extraLoose: "large-100"
};
```

### Tone/Appearance Values

```typescript
type Tone = "auto" | "neutral" | "info" | "success" | "warning" | "critical" | "custom";
const toneMap: Record<string, Tone> = {
  normal: "auto",
  accent: "neutral",
  subdued: "neutral",
  info: "info",
  success: "success",
  warning: "warning",
  critical: "critical",
  decorative: "custom"
};
```

## Example: Complete Conversion

**Before (Checkout.jsx):**

```jsx
import "@shopify/ui-extensions/preact";
import { render } from "preact";

export default async () => {
  render(<Extension />, document.body);
};

function Extension() {
  const settings = shopify.settings.value;
  const text = String(settings?.text || "");
  return <s-text>{text}</s-text>;
}
```

**After (Checkout.tsx):**

```tsx
import "@shopify/ui-extensions/preact";
import { render } from "preact";

export default async () => {
  render(<Extension />, document.body);
};

function Extension() {
  const settings = shopify.settings.value;
  const text: string = String(settings?.text || "");
  return <s-text>{text}</s-text>;
}
```

## Notes

- Shopify's extension bundler supports TypeScript and will compile `.tsx` files
- The `shopify` global object is typed via `shopify.d.ts`
- Path aliases (`@lib/*`) work the same in TypeScript
- All Polaris web components are typed via `@shopify/ui-extensions/preact`
规则

Shopify App UI Extension Development

# Shopify App UI Extension Development Best Practices

Always stick to this guide which provides best practices, patterns, and common pitfalls for developing Shopify App UI Extension using Preact and Polaris web components.

## Table of Contents

1. [Architecture Overview](#architecture-overview)
2. [Component Selection Guide](#component-selection-guide)
3. [Text Styling Limitations](#text-styling-limitations)
4. [Responsive Design Patterns](#responsive-design-patterns)
5. [Shared Code Patterns](#shared-code-patterns)
6. [Type Safety Guidelines](#type-safety-guidelines)
7. [Common Errors and Solutions](#common-errors-and-solutions)
8. [Settings Configuration](#settings-configuration)

## Architecture Overview

### Extension Structure

App UI Extensions are self-contained modules that:
- Use Preact as the framework (scaffolded by default)
- Render Polaris web components (custom HTML elements prefixed with `s-`)
- Can import utilities from `@lib/utils` via TypeScript path aliases (configured in `tsconfig.json`)
- Run in an isolated sandbox environment
- Have access to `shopify` global object for APIs and settings
- Use TypeScript for type safety (`.tsx` files)

**CRITICAL: HTML Elements Will NEVER Work**
- **ONLY Polaris web components work** (`<s-text>`, `<s-box>`, `<s-grid>`, `<s-stack>`, `<s-image>`, etc.)
- **HTML elements will NOT render** (`<div>`, `<span>`, `<p>`, `<style>`, `<script>`, etc.)
- **`<style>` and `<script>` tags will NEVER work** - they are HTML elements and will be ignored
- If you need styling, use Polaris component props (`background`, `border`, `paddingBlock`, etc.)
- If you need custom behavior, use Preact hooks and Polaris component event handlers

### File Structure

```
extensions/
└── your-extension/
    ├── src/
    │   └── Checkout.tsx          # Main extension file (TypeScript)
    ├── locales/
    │   └── en.default.json       # Translations
    ├── shopify.extension.toml    # Extension configuration
    ├── tsconfig.json              # TypeScript configuration with path aliases
    └── package.json
```

## Component Selection Guide

### Text Components

#### `s-text` - Inline Text

**Use for:**
- Labels, inline emphasis, small text
- Text within paragraphs or other block elements
- Short phrases or single words

**Props:**
- `type`: `'small' | 'address' | 'mark' | 'strong' | 'generic' | 'redundant' | 'emphasis' | 'offset'`
- `tone`: `'auto' | 'neutral' | 'info' | 'success' | 'warning' | 'critical' | 'custom'`
- `color`: `'base' | 'subdued'`

**Limitations:**
- No font size control beyond `type="small"`
- Cannot control font weight separately (use `type="strong"` for bold)
- Cannot control font style separately (use `type="emphasis"` for italic)

**Example:**
```jsx
<s-text type="strong" tone="success">Important text</s-text>
```

#### `s-paragraph` - Block Text Wrapper

**Use for:**
- Wrapper for `s-text` elements (acts as a block-level container)
- Multi-line content split by newlines
- Standalone blocks of text

**Props:**
- `type`: `'paragraph' | 'small'` - Controls font size (applies to paragraph)
- `tone`: `'auto' | 'neutral' | 'info' | 'success' | 'warning' | 'critical' | 'custom'` - Controls color/appearance (applies to paragraph)
- `color`: `'base' | 'subdued'`

**Important:** `s-paragraph` is a wrapper component. Use `s-text` elements as children for font style (bold/italic) and individual line control.

**Limitations:**
- Only two size options: `'paragraph'` (default) or `'small'`
- Font style (bold/italic) must be applied to child `s-text` elements, not `s-paragraph`

**Example:**
```jsx
<s-paragraph type="small" tone="info">
  <s-text type="strong">Bold label: </s-text>
  <s-text>Regular text</s-text>
</s-paragraph>

// Multi-line text split by \n
{lines.map((line, index) => (
  <s-paragraph key={index} {...paragraphProps}>
    <s-text {...textProps}>{line}</s-text>
  </s-paragraph>
))}
```

#### `s-heading` - Hierarchical Titles

**Use for:**
- Titles, headings, large text
- Section headers
- Content that should be semantically a heading

**Props:**
- Heading level is automatically determined by nesting within `s-section` components
- No explicit size props - size controlled by nesting level

**Note:**
- Using `s-heading` for regular text changes semantics (not recommended for body text)
- Size is controlled by nesting, not props

**Example:**
```jsx
<s-heading>Section Title</s-heading>
```

### Layout Components

#### `s-stack` - Flexible Layout Container

**Use for:**
- Organizing elements horizontally or vertically
- Controlling spacing between elements
- Aligning content

**Key Props:**
- `direction`: `'block' | 'inline'` - Controls layout direction
- `gap`: Spacing between children (e.g., `'none' | 'small' | 'base' | 'large'`)
- `justifyContent`: Aligns along main axis (use with `direction="inline"` for horizontal alignment)
- `alignItems`: Aligns along cross axis (use with `direction="block"` for vertical alignment)
- `paddingBlock`: Vertical padding
- `paddingInline`: Horizontal padding

**Alignment Rules:**
- **Horizontal alignment** (`direction="inline"`): Use `justifyContent` (`'start' | 'center' | 'end'`)
- **Cross-axis alignment** (`direction="block"`): Use `alignItems` (`'start' | 'center' | 'end' | 'stretch'`) for horizontal alignment of block children
- **Text alignment**: For text content, use `alignItems` with `direction="block"` to align paragraphs horizontally

**Example:**
```jsx
// Horizontal alignment (inline direction)
<s-stack direction="inline" justifyContent="center" gap="base">
  <s-text>Left</s-text>
  <s-text>Center</s-text>
  <s-text>Right</s-text>
</s-stack>

// Text alignment (block direction with alignItems)
<s-stack direction="block" alignItems="center" gap="small">
  <s-paragraph>Centered paragraph</s-paragraph>
  <s-paragraph>Another centered paragraph</s-paragraph>
</s-stack>
```

#### `s-box` - Generic Container

**Use for:**
- Applying styling (backgrounds, borders, padding)
- Controlling size constraints (`maxInlineSize`, `minInlineSize`)
- Wrapping content that needs size control

**Common Use Cases:**
- Wrapping images with size constraints
- Creating containers with backgrounds or borders
- Applying responsive display (using `display` prop with container queries)

**Example:**
```jsx
<s-box 
  padding="base" 
  background="subdued" 
  border="base"
  maxInlineSize="400px"
>
  Content here
</s-box>
```

#### `s-grid` - Grid Layout

**Use for:**
- Complex multi-column layouts
- Responsive grid systems
- Aligning items in rows and columns

**Key Props:**
- `gridTemplateColumns`: Column template (e.g., `"1fr 1fr"`, `"repeat(3, 1fr)"`, `"repeat(3, auto)"`)
- `gap`: Spacing between grid items

**Best Practice:** Use `repeat()` function for repeated column patterns instead of repeating values:
```jsx
// ✅ Good - use repeat() for repeated patterns
<s-grid gridTemplateColumns="repeat(3, auto)" gap="small">
  <s-box>Item 1</s-box>
  <s-box>Item 2</s-box>
  <s-box>Item 3</s-box>
</s-grid>

// ❌ Avoid - repeating values manually
<s-grid gridTemplateColumns="auto auto auto" gap="small">
```

**Replicating Flex Behavior:** When displaying multiple inline contents (like text, icons, or small elements) that should flow naturally like flex items, use `repeat(auto-fit, minmax(0, max-content))`:
```jsx
// ✅ Good - flex-like behavior for inline contents
<s-grid gridTemplateColumns="repeat(auto-fit, minmax(0, max-content))" gap="small" alignItems="center">
  {stars && <s-image src={stars} alt="" />}
  {author && <s-text>{author}</s-text>}
  {date && <s-text color="subdued">{date}</s-text>}
</s-grid>

// This pattern:
// - Automatically fits items based on content size
// - Wraps items naturally when space is limited
// - Behaves like flexbox with flex-wrap
// - Each item takes only the space it needs (max-content)
```

### Media Components

#### `s-image` - Images

**Use for:**
- Displaying images
- Product thumbnails
- Icons or graphics

**Key Props:**
- `src`: Image URL (required)
- `alt`: Alternative text (required for accessibility)
- `inlineSize`: `'fill' | 'auto'` - Controls width
- `objectFit`: `'contain' | 'cover'` - How image fits container
- `borderRadius`: Corner radius

**Size Control:**
- Wrap in `s-box` with `maxInlineSize` to control maximum width
- Use `inlineSize="fill"` to fill container, `inlineSize="auto"` for natural size

**Example:**
```jsx
<s-box maxInlineSize="300px">
  <s-image 
    src="https://example.com/image.jpg" 
    alt="Description"
    inlineSize="fill"
    objectFit="contain"
  />
</s-box>
```

## Text Styling Limitations

### Font Size Control

**Problem:** Polaris web components have very limited font size control.

**Available Options:**
- `s-text`: Only `type="small"` for smaller text
- `s-paragraph`: Only `type="small"` for smaller text
- `s-heading`: Size controlled by nesting level (not recommended for body text)

**Workaround:**
- For larger text, you can use `s-heading`, but this changes semantics
- Accept the limitation and use `s-text`/`s-paragraph` with available options
- Use `tone` and `color` props to create visual hierarchy instead

**Example:**
```jsx
// Limited size options
<s-text type="small">Small text</s-text>
<s-text>Normal text</s-text>
<s-heading>Large text (semantic heading)</s-heading>

// Use tone for visual hierarchy instead
<s-text tone="critical">Important</s-text>
<s-text tone="subdued">Less important</s-text>
```

### Font Style Control

**Problem:** No separate font weight or style props.

**Available Options:**
- `s-text type="strong"`: Bold text
- `s-text type="emphasis"`: Italic text
- Cannot combine bold and italic
- Cannot control font weight separately

**Important Pattern:** `s-paragraph` is a wrapper for `s-text` elements. Apply font style to `s-text` children, not `s-paragraph`:

```jsx
// ✅ Correct - font style on s-text, size/tone on s-paragraph
<s-paragraph type="small" tone="info">
  <s-text type="strong">Bold label: </s-text>
  <s-text>Regular text</s-text>
</s-paragraph>

// Multi-line text pattern
{lines.map((line, index) => (
  <s-paragraph key={index} {...paragraphProps}>
    <s-text {...textProps}>{line}</s-text>
  </s-paragraph>
))}
```

**Workaround:**
- Use `type="strong"` for bold on `s-text`
- Use `type="emphasis"` for italic on `s-text`
- Apply size (`type`) and color (`tone`) to `s-paragraph`
- Accept that you cannot combine both bold and italic

**Example:**
```jsx
<s-text type="strong">Bold text</s-text>
<s-text type="emphasis">Italic text</s-text>
// Cannot do: <s-text type="strong" style="italic">Bold italic</s-text>
```

### Text Color/Appearance

**Available Options:**
- `tone`: `'auto' | 'neutral' | 'info' | 'success' | 'warning' | 'critical' | 'custom'`
- `color`: `'base' | 'subdued'` (intensity control)

**Mapping Custom Values:**
If your settings use custom appearance values, map them to valid tones:

```jsx
const toneMap = {
  'normal': 'auto',
  'accent': 'neutral',
  'subdued': 'neutral',
  'info': 'info',
  'success': 'success',
  'warning': 'warning',
  'critical': 'critical',
  'decorative': 'custom'
};
const validTone = toneMap[textAppearance] || 'auto';
```

## Responsive Design Patterns

### Container Queries

**Important:** Preact extensions do NOT support the Style API. Use container query strings instead.

**Syntax:**
```
@container (inline-size >= 380px) value1, value2
```

**Breakpoint:**
- Default breakpoint is `380` (integer, in pixels) - used by `displayDesktopStyle()` and `displayMobileStyle()` functions
- Below breakpoint = mobile/small
- Breakpoint and above = desktop/medium+
- Custom breakpoints can be passed as integer parameters to `displayDesktopStyle(breakpoint)` and `displayMobileStyle(breakpoint)`

### ⚠️ CRITICAL: s-query-container Requirement

**🚨 MANDATORY:** When using responsive values (`displayDesktopStyle`, `displayMobileStyle`, `displayDesktopStyleAuto`, `displayMobileStyleAuto`, or any custom `@container` queries), you **MUST** wrap the responsive content in an `<s-query-container>` component.

**Why:** Container queries need a container context to query against. Without `<s-query-container>`, responsive styles will NOT work.

**Correct Pattern:**
```jsx
<s-box paddingBlock={paddingBlock} paddingInline={paddingInline}>
  <s-query-container>
    <s-box display={displayDesktopStyle}>Desktop content</s-box>
    <s-box display={displayMobileStyle}>Mobile content</s-box>
  </s-query-container>
</s-box>
```

**Incorrect Pattern (WILL NOT WORK):**
```jsx
<s-box paddingBlock={paddingBlock} paddingInline={paddingInline}>
  <s-box display={displayDesktopStyle}>Desktop content</s-box>
  <s-box display={displayMobileStyle}>Mobile content</s-box>
</s-box>
```

**Named Containers (Advanced):**
If you need to query a specific parent container, use the `queryname` attribute:
```jsx
<s-query-container queryname="myContainer">
  {/* content */}
</s-query-container>

// Then reference it in your query:
const customQuery = '@container myContainer (inline-size >= 380px) block, none';
```

### Responsive Display

**Desktop-only display:**
```jsx
<s-box paddingBlock={paddingBlock} paddingInline={paddingInline}>
  <s-query-container>
    <s-box display={displayDesktopStyle}>Desktop content</s-box>
  </s-query-container>
</s-box>
```

**Mobile-only display:**
```jsx
<s-box paddingBlock={paddingBlock} paddingInline={paddingInline}>
  <s-query-container>
    <s-box display={displayMobileStyle}>Mobile content</s-box>
  </s-query-container>
</s-box>
```

**Both desktop and mobile:**
```jsx
<s-box paddingBlock={paddingBlock} paddingInline={paddingInline}>
  <s-query-container>
    <s-box display={displayDesktopStyle}>Desktop content</s-box>
    <s-box display={displayMobileStyle}>Mobile content</s-box>
  </s-query-container>
</s-box>
```

### Responsive Padding/Spacing

**Example:**
```jsx
// Larger padding on desktop, smaller on mobile
const responsivePadding = '@container (inline-size >= 380px) large, base';
<s-box paddingBlock={paddingBlock} paddingInline={paddingInline}>
  <s-query-container>
    <s-stack padding={responsivePadding}>Content</s-stack>
  </s-query-container>
</s-box>
```

### Responsive Utilities

**Import from `@lib/utils` (recommended):**

```typescript
import { displayDesktopStyle, displayMobileStyle } from '@lib/utils';

// Usage - ALWAYS wrap in s-query-container
// Functions accept optional breakpoint parameter as integer (default: 380)
<s-box paddingBlock={paddingBlock} paddingInline={paddingInline}>
  <s-query-container>
    <s-box display={displayDesktopStyle()}>Desktop content</s-box>
    <s-box display={displayMobileStyle()}>Mobile content</s-box>
  </s-query-container>
</s-box>

// With custom breakpoint (integer in pixels)
const breakpoint = 380;
<s-box display={displayDesktopStyle(breakpoint)}>Desktop content</s-box>
<s-box display={displayMobileStyle(breakpoint)}>Mobile content</s-box>
```

**Function Signatures:**
```typescript
displayDesktopStyle(breakpoint?: number, auto?: boolean): any
displayMobileStyle(breakpoint?: number, auto?: boolean): any
```

**Parameters:**
- `breakpoint`: Optional breakpoint as integer in pixels (e.g., `380`, `500`). Default: `380`
- `auto`: Optional boolean to use `'auto'` instead of `'block'`. Default: `false`

**Or define inline if needed:**

```typescript
const breakpoint = 380;
const displayDesktopStyle = `@container (inline-size >= ${breakpoint}px) block, none`;
const displayMobileStyle = `@container (inline-size >= ${breakpoint}px) none, block`;
```

**Remember:** Any component using these responsive values MUST be wrapped in `<s-query-container>`. Always call the functions with `()` - they are functions, not constants.

## Shared Code Patterns

### Importing Utilities from `@lib/utils`

Extensions can import utilities via TypeScript path aliases configured in `tsconfig.json`:

```typescript
import { logger, displayDesktopStyle, displayMobileStyle, normalizeImageSize } from '@lib/utils';
```

**Available Utilities:**
- `logger`: Debug logging utility with `collapse()`, `table()`, `log()`, `error()` methods
- `displayDesktopStyle(breakpoint?, auto?)`: Function that returns container query string for desktop-only display (default breakpoint: `380` as integer)
- `displayMobileStyle(breakpoint?, auto?)`: Function that returns container query string for mobile-only display (default breakpoint: `380` as integer)
- `normalizeImageSize`: Normalizes image size values with defaults
- `formatTime`: Formats seconds to MM:SS format

**Example Usage:**

```typescript
import '@shopify/ui-extensions/preact';
import { render } from 'preact';
import { logger, displayDesktopStyle, displayMobileStyle, normalizeImageSize } from '@lib/utils';

function Extension() {
  const settings = shopify.settings.value;
  logger.collapse(settings, 'Extension Name | Settings', 'info');
  
  // Use utilities
  const imageSize = normalizeImageSize(settings?.size, 600);
  
  // Use responsive display functions (always call with parentheses)
  // Breakpoint is an integer in pixels
  const breakpointRaw = settings?.breakpoint != null ? parseInt(settings.breakpoint as any, 10) : undefined;
  const breakpoint = breakpointRaw && breakpointRaw >= 200 && breakpointRaw <= 640 ? breakpointRaw : 380;
  // ...
  
  return (
    <s-box>
      <s-query-container>
        <s-box display={displayDesktopStyle(breakpoint)}>Desktop</s-box>
        <s-box display={displayMobileStyle(breakpoint)}>Mobile</s-box>
      </s-query-container>
    </s-box>
  );
}
```

**Best Practice:**
- Import utilities from `@lib/utils` rather than copying
- Use `logger.collapse()` for debugging settings
- Keep extension code focused on UI logic

## Type Safety Guidelines

### Settings Values

**Problem:** Settings from `shopify.settings.value` can be `string | number | boolean | null | undefined`

**Solution:** Always validate and cast settings values. Use TypeScript interface and logger for debugging:

```typescript
interface Settings {
  text_block?: string;
  text_align?: string;
  padding_block?: string;
  // ... other settings
}

function Extension() {
  const settings = shopify.settings.value as Settings;
  logger.collapse(settings, 'Extension Name | Settings', 'info');

  const text = String(settings?.text_block || '');
  const number = settings?.number != null ? parseInt(settings.number as any, 10) : undefined;
const enabled = Boolean(settings?.enabled);
}
```

**Best Practice:**
- Define `Settings` interface at top of file
- Use `logger.collapse()` instead of `console.log()` for consistent debugging
- Cast settings with `as Settings` for type safety
- Always provide defaults with `|| ''` or `|| 'none'`

### Type Definitions

**Best Practice:** Use inline literal types instead of recreating type aliases. This ensures proper TypeScript inference and keeps code compact:

```jsx
// ✅ Good - inline literal types
const paddingMap: Record<string, 'none' | 'small' | 'base' | 'large'> = {
  'none': 'none',
  'extraTight': 'small',
  'base': 'base',
  'loose': 'large'
};

// ❌ Avoid - unnecessary type aliases unless reused multiple times
type PaddingKeyword = 'none' | 'small' | 'base' | 'large';
```

### Padding Values

**Problem:** Settings might use custom values that don't match valid `PaddingKeyword` types.

**Valid PaddingKeyword Values:**
- `'none'`
- `'small-500' | 'small-400' | 'small-300' | 'small-200' | 'small-100' | 'small'`
- `'base'`
- `'large' | 'large-100' | 'large-200' | 'large-300' | 'large-400' | 'large-500'`

**Solution:** Map custom values to valid ones with proper TypeScript types:

```typescript
const paddingMap: Record<string, 'none' | 'small' | 'small-100' | 'base' | 'large' | 'large-100'> = {
  'none': 'none',
  'extraTight': 'small',
  'tight': 'small-100',
  'base': 'base',
  'loose': 'large',
  'extraLoose': 'large-100'
};
const paddingBlock = String(settings?.padding_block || 'none');
const safePaddingBlock = paddingMap[paddingBlock] || 'none';
```

**Usage in Components:**

```typescript
<s-box paddingBlock={safePaddingBlock} paddingInline={safePaddingInline}>
  {/* content */}
</s-box>
```

### Component Props

**Problem:** TypeScript infers prop values as `string` instead of specific literal types.

**Solution:** Use inline literal values in JSX instead of variables:

```jsx
// ❌ Bad - TypeScript infers as string
const textType = 'strong';
content = <s-text type={textType}>Text</s-text>;

// ✅ Good - TypeScript infers as literal type
if (textStyle === 'bold') {
  content = <s-text type="strong">Text</s-text>;
}
```

**Alternative:** Use conditional rendering with explicit literal values:

```jsx
if (textStyle === 'bold' && validTone === 'success') {
  content = <s-text type="strong" tone="success">Text</s-text>;
} else if (textStyle === 'bold') {
  content = <s-text type="strong">Text</s-text>;
} else if (validTone === 'success') {
  content = <s-text tone="success">Text</s-text>;
} else {
  content = <s-text>Text</s-text>;
}
```

### Tone Values

**Valid Tone Values:**
- `'auto' | 'neutral' | 'info' | 'success' | 'warning' | 'critical' | 'custom'`

**Solution:** Map custom appearance values to valid tones with proper TypeScript types:

```typescript
const toneMap: Record<string, 'auto' | 'neutral' | 'info' | 'success' | 'warning' | 'critical' | 'custom'> = {
  'normal': 'auto',
  'accent': 'neutral',
  'subdued': 'neutral',
  'info': 'info',
  'success': 'success',
  'warning': 'warning',
  'critical': 'critical',
  'decorative': 'custom'
};
const textAppearance = String(settings?.text_appearance || 'normal');
const validTone = toneMap[textAppearance] || 'auto';
```

**Usage with Conditional Props:**

```typescript
const paragraphProps: {
  type?: 'paragraph' | 'small';
  tone?: 'auto' | 'neutral' | 'info' | 'success' | 'warning' | 'critical' | 'custom'
} = {};
if (paragraphType !== 'paragraph') paragraphProps.type = paragraphType;
if (validTone !== 'auto') paragraphProps.tone = validTone;

<s-paragraph {...paragraphProps}>
  <s-text {...textProps}>Content</s-text>
</s-paragraph>
```

## Common Errors and Solutions

### Error: "Cannot find module '@lib/utils'"

**Cause:** TypeScript path alias not configured or incorrect import path.

**Solution:** Ensure `tsconfig.json` has path aliases configured:

```json
{
  "compilerOptions": {
    "baseUrl": "../..",
    "paths": {
      "@lib/utils": ["./lib/utils.ts"],
      "@lib/*": ["./lib/*"]
    }
  },
  "include": ["./src", "./shopify.d.ts", "../../lib/**/*"]
}
```

**Then import correctly:**

```typescript
// ✅ Good - use path alias
import { normalizeImageSize, logger, displayDesktopStyle } from '@lib/utils';

// ❌ Bad - relative path won't work
import { normalizeImageSize } from '../../lib/utils';
```

### Error: "Module '@shopify/ui-extensions/preact' has no exported member 'Style'"

**Cause:** Preact extensions don't have a Style API. The Style API is only available in React-based extensions.

**Solution:** Use container query strings instead:

```jsx
// ❌ Bad
import { Style } from '@shopify/ui-extensions/preact';
const displayStyle = Style.default("none").when({ viewportInlineSize: { min: "medium" } }, "block");

// ✅ Good
const displayStyle = '@container (inline-size >= 380px) block, none';
```

### Error: "Type 'string' is not assignable to type 'PaddingKeyword'"

**Cause:** TypeScript infers padding values as `string` instead of specific literal types.

**Solution:** Map values to valid `PaddingKeyword` types and use inline literals:

```jsx
// ❌ Bad
const padding = String(settings?.padding || 'none');
<s-stack paddingBlock={padding}>

// ✅ Good
const paddingMap = {
  'none': 'none',
  'extraTight': 'small',
  'tight': 'small-100',
  'base': 'base',
  'loose': 'large',
  'extraLoose': 'large-100'
};
const safePadding = paddingMap[String(settings?.padding || 'none')] || 'none';
<s-stack paddingBlock={safePadding}>
```

### Error: "Alignment not working"

**Cause:** Using wrong prop for alignment direction.

**Solution:**
- For `direction="inline"` (horizontal layout): Use `justifyContent` for main-axis alignment
- For `direction="block"` (vertical layout): Use `alignItems` for cross-axis (horizontal) alignment

```jsx
// ❌ Bad - alignItems doesn't work for horizontal alignment with inline direction
<s-stack direction="inline" alignItems="center">

// ✅ Good - use justifyContent for horizontal alignment with inline direction
<s-stack direction="inline" justifyContent="center">

// ✅ Good - use alignItems for horizontal alignment with block direction
<s-stack direction="block" alignItems="center">
```

### Error: "Font size setting doesn't work"

**Cause:** `s-text` and `s-paragraph` don't have a `size` prop.

**Solution:** 
- Use `type="small"` for smaller text
- Use `s-heading` for larger text (but changes semantics)
- Accept the limitation and use `tone`/`color` for visual hierarchy

```jsx
// ❌ Bad - size prop doesn't exist
<s-text size="large">Text</s-text>

// ✅ Good - use available options
<s-text type="small">Small text</s-text>
<s-text>Normal text</s-text>
<s-heading>Large text</s-heading>
```

### Error: "Text appearance (color) doesn't work"

**Cause:** Custom appearance values don't match valid `tone` values.

**Solution:** Map custom values to valid tones:

```jsx
const toneMap = {
  'normal': 'auto',
  'accent': 'neutral',
  'subdued': 'neutral',
  'info': 'info',
  'success': 'success',
  'warning': 'warning',
  'critical': 'critical',
  'decorative': 'custom'
};
const validTone = toneMap[textAppearance] || 'auto';

if (validTone !== 'auto') {
  content = <s-text tone={validTone}>Text</s-text>;
}
```

## Settings Configuration

### Settings Organization

**Best Practice:** Organize settings in `shopify.extension.toml` with comment sections matching the visual order and logical grouping:

```toml
[extensions.settings]
# --------------- Container ----------------
[[extensions.settings.fields]]
key = "background"
type = "single_line_text_field"
name = "Background"
[[extensions.settings.fields.validations]]
name = "choices"
value = '["none", "base", "subdued", "transparent"]'

[[extensions.settings.fields]]
key = "border"
type = "single_line_text_field"
name = "Border"
[[extensions.settings.fields.validations]]
name = "choices"
value = '["none", "base base auto", "base base dashed", "base base dotted", "large base auto", "large base dashed", "large base dotted", "large-100 base auto", "large-100 base dashed", "large-100 base dotted", "large-200 base auto", "large-200 base dashed", "large-200 base dotted"]'

[[extensions.settings.fields]]
key = "border_radius"
type = "single_line_text_field"
name = "Border Radius"
[[extensions.settings.fields.validations]]
name = "choices"
value = '["none", "small", "base", "large", "max"]'

# --------------- Content Section ----------------
[[extensions.settings.fields]]
key = "text"
type = "multi_line_text_field"
name = "Text"

# --------------- Padding ----------------
[[extensions.settings.fields]]
key = "padding_block"
type = "single_line_text_field"
name = "Padding Block"
[[extensions.settings.fields.validations]]
name = "choices"
value = '["none", "small", "base", "large"]'

[[extensions.settings.fields]]
key = "padding_inline"
type = "single_line_text_field"
name = "Padding Inline"
[[extensions.settings.fields.validations]]
name = "choices"
value = '["none", "small", "base", "large"]'
```

**Comment Sections:**
- Use `# --------------- Section Name ----------------` format
- Order sections to match visual hierarchy (Container → Content → Padding)
- **Padding settings MUST always be at the bottom** - Place `padding_block` and `padding_inline` as the last settings section
- Group related settings together (e.g., all image settings, all text settings)

**Naming Convention:** All setting keys must use snake_case (e.g., `border_radius`, `author_image_shape`, `inline_alignment`). Never use camelCase for setting keys.

### Border Settings

**Best Practice:** Border settings follow Polaris web component structure with format: `"size-keyword color-keyword style-keyword"` or `"none"`.

**Border Format:**
- Format: `"size-keyword color-keyword style-keyword"` (three space-separated keywords)
- Size keywords: `base`, `large`, `large-100`, `large-200`
- Color keywords: `base` (only option currently)
- Style keywords: `auto`, `dashed`, `dotted`
- Special value: `none` (removes border)

**Example:**
```toml
[[extensions.settings.fields]]
key = "border"
type = "single_line_text_field"
name = "Border"
[[extensions.settings.fields.validations]]
name = "choices"
value = '["none", "base base auto", "base base dashed", "base base dotted", "large base auto", "large base dashed", "large base dotted", "large-100 base auto", "large-100 base dashed", "large-100 base dotted", "large-200 base auto", "large-200 base dashed", "large-200 base dotted"]'
```

**In Code:**
```typescript
import type { BorderKeyword } from '@lib/types';

// Handle border setting - default to 'none' (valid keyword that translates to 'none base auto')
const border = (settings?.border || 'none') as BorderKeyword;

// Use in component
<s-box border={border}>
  {/* content */}
</s-box>
```

**Polaris Web Component Structure:**
The border prop accepts a shorthand string that can be a size, optionally followed by a color, optionally followed by a style. If color is not specified, it defaults to `base`. If style is not specified, it defaults to `auto`. Values can also be overridden by `borderWidth`, `borderStyle`, and `borderColor` props.

**Examples:**
```jsx
// Equivalent:
<s-box border="large-100 base dashed" />
<s-box borderWidth="large-100" borderColor="base" borderStyle="dashed" />

// Default is 'none' (equivalent to 'none base auto')
<s-box border="none" />
```

### Image Size Settings

**Best Practice:** For image size settings with specific ranges, include min/max validation and description. **ALWAYS use `normalizeImageSize` utility** to handle invalid or empty values:

```toml
[[extensions.settings.fields]]
key = "author_image_size"
type = "number_integer"
name = "Author Image Size"
description = "Min: 24px, Max: 100px, Default: 60px"
[[extensions.settings.fields.validations]]
name = "min"
value = "24"
[[extensions.settings.fields.validations]]
name = "max"
value = "100"
```

**In Code (REQUIRED Pattern):**
```typescript
import { normalizeImageSize } from '@lib/utils';

// ✅ ALWAYS use normalizeImageSize - prevents invalid sizes and handles empty values
const authorImageSizeRaw = settings?.author_image_size != null ? parseInt(settings.author_image_size as any, 10) : undefined;
const authorImageSizeNormalized = normalizeImageSize(authorImageSizeRaw, 60); // Default: 60px
// Clamp to specific range if needed (e.g., 24-100px)
const authorImageSize = authorImageSizeNormalized >= 24 && authorImageSizeNormalized <= 100 ? authorImageSizeNormalized : 60;
```

**Why use `normalizeImageSize`:**
- Handles `undefined`/`null` values gracefully (returns default)
- Validates minimum size (default: >= 10px)
- Prevents invalid sizes from breaking the layout
- Provides consistent default value handling across extensions

**❌ Avoid manual validation:**
```typescript
// ❌ Bad - manual validation without normalizeImageSize
const authorImageSize = settings?.author_image_size && settings.author_image_size >= 24 ? settings.author_image_size : 60;
```

### Image Shape Settings

**Best Practice:** Use border radius choices for image shape settings:

```toml
[[extensions.settings.fields]]
key = "author_image_shape"
type = "single_line_text_field"
name = "Author Image Shape"
[[extensions.settings.fields.validations]]
name = "choices"
value = '["none", "small", "base", "large", "max"]'
```

**In Code:**
```typescript
const authorImageShape = (settings?.author_image_shape || 'max') as BorderRadiusKeyword;
// Use with s-image borderRadius prop
<s-image borderRadius={authorImageShape} />
```

### Breakpoint Settings

**Best Practice:** For extensions that need custom breakpoints for responsive display, add a breakpoint setting with min/max validation and description:

```toml
[[extensions.settings.fields]]
key = "breakpoint"
type = "number_integer"
name = "Mobile Breakpoint"
description = "Breakpoint width in pixels when switching to mobile version. Default: 380px"
[[extensions.settings.fields.validations]]
name = "min"
value = "200"
[[extensions.settings.fields.validations]]
name = "max"
value = "640"
```

**In Code:**
```typescript
import { displayDesktopStyle, displayMobileStyle } from '@lib/utils';

// Parse and validate breakpoint (returns integer, not string)
const breakpointRaw = settings?.breakpoint != null ? parseInt(settings.breakpoint as any, 10) : undefined;
const breakpoint = breakpointRaw && breakpointRaw >= 200 && breakpointRaw <= 640 ? breakpointRaw : 380;

// Use with responsive display functions (pass integer, not string)
<s-box display={displayDesktopStyle(breakpoint)}>Desktop content</s-box>
<s-box display={displayMobileStyle(breakpoint)}>Mobile content</s-box>
```

**Recommended Range:**
- Min: 200 (minimum practical breakpoint in pixels)
- Max: 640 (common tablet breakpoint in pixels)
- Default: 380 (standard mobile/desktop breakpoint in pixels)

### Padding Settings

**Recommended Settings Schema:**
```toml
[[extensions.settings.fields]]
key = "padding_block"
type = "single_line_text_field"
name = "Padding Block"
[[extensions.settings.fields.validations]]
name = "choices"
value = '["none", "extraTight", "tight", "base", "loose", "extraLoose"]'
```

**Mapping in Code:**
```typescript
const paddingMap: Record<string, 'none' | 'small' | 'small-100' | 'base' | 'large' | 'large-100'> = {
  'none': 'none',
  'extraTight': 'small',
  'tight': 'small-100',
  'base': 'base',
  'loose': 'large',
  'extraLoose': 'large-100'
};
const paddingBlock = String(settings?.padding_block || 'none');
const safePaddingBlock = paddingMap[paddingBlock] || 'none';
```

### Text Size Settings

**Recommended Settings Schema:**
```toml
[[extensions.settings.fields]]
key = "text_size"
type = "single_line_text_field"
name = "Text Size"
[[extensions.settings.fields.validations]]
name = "choices"
value = '["extraSmall", "small", "base", "medium", "large", "extraLarge"]'
```

**Limitation:** Only `'small'` and `'extraSmall'` map to actual size control. Larger sizes require using `s-heading` (semantic change).

**Mapping in Code:**
```jsx
const textSize = String(settings?.text_size || 'base');
const isSmall = textSize === 'small' || textSize === 'extraSmall';
const useHeading = textSize === 'large' || textSize === 'extraLarge' || textSize === 'medium';

if (useHeading) {
  content = <s-heading>{text}</s-heading>;
} else if (isSmall) {
  content = <s-text type="small">{text}</s-text>;
} else {
  content = <s-text>{text}</s-text>;
}
```

### Text Style Settings

**Recommended Settings Schema:**
```toml
[[extensions.settings.fields]]
key = "text_style"
type = "single_line_text_field"
name = "Text Style"
[[extensions.settings.fields.validations]]
name = "choices"
value = '["normal", "italic", "bold"]'
```

**Mapping in Code:**
```jsx
const textStyle = String(settings?.text_style || 'normal');

if (textStyle === 'bold') {
  content = <s-text type="strong">{text}</s-text>;
} else if (textStyle === 'italic') {
  content = <s-text type="emphasis">{text}</s-text>;
} else {
  content = <s-text>{text}</s-text>;
}
```

### Text Appearance Settings

**Recommended Settings Schema:**
```toml
[[extensions.settings.fields]]
key = "text_appearance"
type = "single_line_text_field"
name = "Text Appearance"
[[extensions.settings.fields.validations]]
name = "choices"
value = '["normal", "accent", "subdued", "info", "success", "warning", "critical", "decorative"]'
```

**Mapping in Code:**
```typescript
const toneMap: Record<string, 'auto' | 'neutral' | 'info' | 'success' | 'warning' | 'critical' | 'custom'> = {
  'normal': 'auto',
  'accent': 'neutral',
  'subdued': 'neutral',
  'info': 'info',
  'success': 'success',
  'warning': 'warning',
  'critical': 'critical',
  'decorative': 'custom'
};
const textAppearance = String(settings?.text_appearance || 'normal');
const validTone = toneMap[textAppearance] || 'auto';

const paragraphProps: { tone?: 'auto' | 'neutral' | 'info' | 'success' | 'warning' | 'critical' | 'custom' } = {};
if (validTone !== 'auto') paragraphProps.tone = validTone;

<s-paragraph {...paragraphProps}>
  <s-text>Text</s-text>
</s-paragraph>
```

## Early Return Pattern

**Best Practice:** Return `null` early if there's no content to render:

```typescript
function Extension() {
  const settings = shopify.settings.value as Settings;
  
  const textBlock = String(settings?.text_block || '');
  if (!textBlock) {
    return null;
  }
  
  // ... rest of component
}
```

**Benefits:**
- Cleaner code flow
- Avoids unnecessary rendering
- Better performance

## Dynamic Values in Text

**Best Practice:** All dynamic values in text labels, descriptions, or any user-facing strings should be wrapped with square brackets `[value]` for placeholder replacement.

**Format:**
- Use `[value]` format for dynamic placeholders (e.g., `[price]`, `[link]`, `[count]`)
- Replace placeholders programmatically in code before rendering
- This makes it clear to merchants which parts of text are dynamic

**Example:**
```typescript
// ✅ Good - use [price] placeholder
const checkboxLabel = String(settings?.checkbox_label || 'Add gift wrapping for [price]');

// Replace placeholder with actual value
const buildCheckboxLabel = () => {
  let label = checkboxLabel;
  if (label.includes('[price]')) {
    label = label.replace('[price]', priceFormatted);
  }
  return label;
};

// Usage
<s-text>{buildCheckboxLabel()}</s-text>
```

**Common Dynamic Values:**
- `[price]` - Formatted currency value
- `[link]` - Clickable link (e.g., terms and conditions)
- `[count]` - Numeric count
- `[name]` - Product or customer name
- `[date]` - Formatted date

**Benefits:**
- Clear indication of dynamic content to merchants
- Easy to identify and replace in code
- Consistent pattern across all extensions

## Rich Text Limitation

**Important:** Rich text editing is **NOT available** in Checkout UI extensions.

**Available Options:**
- Plain text only
- Limited styling via `s-text` and `s-paragraph` props
- No HTML, Markdown, or formatted text support
- Multi-line text: Split by `\n` and map to separate `s-paragraph` elements

**Workaround:**
- Use multiple `s-text` components with different props for formatting
- Use `s-heading` for titles
- Split multi-line text: `textBlock.split('\n').map((line, index) => ...)`
- Accept the limitation and design accordingly

## Best Practices Summary

1. **Use TypeScript** - Define `Settings` interface, use type assertions, leverage type safety
2. **Always validate settings values** - Cast to proper types before use, use `logger.collapse()` for debugging
3. **Map custom values to valid types** - Use `Record<string, LiteralType>` for padding/tone mappings
4. **Early return for empty content** - Return `null` if no content to render
5. **Import utilities from `@lib/utils`** - Use `logger`, `displayDesktopStyle`, `displayMobileStyle`, `normalizeImageSize`
6. **Use `s-paragraph` as wrapper** - Wrap `s-text` elements in `s-paragraph` for block-level text with multi-line support
7. **Use container queries for responsive** - No Style API in Preact extensions, use `displayDesktopStyle()`/`displayMobileStyle()` functions
8. **Call responsive functions with parentheses** - Always call `displayDesktopStyle()` and `displayMobileStyle()` with `()` - they are functions that accept optional breakpoint parameter as integer (default: `380`)
9. **Wrap responsive content in `s-query-container`** - MANDATORY for any component using `displayDesktopStyle()`, `displayMobileStyle()`, or custom `@container` queries
10. **Use `repeat()` for grid columns** - Use `repeat(3, auto)` instead of `auto auto auto` for repeated patterns
11. **Replicate flex behavior with grid** - Use `repeat(auto-fit, minmax(0, max-content))` for multiple inline contents that should flow naturally like flex items
12. **Organize settings with comments** - Use comment sections (`# --------------- Section ----------------`) to group related settings
13. **Use correct alignment props** - `justifyContent` for inline direction, `alignItems` for block direction cross-axis
14. **Accept text styling limitations** - Font size control is very limited, use `s-text` children for style control
15. **Keep code compact** - Always try to keep code compact and readable. Never write verbose code like:
   ```typescript
   // ❌ Bad - verbose and bloated
   if (someConditionThatIsTrue) {
     return;
   }
   
   // ✅ Good - compact and clean
   if (someConditionThatIsTrue) return;
   ```
   - Minimize whitespace and unnecessary line breaks
   - Use inline types instead of separate type aliases when possible
   - Avoid verbose comments - only add comments where they explain *why* something is needed, not *what* it does
   - Keep things simple, not bloated or overcomplicated
   - Code should be readable but compact
16. **Conditional props pattern** - Build props objects conditionally, spread into components
17. **Image size settings** - Include min/max validation and description for numeric image size settings. **ALWAYS use `normalizeImageSize` utility** to handle invalid or empty values, then clamp to specific range if needed
18. **Image shape settings** - Use border radius choices (`none`, `small`, `base`, `large`, `max`) for image shape/border radius settings
19. **Border settings** - Use format `"size-keyword color-keyword style-keyword"` (e.g., `"base base dashed"`, `"large-100 base auto"`) or `"none"`. Default to `'none'` (valid keyword that translates to `'none base auto'`)
20. **Breakpoint settings** - For custom responsive breakpoints, add breakpoint setting (200-640 range, default 380) as integer and pass to `displayDesktopStyle(breakpoint)` and `displayMobileStyle(breakpoint)` functions
21. **Padding settings order** - Padding settings (`padding_block`, `padding_inline`) MUST always be at the bottom of settings sections
22. **HTML elements will NEVER work** - Only Polaris web components (`<s-text>`, `<s-box>`, `<s-grid>`, etc.) work. HTML elements (`<div>`, `<span>`, `<style>`, `<script>`, etc.) will NOT render and will be ignored
23. **Icons** - Use `s-icon` component with `type` attribute for icons (e.g., `<s-icon type="arrow-left" size="small" />`)

## Component Quick Reference

### Text Components
- `s-text`: Inline text (use inside `s-paragraph`), `type` (`'small' | 'strong' | 'emphasis'`), `tone`, `color`
- `s-paragraph`: Block text wrapper (contains `s-text` children), `type` (`'paragraph' | 'small'`), `tone`, `color`
- `s-heading`: Titles, auto-levels by nesting

### Layout Components
- `s-stack`: Flexible container, `direction`, `gap`, `justifyContent`, `alignItems`, `paddingBlock`, `paddingInline`
- `s-box`: Generic container, `padding`, `background`, `border`, `maxInlineSize`, `display`
- `s-grid`: Grid layout, `gridTemplateColumns` (use `repeat(auto-fit, minmax(0, max-content))` for flex-like behavior), `gap`

### Media Components
- `s-image`: Images, `src`, `alt`, `inlineSize`, `objectFit`, `borderRadius`
- `s-icon`: Icons, use `type` attribute (e.g., `type="arrow-left"`, `type="arrow-right"`), `size` (`'small' | 'base' | 'large'`)

### Common Props
- `paddingBlock`: Vertical padding (`PaddingKeyword`)
- `paddingInline`: Horizontal padding (`PaddingKeyword`)
- `gap`: Spacing between children (`SpacingKeyword`)
- `justifyContent`: Main axis alignment (`'start' | 'center' | 'end'`)
- `alignItems`: Cross axis alignment (`'start' | 'center' | 'end' | 'stretch'`)
- `display`: Responsive display (`'auto' | 'none' | '@container...'`)

来源:https://github.com/eyyMinda/directories