From goo-ui
Provides Goo UI authoring rules, surface map, and engine facts for s&box projects. Use when writing or modifying Goo-based UI.
How this skill is triggered — by the user, by Claude, or both
Slash command
/goo-ui:goo-uiThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
This skill matches goo library version 1.0.280094. If the project's `Libraries/xaz.goo/.version` differs, warn the user the skill may be stale and let the live library and its docs win.
This skill matches goo library version 1.0.280094. If the project's Libraries/xaz.goo/.version differs, warn the user the skill may be stale and let the live library and its docs win.
AddRange items and the obvious interactive child, then leave one lone plain sibling unkeyed - a message wrapper next to a keyed status dot, a swatch row next to keyed slider rows. Key the lone wrapper too, or key none. Prefer Children.AddRange(...) which auto-keys. Full mechanics, uniqueness rule, and why: references/engine-facts.md entry 1.StartAngle, EndAngle, radii, polygon points) re-bakes a mask texture each frame. Sweep fixed geometry with Transform = PanelTransform.Rotate(...) instead.Container across rebuilds. Its children list comes from a per-build pool; extract a function that builds it fresh.Compiling is not done. Two gates, both required before you call the work finished:
file-scoped helper type used in a non-file member signature (CS9051), a readonly struct with settable fields (CS8340), and a blob type that collides with Sandbox.UI (new TextEntry is CS0104 ambiguous - qualify Goo.TextEntry).Children list and .Add(...)/.AddRange(...) sequence for partial keying before declaring done.The behavioral model is the easy part; the property/type names are where output fails to build. Burn these in:
Text colors text with FontColor, NEVER Color. Text (and Container) have no Color property - only FontColor (plus HoverFontColor/ActiveFontColor and a Style preset bundle). Color is a CSS/Sandbox.UI prior and exists on no blob. This is the single most-repeated mistake - if you typed Color = inside a new Text {...} or new Container {...}, it is wrong; change it to FontColor.MathF/Math need an explicit using System; - the s&box compiler has NO implicit usings. A file that calls MathF.Sin/MathF.Cos/MathF.Sqrt/MathF.Atan2/MathF.PI or Math.Clamp etc. without using System; fails with CS0103 ("the name 'MathF' does not exist"), and the canonical header below does NOT import System - add it yourself the moment you touch trig or math. (Vector2/MathX/Color/Game come from Sandbox; MathF/Math/TimeSpan are in System.) Prefer engine helpers where they exist - value.Clamp(lo, hi), MathX.Lerp/LerpDegrees/DegreeToRadian - but trig still routes through System.MathF.Text blob drops a mid-string ASCII colon followed by more text. The engine label renderer runs an emoji/shortcode pass that eats a : sitting between characters: a clock "05:28" renders "0528", a ratio "3:1" renders "31". (A trailing colon like "Save as:" is fine.) For any in-string colon - timers, ratios, score lines - use U+2236 RATIO ∶ (visually identical): $"{m:D2}∶{s:D2}", or split the parts into separate Text blobs with a literal separator. This is a render bug, not a format-string bug - the C# string provably contains :.Padding is a single Length? - no Thickness/Edges overload, and there is no TextWrap. Padding = new Thickness(...) / new Edges(...) does not bind; for per-side padding use PaddingLeft/PaddingTop/PaddingRight/PaddingBottom. Text wrapping is WhiteSpace/WordBreak, never TextWrap. Padding (and Border*, Overflow*, Gap, TransformOriginX/Y) are Container/TextEntry-only - they do NOT exist on Text, Image, or shape blobs; to pad/border a Text, wrap it in a Container. When unsure whether a property exists on a given blob, check references/style-properties.md (the per-blob property index) - do not invent.TextAlign takes the TextAlign enum, never TextFlag. new Text("x") { TextAlign = TextAlign.Center } - TextFlag is the Canvas immediate-draw enum and does not bind to the style property. (You rarely need it anyway: to center a Text inside a box, set AlignItems/JustifyContent on the parent Container - that centers the text panel.)Controls, Shapes, and Layout are static classes, not namespaces. using Goo.Controls; / using Goo.Shapes; / using Goo.Layout; is a compile error. Qualify them: Controls.Button(...), Shapes.Disc(...), Layout.Anchor.TopCenter (with using Goo;).Goo.Controls.Button(label, onClick) takes an Action, not Action<MousePanelEvent>. Write Controls.Button("Save", () => _save()), never e => .... (Raw blob handlers like OnClick DO take Action<MousePanelEvent> - only the Controls.Button/Slider callbacks are parameterless Action/Action<float>.)Hud.*/Controls.* results are init-only. You cannot assign .OnClick = ... after construction. Add a handler to a factory result with a with expression: Hud.Scrim(color) with { OnClick = e => Close() }, or build the Container directly with OnClick inside the initializer.HoverOpacity (or any Hover/Active/Focus opacity). State variants are Hover/Active/Focus × BackgroundColor/FontColor only. Animate opacity with Opacity + TransitionMs, or fade via a color's alpha.PanelTransform.Translate is (Length, Length) [or (Length, Length, Length)] only - no (float, float, unit:) overload. Length.Percent(x) returns a Length?, so unwrap it: PanelTransform.Translate(Length.Percent(-50) ?? default, Length.Percent(-50) ?? default) is the codebase's centering idiom.Goo.Animation - add using Goo.Animation;. SpringFloat/SmoothFloat/DecayFloat and their Color/Vector2 variants are NOT in the Goo namespace, and using Goo; does not pull them in (compile error: type not found). The sampled field is .Current - there is no .Value. Construct new SpringFloat(initial, frequency, damping); advance with .Tick(dt) inside Tick (chain multiple dampers with |, not ||, so each advances every frame); drive it by setting .Target or kicking .Velocity; read .Current in Build.Cell.Mount<TCell>(...) returns a CellElement, not your cell. You cannot hold the returned value as TCell, store the instance in a field, or call methods on it - the reconciler owns the instance and creates it via the factory. To coordinate a cell from outside (reset, push a value), thread it through configure (runs every diff, wins over seed): e.g. the parent owns a target/generation value and configure: c => c.Target = _value, and the cell honors it each rebuild. There is no OnMount override on Cell<TRoot> - only Build() and Rebuild(). Timing trap: configure runs during the reconciler's Diff, which fires after your parent Build() returns. So a "reset generation" counter bumped in a handler must NOT be cleared at the end of Build() (it would be zero by the time configure reads it, and the reset never happens). Either make the reset idempotent (don't clear the flag), or clear it at the top of the next handler.Sector, not an Arc. Sector has InnerRadius/OuterRadius (0..1) - use it for filled discs, rings, pie wedges. Arc has Radius/StrokeWidth (0..1) - a stroked line only, no inner/outer. Putting InnerRadius/OuterRadius on an Arc does not compile.GooPanel has no Razor lifecycle. There is no OnAfterTreeRender, OnInitialized, or OnParametersSet - overriding one is a compile error (CS0115) and your init silently never runs. The lifecycle is: field initializers, OnEnabled() (seed data here), Tick(float dt) (per frame, before the build gate), Build(). For one-time setup use OnEnabled or a bool _seeded flag checked at the top of Build.FlexGrow/FlexShrink, never Grow/Shrink. The CSS-style short names bind to nothing on any blob. Spacer idiom: new Container { FlexGrow = 1 }.#RRGGBB into Color.FromBytes hex arguments. This reliably produces mangled bytes like FromBytes(0x2A, 0x6DB5, 0xFF) - it compiles (int args) and renders garbage. Write new Color(r, g, b) with 0-1 floats, or Color.Parse("#2A6DB5").Value, or decimal bytes FromBytes(42, 109, 181).key: to Controls.Slider. The key pins reconciler identity so the thumb's Active state survives the rebuild every onChanged triggers; without it continuous dragging breaks. Controls.Slider(value: v, min: 0, max: 1, step: 0.01f, onChanged: ..., key: "vol").TransformOrigin. TransformOrigin = "left center" does not compile; the facade has TransformOriginX/TransformOriginY (Length?): TransformOriginX = Length.Percent(0), TransformOriginY = Length.Percent(50).Hud.Overlay() as the panel root or Hud.Scrim(color) - there is NO PositionMode.Fixed. s&box has only Static/Relative/Absolute, and containers default to Static. An Absolute layer (which Hud.Overlay/Hud.Scrim are) positions against the nearest non-static ancestor, escaping static parents up to the screen. Two consequences: (a) PositionMode.Fixed does not exist - do not reach for it (it's the CSS prior, a compile error); (b) a screen scrim placed under a Position = Relative parent is clipped to that parent's box, because Relative makes the parent the containing block. Robust pattern: Hud.Overlay() as the root with the scrim + a centered card as its children; pair SwallowClick = true on the card (rule 16). A bare backdrop set Position = Absolute inside a fixed-width panel only covers that panel.The primer below spells out layout, styling, state, and basic animation. For everything else it only summarizes - and a plausible-looking guess at those APIs is almost always a compile error. Before writing any code that touches one of these, READ the matching file first:
references/input.md (KeyTracker: Poll() per frame, Pressed("w") string engine names - there is no KeyCode enum and no Input.Down(KeyCode))references/tokens.md (Tokens.Scope<T>(dictionary, () => body) wraps the subtree lexically; there is no Scope(name, value))references/drag-and-drop.md (DragSource/DropZone/DragLayer - never hand-roll mouse-event dragging)references/composition.md and references/cells.md (Cell<TRoot> has only Build()/Rebuild() - no Tick, no OnMount; per-frame state lives on the GooPanel)references/animations.md (tweens, timelines, AnimationSet, age phases)references/events.mdreferences/overlay-layout.mdAnd before calling ANY Goo factory or helper whose exact parameter list you have not seen in this file, check it in references/api-signatures.md (the generated one-line-per-member index of the whole public API). Do not guess parameter lists; e.g. Controls.Slider requires a step argument. For style properties (what each blob accepts in { ... }), the authority is references/style-properties.md - check it before putting any property on a Text, Image, or shape blob, since their property sets are far smaller than Container's.
goo is a C#-only retained UI framework over Sandbox.UI.Panel for s&box. Build() returns a tree of blob value-structs; goo diffs it against the previous tree and applies minimal ops to the engine panels. drop the React/Razor priors: there is no markup, no stylesheet, no every-frame render loop. the tree is plain C#, rebuilt only when asked.
reach for these before hand-rolling anything:
| need | use |
|---|---|
| layout, style, events | Container plus blobs: Text, Image, Sector, Arc, Polygon, ScenePanel, SvgPanel, WebPanel, TextEntry |
| full-screen HUD scaffolding | Hud.Overlay(), Hud.Anchored(anchor, content), Hud.Fill(), Hud.Scrim(color), Hud.Wallpaper(tex, path), Hud.Spacer(), Hud.Divider() |
| corner pinning math | Layout.Anchor (4 corners, top/bottom center, center) via Hud.Anchored |
| rings, discs, wedges | Goo.Shapes (Ring, Disc) and the raw shape blobs |
| ready-made controls | Goo.Controls.Button(...), Goo.Controls.Slider(...) |
| encapsulated stateful widgets | Cell.Mount<TCell>(key, seed, configure) - see composition |
| dampers and tweens | Goo.Animation: Spring/Smooth/Decay x Float/Color/Vector2, Tween, Animator, Timeline, Easing |
| custom shaders on a panel | Effect = new ShaderEffect("shaders/x.shader", GrabMode.None/Sharp/Blurred) { ["Uniform"] = value } |
| keyboard | Goo.Input.KeyTracker (Poll() per frame, JustPressed, modifiers) - see input |
| drag and drop | DragSource / DropZone / DragLayer - see drag-and-drop |
| drag math under UI scale | PointerDrag with Current(Panel) |
| theming values | Tokens.Scope / Tokens.Get - see tokens |
C# only. no Razor, no SCSS, no markup, no DSL.
only the ten blob types exist. do not invent or subclass blob types. helpers like Shapes.Ring return Container subtrees.
init-only property syntax. no fluent chains; .Padding(8) does not exist. properties go inside { } on construction. to extend an already-built blob use a with expression (this is your style-spread); with shallow-copies, so the copy shares the original's Children list - call helpers fresh per use.
children go in Children = { ... }. mixing init properties with bare child items in one brace block is a CS0747 compile error. after construction Children is a live list: Add, AddRange(items, (i, item) => ...) (auto-keys by index; your Key wins), AddIf(cond, child).
mount by subclassing GooPanel<TRoot> on a GameObject under a ScreenPanel or WorldPanel. no manual instantiation, no Render().
Build() does not run every frame. it runs at mount, hotload, re-enable, and on rebuild. event handlers trigger a rebuild automatically after they run, so OnClick = e => _count++ is complete - no Rebuild() call needed in handlers. call Rebuild() only when state changes outside a handler (timers, network, polls). for continuous motion override Tick(float dt) and return true while moving.
state lives in fields on the GooPanel (or Cell) subclass. no hooks, no observables, no binding. for a reusable widget with private state, subclass Cell<TRoot> and mount with Cell.Mount<TCell>(seed: c => c.Initial = x, configure: c => c.Label = y) - seed runs once at first mount, configure runs every rebuild and wins on overlap.
state variants are free. HoverBackgroundColor, ActiveFontColor, FocusBackgroundColor etc resolve engine-side with no rebuild. never wire OnMouseEnter/OnMouseLeave just for visual feedback. pair with TransitionMs for fades. declare the resting BackgroundColor and its variant together on the same blob.
compose controls. a button is a Container with OnClick and a hover variant; Goo.Controls covers button/slider; everything else is function extraction: static Container Card(string title) => new Container { ... };. repeated property bundles are a missing helper, not a style class. promote repeated literals to a theme constants class.
never hold a Container across rebuilds. its children list comes from a per-build pool. extract the function that builds it; never cache the blob in a field or static readonly. Text and Image are pool-free and safe to cache.
key every child of a list that reorders, filters, or grows, and never mix keyed with unkeyed siblings. a mixed list makes the reconciler abandon keys for the whole list (a warning plus real per-frame cost). keys must be unique and derived from a stable id, never a display value that can repeat. Children.AddRange keys loops for you.
Position = Absolute resolves against the nearest positioned ancestor. give the intended parent Position = PositionMode.Relative. the inverse trap: sibling shapes meant to overlap must each be Position = Absolute, Top = 0, Left = 0 or flex lays them side by side (shapes).
animate through the cheap channels. per-frame changes to transforms (Transform = PanelTransform.Rotate(deg).Scale(s)), colors, opacity, and positions are style writes and effectively free. per-frame changes to these are NOT free and tank the frame rate:
StartAngle, radii, polygon points) re-bakes a mask texture each frame - sweep a fixed wedge with a Transform instead;ShaderEffect uniform accepts a literal or a per-frame Func<T> (["LightPos"] = (Func<Vector2>)(() => Mouse.Position / Screen.Size) - the explicit Func cast is required, a bare lambda does not compile), bypassing the reconciler entirely.animation state is a damper field, advanced in Tick, sampled in Build. SpringFloat _pulse = new(1f, 4f, 0.5f); then _pulse.Tick(dt) in Tick, set .Target (or kick .Velocity) on input, read .Current in Build. easing names are exactly: Linear, Ease, EaseIn, EaseOut, EaseInOut, ExpoIn, ExpoOut, ExpoInOut, BounceIn, BounceOut, BounceInOut, SineIn, SineOut, SineInOut, StepStart, StepEnd.
do not declare PointerEvents defensively. goo auto-gates: handlers (or TextEntry/WebPanel) resolve to All, state variants force All, everything else resolves to None. the one case auto-gating does not cover: a scroll viewport, which needs an explicit PointerEvents.All or it looks inert. a scroll container needs all three of: a bounded size on the scroll axis, OverflowY = OverflowMode.Scroll (not Auto, not Hidden), and PointerEvents = PointerEvents.All.
SwallowClick = true stops a click from bubbling. use it on content drawn over a dismiss-on-click scrim. an empty OnClick = e => { } does NOT stop propagation.
if a property seems missing, do not invent it. the generated files Container.Style.g.cs, Text.Style.g.cs, etc under the goo library's Code/Core/ are the property tables (grep *.Style.g.cs from the project root). if it is not there, it does not exist - say so instead of guessing.
two C# name traps. PanelTransform is ambiguous between Goo and Sandbox.UI when both are imported: add using PanelTransform = Goo.PanelTransform;. inside any component class, a bare Components resolves to the engine's ComponentList property, so alias static component classes (using Kit = Goo.Components.Components;). do not alias anything to UI - it collides with the Sandbox.UI namespace from inside namespace Sandbox.
every Goo source file opens with this header — the PanelTransform alias (rule 18) is non-optional whenever you touch Transform, and dampers need Goo.Animation:
using Goo;
using Sandbox;
using Sandbox.UI;
using PanelTransform = Goo.PanelTransform; // rule 18: PanelTransform is otherwise ambiguous with Sandbox.UI
using Goo.Animation; // only when using a damper (SpringFloat/SmoothFloat/DecayFloat/...)
public class CounterUI : GooPanel<Container>
{
int _count;
protected override Container Build() => new Container
{
Padding = 16, Gap = 12, FlexDirection = FlexDirection.Row, AlignItems = Align.Center,
BackgroundColor = Color.White,
Children =
{
new Text( _count.ToString() ),
new Container
{
Padding = 8, BorderRadius = 6,
BackgroundColor = Color.Gray, HoverBackgroundColor = Color.White, TransitionMs = 150,
OnClick = e => _count++, // handler triggers the rebuild automatically
Children = { new Text( "+" ) },
},
},
};
}
using Goo;
using Goo.Animation; // SpringFloat lives here, NOT in Goo
using Sandbox;
using PanelTransform = Goo.PanelTransform; // required: Sandbox.UI also defines PanelTransform
public class PulseUI : GooPanel<Container>
{
float _t;
SpringFloat _scale = new( 1f, 4f, 0.5f );
protected override bool Tick( float dt ) { _t += dt; _scale.Tick( dt ); return true; }
protected override Container Build() => new Container
{
Width = 64, Height = 64, BorderRadius = 8,
Transform = PanelTransform.Rotate( _t * 45f ).Scale( _scale.Current ),
BackgroundColor = Color.Red,
OnClick = e => _scale.Velocity += 5f, // springy kick on click
};
}
var grid = new Container { FlexWrap = Wrap.Wrap, Gap = 4, Width = 232 };
grid.Children.AddRange( _items, ( i, item ) => new Container
{
Width = 52, Height = 52,
BackgroundColor = item.Color, HoverBackgroundColor = Color.White,
OnClick = e => Select( item ),
} );
protected override Container Build()
{
var root = Hud.Overlay(); // full-screen, pointer-through, Column
root.Children.Add( Hud.Fill() with
{
Effect = new ShaderEffect( "shaders/ui_particles.shader" ) { ["Speed"] = 0.4f },
} );
root.Children.Add( Hud.Anchored( Layout.Anchor.TopRight, Minimap(), padding: Px.Of( 16 ) ) );
root.Children.Add( Hud.Anchored( Layout.Anchor.BottomLeft, HealthCard(), padding: Px.Of( 32 ) ) );
return root;
}
note: Hud factory style fields are first-declared and cannot be overridden via with; goo style lists resolve first-declared-wins, so declare per-edge before shorthand and do not redeclare what a factory set.
var viewport = new Container
{
Height = 240, // bounded so children overflow
FlexDirection = FlexDirection.Column,
OverflowY = OverflowMode.Scroll, // Scroll, not Auto or Hidden
PointerEvents = PointerEvents.All, // required or the wheel never arrives
};
no visible scrollbar is rendered; scrolling is wheel and drag. programmatic scroll offset is not exposed on blobs - surface that as a gap rather than faking it.
PositionIn, pointer-events policy, click swallowing*-reference.md pages are the authoritative property tablesif anything in this primer contradicts the code, the code wins — trust the live library and open an issue on the goo repository.
references/api-signatures.md: generated signature index of the entire public API - the authority on call shapesreferences/style-properties.md: generated per-blob style-property index - the authority on which { ... } properties each blob acceptsreferences/input.md, references/tokens.md, references/drag-and-drop.md, references/composition.md, references/cells.md, references/animations.md, references/events.md, references/overlay-layout.md: the full system articles (see the routing table above)references/shapes-guidance.md: Sector/Arc/Polygon usage, stacking, the geometry-animation trapreferences/gotchas.md: sizing, null-ternary colors, engine quirksreferences/engine-facts.md: hard-won engine lessons, each paid for with a real debugging sessionFull docs and API reference: https://obselate.github.io/Goo/docs/
Provides CDSS development patterns for drug interaction checking, dose validation, clinical scoring (NEWS2, qSOFA), and alert classification integrated into EMR workflows.
npx claudepluginhub obselate/goo --plugin goo-ui