Skip to content

Custom Widgets

Partner Documentation

Build configurable widgets that site owners can customize through the DCS portal.

What are Widgets?

Widgets are reusable Vue components that:

  • Accept configuration from the portal
  • Display editable text via useTextContent
  • Can be customized without code changes

Widget Architecture

┌─────────────────────────────────────────┐
│              DCS Portal                 │
│  ┌─────────────────────────────────┐   │
│  │      Widget Configuration       │   │
│  │  - Properties                   │   │
│  │  - Layout options               │   │
│  │  - Text content                 │   │
│  └──────────────┬──────────────────┘   │
└─────────────────┼───────────────────────┘
                  │ API

┌─────────────────────────────────────────┐
│              Your Site                  │
│  ┌─────────────────────────────────┐   │
│  │         Widget Component        │   │
│  │  - Props from config            │   │
│  │  - Text from useTextContent     │   │
│  │  - Styled with Tailwind         │   │
│  └─────────────────────────────────┘   │
└─────────────────────────────────────────┘

Basic Widget

Component Structure

vue
<!-- components/FeatureCard.vue -->
<script setup lang="ts">
import { useTextContent } from '@/lib/use-text-content'

interface Props {
  id: string           // Unique identifier for text keys
  icon: string         // Icon name
  defaultTitle: string
  defaultDescription: string
}

const props = defineProps<Props>()
const { t } = useTextContent()
</script>

<template>
  <div class="feature-card">
    <div class="icon">
      <component :is="iconComponent" />
    </div>
    <h3>{{ t(`features.${id}.title`, defaultTitle) }}</h3>
    <p>{{ t(`features.${id}.description`, defaultDescription) }}</p>
  </div>
</template>

<style scoped>
.feature-card {
  @apply p-6 rounded-lg bg-white shadow-sm;
}
</style>

Usage

vue
<template>
  <div class="features-grid">
    <FeatureCard
      id="fast"
      icon="rocket"
      default-title="Lightning Fast"
      default-description="Deploy in seconds"
    />
    <FeatureCard
      id="secure"
      icon="shield"
      default-title="Secure"
      default-description="Enterprise-grade security"
    />
  </div>
</template>

Configurable Widgets

Define Widget Schema

Create a schema file for portal configuration:

yaml
# .dcs/widgets/testimonials.yaml
name: Testimonials
description: Display customer testimonials
category: Social Proof

props:
  - name: layout
    type: select
    label: Layout Style
    options:
      - value: grid
        label: Grid (3 columns)
      - value: carousel
        label: Carousel
      - value: list
        label: List
    default: grid
    
  - name: showRating
    type: boolean
    label: Show Star Rating
    default: true
    
  - name: maxItems
    type: number
    label: Maximum Testimonials
    default: 3
    min: 1
    max: 12

textKeys:
  - key: testimonials.section.title
    label: Section Title
    default: What Our Customers Say
  - key: testimonials.section.subtitle
    label: Section Subtitle
    default: ""

Configurable Component

vue
<!-- components/Testimonials.vue -->
<script setup lang="ts">
import { useTextContent } from '@/lib/use-text-content'
import { useWidgetConfig } from '@/lib/use-widget-config'

const props = defineProps<{
  widgetId: string
}>()

const { t } = useTextContent()
const { config, isLoading } = useWidgetConfig(props.widgetId)

// Config with defaults
const layout = computed(() => config.value?.layout ?? 'grid')
const showRating = computed(() => config.value?.showRating ?? true)
const maxItems = computed(() => config.value?.maxItems ?? 3)
</script>

<template>
  <section class="testimonials">
    <h2>{{ t('testimonials.section.title', 'What Our Customers Say') }}</h2>
    
    <div :class="layoutClass">
      <TestimonialCard
        v-for="(item, index) in testimonials.slice(0, maxItems)"
        :key="index"
        :testimonial="item"
        :show-rating="showRating"
      />
    </div>
  </section>
</template>

Widget Configuration API

Fetch Widget Config

typescript
// lib/use-widget-config.ts
import { ref, onMounted } from 'vue'

interface WidgetConfig {
  [key: string]: unknown
}

export function useWidgetConfig(widgetId: string) {
  const config = ref<WidgetConfig | null>(null)
  const isLoading = ref(true)
  
  onMounted(async () => {
    try {
      const response = await fetch(
        `${import.meta.env.VITE_DCS_API}/sites/${import.meta.env.VITE_SITE_ID}/widgets/${widgetId}`
      )
      
      if (response.ok) {
        config.value = await response.json()
      }
    } catch (e) {
      console.warn(`Widget config not found: ${widgetId}`)
    } finally {
      isLoading.value = false
    }
  })
  
  return { config, isLoading }
}

