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.yamlSchema Reference
Property Types
| Type | Description | Example |
|---|---|---|
string | Text input | Title, URL |
number | Numeric input | Count, size |
boolean | Toggle switch | Show/hide |
select | Dropdown | Layout style |
color | Color picker | Background |
image | Image upload | Hero 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: trueBest 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 SectionNext Steps
- Text Content API — Text integration
- Configuration — Site configuration
- Deployment — Deploy widgets
