Skip to content

Repository files navigation

my-widget

Standalone Fluid widget project generated by @fluid-app/fluid-cli-widget.

This project builds and publishes a widget package only. It does not scaffold droplets, a full portal application, or backend services.

Scripts

pnpm dev                              # Run local widget protocol endpoints
pnpm typecheck                        # Run TypeScript checking
pnpm validate                         # Validate package metadata with the Fluid widget CLI
pnpm build                            # Validate and build runtime artifacts
pnpm run widget:link                  # Link this project to an existing Fluid droplet
pnpm run widget:publish               # Publish to the linked droplet
pnpm run widget:publish --dry-run     # Build upload payload without uploading

Project structure

.
├── AGENTS.md                         # Portable AI authoring guidance
├── CLAUDE.md                         # Claude-compatible AI authoring guidance
├── .agents/skills/                   # Agent skill docs for this project
├── .claude/skills/                   # Claude skill docs for this project
├── fluid.widget.config.ts            # Widget CLI ownership + package exports
├── manifest.ts                       # defineWidget/defineWidgetPackage metadata
├── styles.css                        # Shadow-root runtime styles
├── src/index.ts                      # Remote DOM worker entry
└── src/widgets/review-carousel/ReviewCarousel.tsx

Local development endpoints

pnpm dev exposes:

  • /__widget-packages__: the local canonical package descriptor consumed by Fluid hosts.
  • /__runtime-entry__: the Remote DOM worker entry referenced by that descriptor.

The standalone project does not include a second host or iframe preview app. Portal and builder hosts render the worker through the shared Remote DOM runtime.

The standalone source of truth is manifest.ts, with droplet ownership in fluid.widget.config.ts. Company portal projects instead use src/widgets.config.ts and fluid portal deploy.

Package boundary

Stay inside the standalone widget package:

  • Put reusable widget components under src/widgets/.
  • Put package and palette metadata in manifest.ts.
  • Keep runtime registration in src/index.ts.
  • Keep runtime styles in styles.css or CSS files imported by widget modules.
  • Do not add Next.js app folders, Rails code, API servers, portal app shells, or droplet scaffolding.

Droplet ownership is managed by the Fluid widget CLI through fluid.widget.config.ts. Link the project with pnpm run widget:link or pass a droplet option to CLI commands when needed.

Authoring widgets

Widgets are authored with defineWidget() and grouped with defineWidgetPackage() from @fluid-app/portal-sdk/widgets/worker.

defineWidget() describes one widget:

  • name: stable URL-safe widget name. Changing it changes the generated widget type.
  • component: React component rendered inside the Remote DOM worker.
  • displayName, description, icon, category: builder palette metadata.
  • defaultProps: JSON-serializable defaults for new widget instances.
  • propertySchema: builder property panel fields.
  • container: block, card, inline, or fullscreen.
  • uses: every typed portal function called by this widget. Pass the function values instead of writing capability names.
  • resizable: false/omitted, true, horizontal, vertical, both, or an object with horizontal/vertical flags and optional minimum sizes.

defineWidgetPackage() describes the package:

  • Keep packageType set to droplet for this template.
  • Keep widgets as the array of widgets exported by this package.
  • Use a SemVer version without build metadata.
  • Do not manually set packageStableId in this generated droplet template; the CLI injects the linked droplet key during validation, build, and publish.
  • Do not author runtime artifact URLs. Dev and build inject the served workerEntryUrl.

Manifest structure

manifest.ts should remain the single source of package metadata:

import {
  defineWidget,
  defineWidgetPackage,
} from "@fluid-app/portal-sdk/widgets/worker";
import { ExampleWidget } from "./src/widgets/example/ExampleWidget";

export const exampleWidget = defineWidget({
  name: "ExampleWidget",
  component: ExampleWidget,
  displayName: "Example Widget",
  description: "A focused reusable Fluid widget.",
  icon: "box",
  category: "components",
  container: "card",
  defaultProps: {
    title: "Featured content",
  },
  propertySchema: {
    tabsConfig: [{ id: "content", label: "Content" }],
    dataSourceTargetProps: ["title"],
    fields: [
      {
        key: "title",
        label: "Title",
        type: "text",
        defaultValue: "Featured content",
        tab: "content",
        group: "Copy",
      },
    ],
  },
});

export const widgetPackage = defineWidgetPackage({
  scope: "droplet",
  version: "0.1.0",
  packageType: "droplet",
  widgets: [exampleWidget],
});

