This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Weave is a component library for building web UIs in F# using WebSharper. It provides reactive UI components, layout primitives, and a theming system (light/dark). Status: experimental, no NuGet packages yet.
Initial setup:
./build.sh init # Linux/macOS
./build.cmd init # WindowsStart the docs site (view components interactively at http://localhost:5000):
dotnet run --project src/Weave.Docs/Weave.Docs.fsprojCompile CSS (required after SCSS changes):
yarn build:css # one-shot
yarn watch:css # watch modeRun all tests:
./build.sh RunTests # Linux/macOS (requires .NET 10, Node 22)
./build.cmd RunTests # Windows
# Or via Docker (matches CI exactly):
docker compose build && docker compose run --rm playwright-testsRun a single unit test project:
dotnet test tests/Weave.Tests.Unit/Weave.Tests.Unit.fsprojRun a single rendering test project:
dotnet test tests/Weave.Tests.Rendering/Weave.Tests.Rendering.fsprojCheck/apply code formatting (Fantomas):
dotnet fantomas . # format all files
dotnet fantomas --check . # verify formatting (what CI runs)Other FAKE build targets: Clean, Restore, Build, BuildDocs, Analyze, CheckFormat
src/
Weave/ # Core library (netstandard2.0)
Helpers.fs # View/Doc/Attr combinators
Theming.fs # Light/Dark theme system, PaletteColor
components/Core.fs # TypedCssClasses (Css), CssHelpers (cl, cls), HTML aliases
components/Utilities.fs # DocumentEventListener, ResizeListener, Disabled
components/*.fs # ~30 UI components
scss/ # SCSS source (compiled to styles.css)
Weave.Docs/ # Interactive docs site (net10.0, ASP.NET Core + WebSharper)
examples/ # One example file per component
ExamplesRouter.fs # Client-side SPA routing
tests/
Weave.Tests.Unit/ # Expecto unit tests (verify toClass mappings)
Weave.Tests.Rendering/ # Playwright layout tests + HTML fixtures
build/
Build.fs # FAKE build targets
Compilation order matters in F# — files in .fsproj must be ordered so dependencies come first. New components go after Core.fs and any components they compose.
Every component follows this pattern:
namespace Weave
open WebSharper
open WebSharper.UI
open WebSharper.UI.Client
open WebSharper.UI.Html
open Weave.CssHelpers
[<JavaScript>]
module MyComponent =
[<RequireQualifiedAccess; Struct>]
type Variant = | Filled | Outlined
module Variant =
let toClass variant =
match variant with
| Variant.Filled -> Css.``weave-mycomponent--filled``
| Variant.Outlined -> Css.``weave-mycomponent--outlined``
// Color module uses global BrandColor DU (no RequireQualifiedAccess needed on Color)
module Color =
let toClass color =
match color with
| BrandColor.Primary -> Css.``weave-mycomponent--primary``
// ...
open MyComponent
[<JavaScript>]
type MyComponent =
static member Create(innerContents: Doc, ?enabled: View<bool>, ?attrs: Attr list) =
let enabled = defaultArg enabled (View.Const true)
let attrs = defaultArg attrs []
div [
cl Css.``weave-mycomponent``
View.not enabled |> Attr.DynamicClassPred Css.``weave-mycomponent--disabled``
yield! attrs
] [ innerContents ]Key rules:
[<JavaScript>]on every module and type (required for WebSharper transpilation)- No styling parameters (
variant,color,size) onCreate— callers pass them via?attrsusingtoClasshelpers ?attrs: Attr listis always the last optional parameter; defaults to[];yield! attrscomes last so callers can extend- Reactive parameters: use
Var<'T>for two-way bindings,View<'T>for read-only reactive inputs; avoid plain values for anything that can change - CSS helpers:
clfor a single class,cls [...]for multiple — never chainclcalls whereclsapplies - DU types for variants/sizes use
[<RequireQualifiedAccess; Struct>]to prevent name collisions;Colormodules use globalBrandColorDU Attr.DynamicClassPredgates a modifier class on aView<bool>
- Files:
src/Weave/scss/components/_{name}.scss(underscore prefix, lowercase) - BEM naming:
.weave-{name},.weave-{name}--modifier,.weave-{name}__element - Never hard-code colors — always use
var(--palette-*),var(--typography-*),var(--default-*) - Color modifiers use the SCSS loop:
@each $color in $palette-colors { ... } - Register new component SCSS in
src/Weave/scss/main.scss(alphabetical order within the// Componentsblock) - After SCSS changes, run
yarn build:cssto regeneratestyles.css
When asked to scaffold a new component, use the /scaffold-component skill — it provides the full step-by-step checklist. The high-level steps are:
src/Weave/components/{Name}.fs— component module + typesrc/Weave/scss/components/_{name}.scss— BEM styles- Register SCSS import in
src/Weave/scss/main.scss - Register F# file in
src/Weave/Weave.fsproj src/Weave.Docs/examples/{Name}Examples.fs— interactive docs examples- Register example in
src/Weave.Docs/Weave.Docs.fsprojandExamplesRouter.fs(4 places:PageDU,pageToString,renderPage, nav list) tests/Weave.Tests.Unit/{Name}Tests.fs— Expecto tests fortoClassmappingstests/Weave.Tests.Rendering/{Name}LayoutTests.fs— Playwright layout teststests/Weave.Tests.Rendering/fixtures/{name}.html— static HTML fixture using compiled CSStests/Weave.Tests.E2E/accessibility/{Name}Tests.fs— axe-core scan + keyboard/focus tests- Register E2E page in
tests/Weave.Tests.E2E.Site/Pages.fs - Run
dotnet fantomas .to format
Unit tests (Expecto) — test pure toClass mappings only, never DOM:
module Weave.Tests.Unit.{Name}Tests
open Expecto
open Weave
[<Tests>]
let {name}Tests =
testList "{Name}" [
testTheory "each variant maps to the correct class" [
MyComponent.Variant.Filled, "weave-mycomponent--filled"
]
<| fun (variant, expected) -> Expect.equal (MyComponent.Variant.toClass variant) expected ""
]Rendering tests (Playwright/xUnit) — load a static HTML fixture, assert on layout:
- Inherit
PageTest(), callthis.LoadFixture()at the start of every[<Fact>] - Use
BoundingBoxAsync()for positional assertions; allow ±1px tolerance - Use
Page.EvaluateAsync<string>for computed styles on hidden elements - Fixtures link to
../../../src/Weave/styles.cssand use BEM classes directly
E2E accessibility tests (Playwright/xUnit + axe-core) — test against live WebSharper-rendered pages:
- Inherit
E2ETestBase(server)(providesNavigateTo,RunAxeScan,Expect) - Every component needs at least
this.RunAxeScan("{name}")for automated a11y scanning - Prefer typed Playwright assertions over
EvaluateAsyncJS strings: usethis.Expect(locator).ToBeFocusedAsync(),.ToBeCheckedAsync(),.ToHaveAttributeAsync(),.ToHaveClassAsync(Regex(...)),.ToHaveCountAsync(),.ToBeVisibleAsync(),.ToBeHiddenAsync() - Playwright assertions auto-retry, so they replace
WaitForFunctionAsync+EvaluateAsyncin most cases - Only use
EvaluateAsyncwhen no typed API exists (e.g. DOM containment, injecting event listeners) - Mark unimplemented keyboard features with
[<Fact(Skip = "Known gap: ...")>]
Helpers.fs:Viewextensions (mapCached,zip,not),ViewOptionmonad,Attr.bindOption,Attr.classSelectionCssHelpers(inCore.fs):cl,cls,Css(TypedCssClasses),Palettemodule (CSS var refs),Attr.toggleStyleOrDefaultUtilities.fs:DocumentEventListener(click-outside detection),ResizeListener(ResizeObserver),Disabledclass helperTheming.fs:ThemeMode(Light/Dark),ThemePalette,PaletteColor