feat: refactor maplibre for reactive signals and Via-native API

- Export Signal type (signal → Signal) so subpackages can reference it
- Expose viewport signals as public fields (CenterLng, CenterLat, Zoom,
  Bearing, Pitch) for .Text() display and .Bind() usage
- Add signal-backed marker positions (LngSignal/LatSignal) with
  data-effect reactivity for server push and dragend writeback
- Add event system (MapEvent, OnClick, OnLayerClick, OnMouseMove,
  OnContextMenu) using hidden inputs + action triggers
- Add Source interface replacing type-switch, with RawSource escape hatch
- Add CameraOptions for FlyTo/EaseTo/JumpTo/FitBounds/Stop
- Add Control interface with NavigationControl, ScaleControl,
  GeolocateControl, FullscreenControl
- Expand Options with interaction toggles, MaxBounds, and Extra map
- Add effectspike example to validate data-effect with server-pushed signals
- Update maplibre example to showcase all new features
This commit is contained in:
Ryan Hamamura
2026-02-20 05:11:22 -10:00
parent 47dcab8fea
commit 7eb86999b2
12 changed files with 724 additions and 146 deletions

View File

@@ -175,11 +175,11 @@ func (c *Context) OnInterval(duration time.Duration, handler func()) func() {
// the Context before each action call.
// If any signal value is updated by the server, the update is automatically sent to the
// browser when using Sync() or SyncSignsls().
func (c *Context) Signal(v any) *signal {
func (c *Context) Signal(v any) *Signal {
sigID := genRandID()
if v == nil {
c.app.logErr(c, "failed to bind signal: nil signal value")
return &signal{
return &Signal{
id: sigID,
val: "error",
err: fmt.Errorf("context '%s' failed to bind signal '%s': nil signal value", c.id, sigID),
@@ -191,7 +191,7 @@ func (c *Context) Signal(v any) *signal {
v = string(j)
}
}
sig := &signal{
sig := &Signal{
id: sigID,
val: v,
changed: true,
@@ -254,13 +254,13 @@ func (c *Context) injectSignals(sigs map[string]any) {
for sigID, val := range sigs {
item, ok := c.signals.Load(sigID)
if !ok {
c.signals.Store(sigID, &signal{
c.signals.Store(sigID, &Signal{
id: sigID,
val: val,
})
continue
}
if sig, ok := item.(*signal); ok {
if sig, ok := item.(*Signal); ok {
sig.val = val
sig.changed = false
}
@@ -284,7 +284,7 @@ func (c *Context) prepareSignalsForPatch() map[string]any {
updatedSigs := make(map[string]any)
c.signals.Range(func(sigID, value any) bool {
switch sig := value.(type) {
case *signal:
case *Signal:
if sig.err != nil {
c.app.logWarn(c, "signal '%s' is out of sync: %v", sig.id, sig.err)
return true
@@ -594,7 +594,7 @@ func (c *Context) unsubscribeAll() {
// can operate on all fields by default.
func (c *Context) Field(initial any, rules ...Rule) *Field {
f := &Field{
signal: c.Signal(initial),
Signal: c.Signal(initial),
rules: rules,
initialVal: initial,
}