export const widgetPackages = [widgetPackage] as const;
export default widgetPackage;

Typed portal functions

Worker code imports typed portal functions from @fluid-app/portal-sdk/widgets/worker, calls them directly, and lists the same function values in the widget's uses array:

import {
  defineWidget,
  getUserAccount,
  navigateTo,
} from "@fluid-app/portal-sdk/widgets/worker";

async function openProfile(): Promise<void> {
  const account = await getUserAccount();
  await navigateTo(account.slug);
}

export const profileButtonWidget = defineWidget({
  name: "ProfileButton",
  component: ProfileButton,
  uses: [getUserAccount, navigateTo],
});

Available built-ins are getUserAccount, getStore, getPortalApp, getPortalProfile, getNavigationState, buildPortalHref, navigateTo, getFullscreenState, requestFullscreen, and exitFullscreen. Getters await and return resolved data instead of exposing query-status snapshots. A navigation target may be a slug string, { slug }, or { href }.

defineWidget() derives the existing descriptor declarations from uses. Both the worker and host enforce those declarations. If a call fails with PortalFunctionError code NOT_DECLARED, add that function to the widget's uses array. Do not author a raw capabilities array.

getUserAccount() returns an allowlisted account view. It may include email, but excludes phone numbers, addresses, payment details, credentials, government or tax identifiers, dates of birth, raw metadata, and unknown future fields. The host's tenantClient is not exposed to widgets.

For company-specific operations, define a shared typed function with definePortalFunction<Output, Input>({ capability, version, method }), call it directly, and include it in uses. The portal owner installs a typed host handler with implementPortalFunction(function, handler) in remoteWidgets.functions. Built-ins cannot be overridden; duplicate custom implementations are rejected.

Custom inputs and outputs must be JSON values: null, booleans, finite numbers, strings, arrays, and objects composed from those values. Do not use undefined, functions, Dates, Maps, Sets, class instances, NaN, or Infinity. The runtime validates the protocol boundary as well as the TypeScript contract.

Property schema reference

Property schemas define editable builder controls. They must be JSON-serializable.

Top-level schema keys:

  • tabsConfig: optional array of tab objects with id and label.
  • fields: array of controls shown in the builder property panel.
  • dataSourceTargetProps: prop keys that can be populated from configured data sources.
  • itemConfigSchema: optional per-item configuration schema for custom data-source selections.
  • validate: optional custom validator in in-app manifests; avoid it for published standalone metadata because package metadata must be serializable.

Base field keys:

Key Purpose
key Widget prop key written by the field. Use a unique non-prop key for visual-only controls.
label Builder label.
type Supported field type listed below.
description Optional helper text.
defaultValue Optional JSON-serializable default.
tab Optional tab id matching tabsConfig.
group Optional group label inside a tab.
advanced Marks theme overrides or low-frequency settings; advanced fields render in Custom styling.
requiresKeyValue Optional conditional display rule. An array means all conditions must match.

Supported field types:

Type Use Extra keys
text Single-line string placeholder, maxLength, tokenSuggestions
textarea Multi-line string placeholder, rows, maxLength
number Number input min, max, step
boolean Toggle None
select Dropdown options array with label and value
color Basic color value None
range Slider Required min, max; optional step
dataSource Data-source selector/configuration None
resource Single shareable/resource picker allowedTypes
image Media picker accept as image, video, or any
alignment Alignment picker options.verticalEnabled, options.horizontalEnabled
slider Numeric slider min, max, step, unit
colorPicker Color picker swatches
sectionHeader Visual section heading subtitle
separator Visual divider None
buttonGroup Segmented choice options with value, optional label, ariaLabel, icon
colorSelect Semantic theme color picker excludeColors
sectionLayoutSelect Visual section layout picker None
background Combined background resource/color control Resource/color field keys as needed
contentPosition 3-by-3 content position picker None
textSizeSelect Theme text-size picker None
cssUnit Number with unit selector allowedUnits, defaultUnit, minByUnit, maxByUnit, stepByUnit
fontPicker Google font picker placeholder
stringArray Editable list of strings placeholder
borderRadius Composite four-corner radius editor keys.topLeft, keys.topRight, keys.bottomLeft, keys.bottomRight
screenPicker Portal screen picker includeSystemItems

Data-source-ready props

