Pinboard

An option area plus a freeform surface you drag items onto at any (x, y) — a floor-plan device placer, a map pin board, a seating chart on a photo. Drag a palette item onto the surface to place it, drag a marker to reposition, drag it back to the palette to remove, and tap a placed marker to fire its action.

The second spatial (Pattern-3) drag control: placements ride normalized x/y (CARSpatialPlacementState) and round-trip through the synced value, so a server can seed or push a layout.

Type

"pinboard"

Sizing

In a mode: "flow" grid set controlHeight (e.g. 560); in a 2-D grid give it a real span. surfaceAspect controls the surface's shape. See Grid Dimensions.

Config (pinboardConfig)

Field Type Description
items array The catalog (below). An item with both x and y starts placed; otherwise it's in the option area.
background string SF Symbol drawn faint behind the surface (e.g. "house.fill", "map"). Omitted ⇒ a plain surface.
surfaceAspect number Surface width ÷ height (default 1.4).
searchable bool Filter field over the option area.
paletteLabel string Option-area header (default "Items").
hidePalette bool Hide the option area.
events object pickup / place / layout fired on drag.

Item { id, label?, icon?, tint?, x?, y?, action?, locked? }

Field Type Description
x, y number Initial normalized position 0…1. Both present ⇒ starts placed.
action object Fired when the placed marker is tapped. {{value}} = the id.
locked bool Can't be dragged (still tappable).

Events

Placing/moving a marker fires place { item, x, y } and layout { points: { id: {x,y} }, changed } (see DragEventActions). Tapping a placed marker fires that item's action. Removing a marker (drag back to the palette) fires layout.

Accessibility

Under VoiceOver tray chips expose a "Place at center" action and markers read their position ("28% across, 40% down"), fire on double-tap, and expose "Return to " — the full surface works without drag.

Synced value

{ "points": { "<id>": { "x": 0.28, "y": 0.4 } } }. Sync a value in this shape to let a server seed or rearrange placements; ids not in the catalog are ignored.

Examples

Floor-plan device placement

{
  "type": "pinboard",
  "id": "home-plan",
  "position": [0, 0],
  "span": [10, 4],
  "controlHeight": 560,
  "label": "Home Devices",
  "pinboardConfig": {
    "background": "house.fill",
    "surfaceAspect": 1.6,
    "paletteLabel": "Unplaced devices",
    "items": [
      { "id": "living-lamp", "label": "Living Lamp", "icon": "lamp.table.fill", "tint": "#FF9500", "x": 0.28, "y": 0.4,
        "action": { "method": "meshsocket", "mode": "send", "event": "route_msg",
                    "payload": { "target_id": "hub", "type": "toggle", "payload": { "device": "{{value}}" } } } },
      { "id": "front-door", "label": "Front Door", "icon": "door.left.hand.closed", "tint": "#34C759",
        "action": { "method": "meshsocket", "mode": "send", "event": "route_msg",
                    "payload": { "target_id": "hub", "type": "toggle", "payload": { "device": "{{value}}" } } } }
    ],
    "events": {
      "place": { "method": "meshsocket", "mode": "send", "event": "route_msg",
                 "payload": { "target_id": "hub", "type": "place_device",
                              "payload": { "device": "{{item}}", "x": "{{x}}", "y": "{{y}}" } } },
      "layout": { "method": "meshsocket", "mode": "send", "event": "floorplan_state" }
    }
  }
}

Try it: the bundled pinboard-demo layout has this floor plan (three devices pre-placed, two in the tray).