Widget Patterns

Dynamic Text Keys

For widgets with multiple items:

vue
<script setup lang="ts">
const items = [
  { id: 'fast', icon: 'rocket' },
  { id: 'secure', icon: 'shield' },
  { id: 'reliable', icon: 'check' }
]
</script>

<template>
  <div v-for="item in items" :key="item.id">
    <Icon :name="item.icon" />
    <h3>{{ t(`features.${item.id}.title`, '') }}</h3>
    <p>{{ t(`features.${item.id}.description`, '') }}</p>
  </div>
</template>

Conditional Sections

vue
<script setup lang="ts">
const { config } = useWidgetConfig('hero')
const showSubtitle = computed(() => config.value?.showSubtitle !== false)
</script>

<template>
  <section class="hero">
    <h1>{{ t('hero.title', 'Welcome') }}</h1>
    <p v-if="showSubtitle">
      {{ t('hero.subtitle', 'Start your journey') }}
    </p>
  </section>
</template>

Responsive Layouts

vue
<script setup lang="ts">
const { config } = useWidgetConfig('features')
const columns = computed(() => config.value?.columns ?? 3)

const gridClass = computed(() => ({
  'grid-cols-1': columns.value === 1,
  'md:grid-cols-2': columns.value >= 2,
  'lg:grid-cols-3': columns.value >= 3,
  'xl:grid-cols-4': columns.value === 4
}))
</script>

<template>
  <div class="grid gap-6" :class="gridClass">
    <slot />
  </div>
</template>

Widget Registry

Register widgets for the portal to discover:

yaml
# .dcs/widgets.yaml
widgets:
  - id: hero
    component: components/HeroSection.vue
    schema: .dcs/widgets/hero.yaml
    
  - id: features
    component: components/FeaturesGrid.vue
    schema: .dcs/widgets/features.yaml
    
  - id: testimonials
    component: components/Testimonials.vue
    schema: .dcs/widgets/testimonials.yaml
    
  - id: cta
    component: components/CallToAction.vue
    schema: .dcs/widgets/cta.yaml

Schema Reference

Property Types

TypeDescriptionExample
stringText inputTitle, URL
numberNumeric inputCount, size
booleanToggle switchShow/hide
selectDropdownLayout style
colorColor pickerBackground
imageImage uploadHero image

Full Schema Example

yaml
name: Hero Section
description: Main hero section with customizable layout

props:
  - name: layout
    type: select
    label: Layout
    options:
      - value: centered
        label: Centered
      - value: left
        label: Left-aligned
      - value: split
        label: Split (Image Right)
    default: centered
    
  - name: backgroundType
    type: select
    label: Background
    options:
      - value: solid
        label: Solid Color
      - value: gradient
        label: Gradient
      - value: image
        label: Image
    default: gradient
    
  - name: backgroundColor
    type: color
    label: Background Color
    default: "#1e3a8a"
    showIf:
      backgroundType: solid
      
  - name: backgroundImage
    type: image
    label: Background Image
    showIf:
      backgroundType: image
      
  - name: showCTA
    type: boolean
    label: Show Call to Action
    default: true
    
  - name: ctaStyle
    type: select
    label: CTA Style
    options:
      - value: primary
        label: Primary Button
      - value: secondary
        label: Secondary Button
      - value: outline
        label: Outline Button
    default: primary
    showIf:
      showCTA: true

textKeys:
  - key: hero.title
    label: Main Title
    default: Welcome to Our Platform
    
  - key: hero.subtitle
    label: Subtitle
    default: Building the future together
    
  - key: hero.cta
    label: Button Text
    default: Get Started
    showIf:
      showCTA: true

Best Practices

1. Sensible Defaults

Always provide good defaults:

vue
const layout = computed(() => config.value?.layout ?? 'grid')

2. Graceful Loading

Show content even before config loads:

vue
<template>
  <section :class="{ 'opacity-50': isLoading }">
    <!-- Content with defaults -->
  </section>
</template>

3. Type Safety

Define TypeScript interfaces:

typescript
interface HeroConfig {
  layout: 'centered' | 'left' | 'split'
  backgroundType: 'solid' | 'gradient' | 'image'
  backgroundColor?: string
  backgroundImage?: string
  showCTA: boolean
  ctaStyle: 'primary' | 'secondary' | 'outline'
}

4. Documentation

Document widget usage:

markdown
## Hero Section Widget

### Usage
\`\`\`vue
<HeroSection widget-id="home-hero" />
\`\`\`

### Configuration
Configure in Portal → Widgets → Hero Section

Next Steps

Duff Cloud Services Documentation