For props that may later be connected to data sources:

  • Add the prop key to dataSourceTargetProps.
  • Keep the component tolerant of missing, empty, or partially populated values.
  • Render useful empty states for empty arrays and failed mapping.
  • Keep prop values JSON-serializable.
  • Avoid fetching inside the component when a prop or data source can provide the data.
  • Use itemConfigSchema when each selected item needs widget-specific settings.

Component quality bar

  • Use TypeScript interfaces for public props.
  • Provide defaults for optional props.
  • Treat incoming props as untrusted and validate before reading nested values.
  • Keep render logic deterministic and side-effect free.
  • Clean up effects and subscriptions.
  • Keep bundles small; avoid heavy libraries unless the widget requires them.
  • Avoid secrets, tenant credentials, local URLs, and environment-only assumptions.
  • Prefer semantic HTML over div-only structures.

Accessibility

  • Use headings, sections, lists, buttons, labels, and form elements semantically.
  • Make every interactive element keyboard reachable.
  • Provide visible focus states.
  • Add aria-label to icon-only controls.
  • Add alt text to meaningful images and empty alt text to decorative images.
  • Preserve logical heading order inside the widget.
  • Use ARIA live regions for important dynamic updates.
  • Respect reduced-motion preferences for animation-heavy widgets.
  • Use semantic foreground/background token pairs for contrast.

Runtime CSS and theme tokens

Fluid hosts provide semantic CSS variables. Prefer token-based CSS instead of hard-coded palettes.

Common CSS variables:

  • Surfaces: --background, --card, --popover.
  • Text: --foreground, --card-foreground, --popover-foreground.
  • Brand/actions: --primary, --primary-foreground.
  • Supporting UI: --secondary, --secondary-foreground, --muted, --muted-foreground, --accent, --accent-foreground.
  • Status/chrome: --destructive, --destructive-foreground, --border, --input, --ring.
  • Charts: --chart-1, --chart-2, --chart-3, --chart-4, --chart-5.
  • Radius: --radius, --radius-sm, --radius-md, --radius-lg, --radius-xl.
  • Theme-engine aliases may include --font-header, --font-body, --font-size-extra-small, --font-size-small, --font-size-regular, --font-size-large, --font-size-extra-large, and --font-size-giant.

Tailwind equivalents, when Tailwind is available, are semantic utilities such as bg-background, text-foreground, bg-card, text-card-foreground, bg-primary, text-primary-foreground, border-border, ring-ring, rounded-lg, text-sm, and text-xl. This standalone template uses plain CSS for runtime portability.

Light/dark behavior:

  • Fluid themes update variables for light and dark mode.
  • Explicit dark mode is represented by data-theme-mode="dark" on a host element.
  • Some hosts also use system dark mode when no explicit mode is selected.
  • Prefer semantic variables so both modes work automatically.
  • If mode-specific styling is unavoidable, scope it with [data-theme-mode="dark"] and keep values token-based.

Runtime CSS rules:

  • Import runtime CSS from the guarded build import in src/index.ts or another worker module included in the build.
  • Prefix selectors with the widget name to prevent host leakage.
  • Do not rely on body styles for published runtime rendering.
  • If published styles are missing, check that the CSS import is reachable from the built entry.

Validation, build, and publish preflight

Before shipping changes, run:

pnpm typecheck
pnpm validate
pnpm build
pnpm run widget:publish --dry-run

Publish only after the dry run succeeds:

pnpm run widget:publish

Build output is written under .fluid/widget-dist/. The runtime entry starts the package in a worker with startWidgetPackage(), while hosts discover its canonical descriptor through the widget catalog.

Common failures

Failure Fix
Missing droplet UUID Run pnpm run widget:link or pass a droplet option to validate/build/publish.
No source package found Ensure fluid.widget.config.ts exports widgetPackage or widgetPackages from manifest.ts.
Invalid package type Keep packageType: "droplet".
Invalid package key or widget name Use URL-safe letters, numbers, dots for package keys, and underscore/hyphen/tilde where allowed.
Invalid version Use SemVer without build metadata.
Non-serializable metadata Remove functions, undefined values, Dates, NaN, Infinity, Maps, Sets, and class instances from propertySchema and defaultProps.
Relative CSS URL rejected Import CSS into the bundle or remove manual cssUrls entries.
Styles are absent after publish Confirm styles.css is reachable from the guarded build import in src/index.ts.
Published widget fails Guard browser-only APIs, remove local-only URLs, and avoid environment variables required at runtime.

About

Starter template for standalone Fluid Commerce widgets — used by `fluid widget create`.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages