@xenosystem/blocks 0.11.5 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import { PanelHost, PanelModule, PanelManifest } from '@xenosystem/block-sdk';
1
+ import { PanelHost, PanelModule, PanelManifest, BlockDeclaration } from '@xenosystem/block-sdk';
2
2
  import { ThrottleClock, XenoPlayheadClock, XenoTimebase, XenoTransportState, XenoTick, XenoStepUnit, XenoLoopRange } from '@xenosystem/time-core';
3
3
  export { BAR_SUBDIVISION, BarsBeats, DEFAULT_TIMEBASE, DISPLAY_CYCLE, ExtrapolateInput, IDLE_CLOCK, ThrottleClock, Throttled, XenoLoopRange, XenoPlayheadClock, XenoStepUnit, XenoTempoEntry, XenoTick, XenoTimeDisplay, XenoTimeSignature, XenoTimebase, XenoTransportIntent, XenoTransportState, clampTick, clockAt, createThrottle, displayLabel, effectiveFps, extrapolateTick, formatBars, formatFrames, formatSeconds, formatTick, formatTimecode, frameToTick, isAnimating, nextDisplay, normalizeClock, normalizeTimebase, secondsToTicks, tickToFrame, ticksToBarsBeats, ticksToSeconds, toTick, wrapIntoLoop } from '@xenosystem/time-core';
4
4
  import { ReactElement, ReactNode } from 'react';
@@ -239,8 +239,9 @@ declare const transportManifest: PanelManifest;
239
239
  * transport is not moving. That is the whole 60fps-without-streaming trick — no per-frame IPC, no
240
240
  * idle wakeups.
241
241
  *
242
- * Monochrome, dense, rectangles with small corner radius, square knobs, no circles, no shadows
243
- * (DESIGN_SYSTEM §2/§3/§4).
242
+ * It draws the same model as `TRANSPORT_DECLARATION` (`transportPanelView(transportViewState(c))`);
243
+ * the declaration test holds the two byte-identical. The scrub gesture arrives from the Slider as a
244
+ * FRACTION and is mapped to a tick by the controller, exactly as the declaration's `ui` intent is.
244
245
  *
245
246
  * @module
246
247
  */
@@ -251,6 +252,158 @@ interface TransportPanelViewProps {
251
252
  }
252
253
  declare function TransportPanelView({ controller }: TransportPanelViewProps): ReactElement;
253
254
 
255
+ /**
256
+ * Inline transport glyphs at lucide geometry, **strokeWidth 1.5** (DESIGN_SYSTEM §6). Zero runtime
257
+ * dependencies — no `lucide-react` import. Shapes are rectangles and triangles only; the record
258
+ * glyph is a rounded SQUARE, never a circle (§3 "no circles anywhere").
259
+ *
260
+ * @module
261
+ */
262
+
263
+ interface IconProps {
264
+ size?: number;
265
+ }
266
+ /** lucide `play` (filled for weight at small sizes). */
267
+ declare function PlayIcon({ size }: IconProps): ReactElement;
268
+ /** lucide `pause`. */
269
+ declare function PauseIcon({ size }: IconProps): ReactElement;
270
+ /** lucide `square` — stop. */
271
+ declare function StopIcon({ size }: IconProps): ReactElement;
272
+ /** Record — a rounded SQUARE (the XENO shape language has no circles). */
273
+ declare function RecordIcon({ size }: IconProps): ReactElement;
274
+ /** lucide `skip-back` — go to start. */
275
+ declare function StartIcon({ size }: IconProps): ReactElement;
276
+ /** lucide `skip-forward` — go to end. */
277
+ declare function EndIcon({ size }: IconProps): ReactElement;
278
+ /** Step one unit back. */
279
+ declare function StepBackIcon({ size }: IconProps): ReactElement;
280
+ /** Step one unit forward. */
281
+ declare function StepForwardIcon({ size }: IconProps): ReactElement;
282
+ /** lucide `rewind` — shuttle reverse. */
283
+ declare function ShuttleLeftIcon({ size }: IconProps): ReactElement;
284
+ /** lucide `fast-forward` — shuttle forward. */
285
+ declare function ShuttleRightIcon({ size }: IconProps): ReactElement;
286
+ /** lucide `repeat` — loop. */
287
+ declare function LoopIcon({ size }: IconProps): ReactElement;
288
+ /** A metronome pendulum. */
289
+ declare function MetronomeIcon({ size }: IconProps): ReactElement;
290
+ /** Play or pause, decided by a prop — one node in a declaration instead of two switched by `when`. */
291
+ declare function PlayPauseIcon({ playing, size }: {
292
+ playing?: boolean;
293
+ size?: number;
294
+ }): ReactElement;
295
+ /**
296
+ * The glyphs by registry name, for a declaration (`transport/PlayIcon`). Block-local until the
297
+ * elements icon set lands, then these resolve to `icons/*` and this map goes.
298
+ */
299
+ declare const TRANSPORT_ICONS: {
300
+ 'transport/PlayIcon': typeof PlayIcon;
301
+ 'transport/PauseIcon': typeof PauseIcon;
302
+ 'transport/PlayPauseIcon': typeof PlayPauseIcon;
303
+ 'transport/StopIcon': typeof StopIcon;
304
+ 'transport/RecordIcon': typeof RecordIcon;
305
+ 'transport/StartIcon': typeof StartIcon;
306
+ 'transport/EndIcon': typeof EndIcon;
307
+ 'transport/StepBackIcon': typeof StepBackIcon;
308
+ 'transport/StepForwardIcon': typeof StepForwardIcon;
309
+ 'transport/ShuttleLeftIcon': typeof ShuttleLeftIcon;
310
+ 'transport/ShuttleRightIcon': typeof ShuttleRightIcon;
311
+ 'transport/LoopIcon': typeof LoopIcon;
312
+ 'transport/MetronomeIcon': typeof MetronomeIcon;
313
+ };
314
+
315
+ /**
316
+ * The transport strip AS A DECLARATION — the fourth block written the §8d way (auth gate, agent,
317
+ * account, transport), and the first whose runtime is a real-time controller.
318
+ *
319
+ * Presentation contract: input `view` (a {@link TransportViewState} the runtime derives from the
320
+ * controller on every change and every animation frame), output `ui` (a {@link TransportUiIntent}),
321
+ * applied by the runtime to the controller's own methods. The declaration holds no state and does no
322
+ * time maths: the readout, the progress and the ARIA triple arrive already computed, and a scrub is
323
+ * a FRACTION of the track (the slider owns its geometry; `fractionToTick` stays in the controller).
324
+ *
325
+ * The code view (`TransportPanelView`) renders the same model through `transportPanelView`; the
326
+ * test holds the two byte-identical in every state the controller can be driven into.
327
+ */
328
+
329
+ /** What the runtime pushes on `view` — everything the strip shows, already formatted. */
330
+ interface TransportViewState {
331
+ playing: boolean;
332
+ recording: boolean;
333
+ loopEnabled: boolean;
334
+ metronome: boolean;
335
+ punch: boolean;
336
+ bpm: number;
337
+ show: {
338
+ shuttle: boolean;
339
+ record: boolean;
340
+ loop: boolean;
341
+ metronome: boolean;
342
+ punch: boolean;
343
+ bpm: boolean;
344
+ inOut: boolean;
345
+ scrubber: boolean;
346
+ };
347
+ readout: string;
348
+ displayLabel: string;
349
+ durationReadout: string;
350
+ progress: number;
351
+ duration: number;
352
+ currentTick: number;
353
+ }
354
+ type TransportUiIntent = {
355
+ kind: 'goToStart';
356
+ } | {
357
+ kind: 'stepBack';
358
+ } | {
359
+ kind: 'shuttleBack';
360
+ } | {
361
+ kind: 'togglePlay';
362
+ } | {
363
+ kind: 'stop';
364
+ } | {
365
+ kind: 'shuttleForward';
366
+ } | {
367
+ kind: 'stepForward';
368
+ } | {
369
+ kind: 'goToEnd';
370
+ } | {
371
+ kind: 'record';
372
+ } | {
373
+ kind: 'toggleLoop';
374
+ } | {
375
+ kind: 'toggleMetronome';
376
+ } | {
377
+ kind: 'cycleDisplay';
378
+ } | {
379
+ kind: 'setTempo';
380
+ bpm: number;
381
+ } | {
382
+ kind: 'setIn';
383
+ } | {
384
+ kind: 'setOut';
385
+ } | {
386
+ kind: 'scrub';
387
+ fraction: number;
388
+ phase: 'begin' | 'move' | 'end' | 'cancel';
389
+ };
390
+ /** Derive the view state from the live controller — the runtime calls this on every draw. */
391
+ declare function transportViewState(c: TransportPanelController): TransportViewState;
392
+ /** Apply a `ui` intent to the controller — the runtime's half of the contract. */
393
+ declare function applyTransportUi(c: TransportPanelController, intent: TransportUiIntent): void;
394
+ declare const transportViewManifest: PanelManifest;
395
+ /** The view model both renderers draw from: the state plus every derived label. */
396
+ interface TransportPanelViewModel extends TransportViewState {
397
+ stopped: boolean;
398
+ playLabel: string;
399
+ positionLabel: string;
400
+ }
401
+ declare function transportPanelView(state: TransportViewState): TransportPanelViewModel;
402
+ declare const BEHAVIOURS: {
403
+ 'time/transportPanelView': (inputs: Record<string, unknown>) => TransportPanelViewModel;
404
+ };
405
+ declare const TRANSPORT_DECLARATION: BlockDeclaration;
406
+
254
407
  /**
255
408
  * `XenoTimeScale` — **the only thing in this package that converts between pixels and ticks.**
256
409
  *
@@ -2258,4 +2411,4 @@ interface CalendarPanelViewProps {
2258
2411
  /** The Calendar panel view. */
2259
2412
  declare function CalendarPanelView({ controller, firstDayOfWeek, emptyHint, }: CalendarPanelViewProps): ReactNode;
2260
2413
 
2261
- export { ALL_CALENDAR_MODES, type BeginDragOptions, type BuildCellsOptions, CALENDAR_PANEL_ID, CalendarController, type CalendarControllerOptions, type CalendarHostBridge, type CalendarPanelState, CalendarPanelView, type CalendarPanelViewProps, type CalendarRenderContext, type CalendarViewState, type CreateCalendarPanelOptions, type CreateTimelinePanelOptions, type CreateTransportPanelOptions, DEFAULT_SNAP, DEFAULT_ZOOM, DRAG_THRESHOLD_PX, type DeriveOptions, EDGE_ZONE_MIN_CLIP_PX, EDGE_ZONE_PX, MIN_MAJOR_PX, MIN_MINOR_PX, TIMELINE_PANEL_ID, TRIM_SNAP_PX, TimelineController, type TimelineControllerOptions, type TimelineHostBridge, type TimelinePanelState, TimelinePanelView, type TimelinePanelViewProps, type TimelineRenderConfig, type TimelineRenderContext, type TimelineViewState, type TransportKeyEvent, TransportPanelController, type TransportPanelControllerOptions, type TransportPanelSerialized, TransportPanelView, type TransportPanelViewProps, type XenoArrangement, type XenoCachedTile, type XenoCalendarCell, type XenoCalendarEdit, type XenoCalendarEvent, type XenoCalendarSlotActivate, type XenoCalendarViewMode, type XenoCalendarWindow, type XenoClip, type XenoClipFade, type XenoDragKind, type XenoDragModifiers, type XenoDragPreview, type XenoDragState, type XenoHitTarget, type XenoLaneTile, type XenoLaneTileRequest, type XenoMarker, type XenoOverlaySpan, type XenoRenderTier, type XenoRowLayout, type XenoRulerTick, type XenoSnapCandidate, type XenoSnapConfig, type XenoSnapMode, type XenoSnapResult, type XenoTickWeight, XenoTileCache, type XenoTileCacheOptions, XenoTimeScale, type XenoTimeScaleGeometry, type XenoTimeScaleState, type XenoTimelineOp, type XenoTimelineSelection, type XenoTimelineTool, type XenoTimelineViewport, type XenoTrack, type XenoTrackKind, type XenoTrimMode, type XenoVisibleTrack, type XenoZoomBounds, addDays, addMonths, alignToTileGrid, beginDrag, blitTile, buildCells, buildRowLayout, calendarManifest, calendarPanel, chooseRung, clampScroll, clipDuration, clipsInMarquee, clipsOverlap, collectSnapTargets, commitDrag, computeVisible, createCalendarPanel, createTimelinePanel, createTransportPanel, dayKey, daysInMonth, dedupeByColumn, deriveEvents, edgeZone, gridStart, gridTicksFor, hasSourceWindow, isZoomGesture, isoWeekday, packOverlaps, parseDayKey, passedThreshold, previewDrag, renderTier, rowAtY, rowInWindow, rulerTicks, scrollToReveal, snapDelta, snapTick, snapWouldFight, sourceRangeFor, statusTone, ticksPerStep, tileKey, tileWidthTicks, timelineManifest, timelinePanel, todayKey, transportManifest, transportPanel, trimModeFor, visibleClips, visibleRows, weekStart, wheelZoomFactor, withSnapModifier, zoomAboutCursor, zoomBucket, zoomToFit };
2414
+ export { ALL_CALENDAR_MODES, type BeginDragOptions, type BuildCellsOptions, CALENDAR_PANEL_ID, CalendarController, type CalendarControllerOptions, type CalendarHostBridge, type CalendarPanelState, CalendarPanelView, type CalendarPanelViewProps, type CalendarRenderContext, type CalendarViewState, type CreateCalendarPanelOptions, type CreateTimelinePanelOptions, type CreateTransportPanelOptions, DEFAULT_SNAP, DEFAULT_ZOOM, DRAG_THRESHOLD_PX, type DeriveOptions, EDGE_ZONE_MIN_CLIP_PX, EDGE_ZONE_PX, MIN_MAJOR_PX, MIN_MINOR_PX, TIMELINE_PANEL_ID, BEHAVIOURS as TRANSPORT_BEHAVIOURS, TRANSPORT_DECLARATION, TRANSPORT_ICONS, TRIM_SNAP_PX, TimelineController, type TimelineControllerOptions, type TimelineHostBridge, type TimelinePanelState, TimelinePanelView, type TimelinePanelViewProps, type TimelineRenderConfig, type TimelineRenderContext, type TimelineViewState, type TransportKeyEvent, TransportPanelController, type TransportPanelControllerOptions, type TransportPanelSerialized, TransportPanelView, type TransportPanelViewModel, type TransportPanelViewProps, type TransportUiIntent, type TransportViewState, type XenoArrangement, type XenoCachedTile, type XenoCalendarCell, type XenoCalendarEdit, type XenoCalendarEvent, type XenoCalendarSlotActivate, type XenoCalendarViewMode, type XenoCalendarWindow, type XenoClip, type XenoClipFade, type XenoDragKind, type XenoDragModifiers, type XenoDragPreview, type XenoDragState, type XenoHitTarget, type XenoLaneTile, type XenoLaneTileRequest, type XenoMarker, type XenoOverlaySpan, type XenoRenderTier, type XenoRowLayout, type XenoRulerTick, type XenoSnapCandidate, type XenoSnapConfig, type XenoSnapMode, type XenoSnapResult, type XenoTickWeight, XenoTileCache, type XenoTileCacheOptions, XenoTimeScale, type XenoTimeScaleGeometry, type XenoTimeScaleState, type XenoTimelineOp, type XenoTimelineSelection, type XenoTimelineTool, type XenoTimelineViewport, type XenoTrack, type XenoTrackKind, type XenoTrimMode, type XenoVisibleTrack, type XenoZoomBounds, addDays, addMonths, alignToTileGrid, applyTransportUi, beginDrag, blitTile, buildCells, buildRowLayout, calendarManifest, calendarPanel, chooseRung, clampScroll, clipDuration, clipsInMarquee, clipsOverlap, collectSnapTargets, commitDrag, computeVisible, createCalendarPanel, createTimelinePanel, createTransportPanel, dayKey, daysInMonth, dedupeByColumn, deriveEvents, edgeZone, gridStart, gridTicksFor, hasSourceWindow, isZoomGesture, isoWeekday, packOverlaps, parseDayKey, passedThreshold, previewDrag, renderTier, rowAtY, rowInWindow, rulerTicks, scrollToReveal, snapDelta, snapTick, snapWouldFight, sourceRangeFor, statusTone, ticksPerStep, tileKey, tileWidthTicks, timelineManifest, timelinePanel, todayKey, transportManifest, transportPanel, transportPanelView, transportViewManifest, transportViewState, trimModeFor, visibleClips, visibleRows, weekStart, wheelZoomFactor, withSnapModifier, zoomAboutCursor, zoomBucket, zoomToFit };
@@ -782,6 +782,209 @@ function ticksPerStep(unit, timebase) {
782
782
  import { useCallback, useEffect, useRef, useSyncExternalStore } from "react";
783
783
  import { Column, IconButton, NumberInput, Readout, Slider, Stack, TextButton } from "@xenosystem/components/chrome";
784
784
 
785
+ // src/time/transport/declaration.ts
786
+ function transportViewState(c) {
787
+ const state = c.getState();
788
+ return {
789
+ playing: c.isPlaying(),
790
+ recording: !!state.recording,
791
+ loopEnabled: !!state.loopEnabled,
792
+ metronome: !!state.metronome,
793
+ punch: !!state.punch,
794
+ bpm: state.bpm ?? 120,
795
+ show: {
796
+ shuttle: c.showsControl("shuttle"),
797
+ record: c.showsControl("record"),
798
+ loop: c.showsControl("loop"),
799
+ metronome: c.showsControl("metronome"),
800
+ punch: c.showsControl("punch"),
801
+ bpm: c.showsControl("bpm"),
802
+ inOut: c.showsControl("inOut"),
803
+ scrubber: c.showsControl("scrubber")
804
+ },
805
+ readout: c.getReadout(),
806
+ displayLabel: (0, format_exports.displayLabel)(c.getDisplay()),
807
+ durationReadout: c.getDurationReadout(),
808
+ progress: c.getProgress(),
809
+ duration: c.getDuration(),
810
+ currentTick: c.getCurrentTick()
811
+ };
812
+ }
813
+ function applyTransportUi(c, intent) {
814
+ switch (intent.kind) {
815
+ case "goToStart":
816
+ c.goToStart();
817
+ break;
818
+ case "stepBack":
819
+ c.step(-1);
820
+ break;
821
+ case "shuttleBack":
822
+ c.shuttleStep(-1);
823
+ break;
824
+ case "togglePlay":
825
+ c.togglePlay();
826
+ break;
827
+ case "stop":
828
+ c.stop();
829
+ break;
830
+ case "shuttleForward":
831
+ c.shuttleStep(1);
832
+ break;
833
+ case "stepForward":
834
+ c.step(1);
835
+ break;
836
+ case "goToEnd":
837
+ c.goToEnd();
838
+ break;
839
+ case "record":
840
+ c.record();
841
+ break;
842
+ case "toggleLoop":
843
+ c.toggleLoop();
844
+ break;
845
+ case "toggleMetronome":
846
+ c.toggleMetronome();
847
+ break;
848
+ case "cycleDisplay":
849
+ c.cycleDisplay();
850
+ break;
851
+ case "setTempo":
852
+ c.setTempo(intent.bpm);
853
+ break;
854
+ case "setIn":
855
+ c.setInPoint();
856
+ break;
857
+ case "setOut":
858
+ c.setOutPoint();
859
+ break;
860
+ case "scrub": {
861
+ const tick = c.fractionToTick(intent.fraction);
862
+ if (intent.phase === "begin") c.beginScrub(tick);
863
+ else if (intent.phase === "move") c.updateScrub(tick);
864
+ else if (intent.phase === "end") c.endScrub();
865
+ else c.cancelScrub();
866
+ break;
867
+ }
868
+ }
869
+ }
870
+ var transportViewManifest = {
871
+ id: transportManifest.id,
872
+ version: "1.0.0",
873
+ title: "Transport",
874
+ category: "time",
875
+ layout: { slot: "bottom" },
876
+ inputs: [{ id: "view", type: "object", label: "View state", description: "The TransportViewState the runtime derives from the controller on every draw." }],
877
+ outputs: [{ id: "ui", type: "object", label: "UI intent", description: "{ kind, bpm?, fraction?, phase? } \u2014 applied by the runtime to the controller." }],
878
+ commands: []
879
+ };
880
+ function transportPanelView(state) {
881
+ return {
882
+ ...state,
883
+ stopped: !state.playing,
884
+ playLabel: state.playing ? "Pause (Space)" : "Play (Space)",
885
+ positionLabel: `Position ${state.readout}`
886
+ };
887
+ }
888
+ var EMPTY = {
889
+ playing: false,
890
+ recording: false,
891
+ loopEnabled: false,
892
+ metronome: false,
893
+ punch: false,
894
+ bpm: 120,
895
+ show: { shuttle: false, record: false, loop: false, metronome: false, punch: false, bpm: false, inOut: false, scrubber: true },
896
+ readout: "",
897
+ displayLabel: "",
898
+ durationReadout: "",
899
+ progress: 0,
900
+ duration: 0,
901
+ currentTick: 0
902
+ };
903
+ var BEHAVIOURS = {
904
+ "time/transportPanelView": (inputs) => transportPanelView(inputs.view ?? EMPTY)
905
+ };
906
+ var ui = (kind) => ({ emit: "ui", value: { object: { kind: { value: kind } } } });
907
+ var text = (b) => ({ component: "text", props: { text: b } });
908
+ var button = (icon, label, kind, extra = {}) => ({
909
+ component: "chrome/IconButton",
910
+ ...extra.when ? { when: extra.when } : {},
911
+ props: { size: { value: "sm" }, icon: { node: { component: `transport/${icon}` } }, label, ...extra.props ?? {} },
912
+ on: { onClick: ui(kind) }
913
+ });
914
+ var TRANSPORT_DECLARATION = {
915
+ format: "xeno.block@1",
916
+ manifest: transportViewManifest,
917
+ behaviour: "time/transportPanelView",
918
+ composition: {
919
+ component: "chrome/Column",
920
+ props: { className: { value: "xeno-transport-panel" }, tabIndex: { value: 0 }, role: { value: "group" }, "aria-label": { value: "Transport" } },
921
+ children: [
922
+ {
923
+ component: "chrome/Stack",
924
+ props: { direction: { value: "row" }, gap: { value: 3 }, pad: { value: "gutter" } },
925
+ children: [
926
+ button("StartIcon", { value: "Go to start" }, "goToStart"),
927
+ button("StepBackIcon", { value: "Step back" }, "stepBack"),
928
+ button("ShuttleLeftIcon", { value: "Shuttle reverse (J)" }, "shuttleBack", { when: { input: "view.show.shuttle" } }),
929
+ {
930
+ component: "chrome/IconButton",
931
+ props: {
932
+ size: { value: "sm" },
933
+ variant: { value: "primary" },
934
+ icon: { node: { component: "transport/PlayPauseIcon", props: { playing: { input: "view.playing" } } } },
935
+ label: { input: "view.playLabel" },
936
+ active: { input: "view.playing" }
937
+ },
938
+ on: { onClick: ui("togglePlay") }
939
+ },
940
+ button("StopIcon", { value: "Stop" }, "stop"),
941
+ button("ShuttleRightIcon", { value: "Shuttle forward (L)" }, "shuttleForward", { when: { input: "view.show.shuttle" } }),
942
+ button("StepForwardIcon", { value: "Step forward" }, "stepForward"),
943
+ button("EndIcon", { value: "Go to end" }, "goToEnd"),
944
+ button("RecordIcon", { value: "Record" }, "record", { when: { input: "view.show.record" }, props: { variant: { value: "danger" }, active: { input: "view.recording" } } }),
945
+ button("LoopIcon", { value: "Loop" }, "toggleLoop", { when: { input: "view.show.loop" }, props: { active: { input: "view.loopEnabled" } } }),
946
+ button("MetronomeIcon", { value: "Metronome" }, "toggleMetronome", { when: { input: "view.show.metronome" }, props: { active: { input: "view.metronome" } } }),
947
+ { component: "chrome/TextButton", when: { input: "view.show.punch" }, props: { strong: { input: "view.punch" } }, children: [text({ value: "PUNCH" })] },
948
+ {
949
+ component: "chrome/Readout",
950
+ props: { value: { input: "view.readout" }, unit: { input: "view.displayLabel" }, label: { input: "view.positionLabel" }, title: { value: "Click to change time display" } },
951
+ on: { onClick: ui("cycleDisplay") }
952
+ },
953
+ { component: "chrome/Readout", props: { size: { value: "sm" }, value: { input: "view.durationReadout" } } },
954
+ {
955
+ component: "chrome/NumberInput",
956
+ when: { input: "view.show.bpm" },
957
+ props: { caption: { value: "BPM" }, label: { value: "Tempo" }, value: { input: "view.bpm" } },
958
+ on: { onChange: { emit: "ui", value: { object: { kind: { value: "setTempo" }, bpm: { arg: "" } } } } }
959
+ },
960
+ {
961
+ component: "chrome/Stack",
962
+ when: { input: "view.show.inOut" },
963
+ props: { direction: { value: "row" }, gap: { value: 2 }, pad: { value: "none" } },
964
+ children: [
965
+ { component: "chrome/TextButton", on: { onClick: ui("setIn") }, children: [text({ value: "IN" })] },
966
+ { component: "chrome/TextButton", on: { onClick: ui("setOut") }, children: [text({ value: "OUT" })] }
967
+ ]
968
+ }
969
+ ]
970
+ },
971
+ {
972
+ component: "chrome/Slider",
973
+ when: { input: "view.show.scrubber" },
974
+ props: {
975
+ progress: { input: "view.progress" },
976
+ label: { value: "Playhead" },
977
+ valueMin: { value: 0 },
978
+ valueMax: { input: "view.duration" },
979
+ valueNow: { input: "view.currentTick" },
980
+ valueText: { input: "view.readout" }
981
+ },
982
+ on: { onScrub: { emit: "ui", value: { object: { kind: { value: "scrub" }, fraction: { arg: "" }, phase: { arg: "", index: 1 } } } } }
983
+ }
984
+ ]
985
+ }
986
+ };
987
+
785
988
  // src/time/transport/react/icons.tsx
786
989
  import { jsx, jsxs } from "react/jsx-runtime";
787
990
  var base = (size) => ({
@@ -854,6 +1057,24 @@ function MetronomeIcon({ size = 11 }) {
854
1057
  /* @__PURE__ */ jsx("path", { d: "m12 18 4-11" })
855
1058
  ] });
856
1059
  }
1060
+ function PlayPauseIcon({ playing, size }) {
1061
+ return playing ? /* @__PURE__ */ jsx(PauseIcon, { size }) : /* @__PURE__ */ jsx(PlayIcon, { size });
1062
+ }
1063
+ var TRANSPORT_ICONS = {
1064
+ "transport/PlayIcon": PlayIcon,
1065
+ "transport/PauseIcon": PauseIcon,
1066
+ "transport/PlayPauseIcon": PlayPauseIcon,
1067
+ "transport/StopIcon": StopIcon,
1068
+ "transport/RecordIcon": RecordIcon,
1069
+ "transport/StartIcon": StartIcon,
1070
+ "transport/EndIcon": EndIcon,
1071
+ "transport/StepBackIcon": StepBackIcon,
1072
+ "transport/StepForwardIcon": StepForwardIcon,
1073
+ "transport/ShuttleLeftIcon": ShuttleLeftIcon,
1074
+ "transport/ShuttleRightIcon": ShuttleRightIcon,
1075
+ "transport/LoopIcon": LoopIcon,
1076
+ "transport/MetronomeIcon": MetronomeIcon
1077
+ };
857
1078
 
858
1079
  // src/time/transport/react/TransportPanelView.tsx
859
1080
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
@@ -879,40 +1100,8 @@ function TransportPanelView({ controller }) {
879
1100
  rafRef.current = null;
880
1101
  };
881
1102
  }, [controller, animating]);
882
- const playing = controller.isPlaying();
883
- const state = controller.getState();
884
- const tickFromPointer = useCallback(
885
- (clientX) => {
886
- const el = scrubRef.current;
887
- if (!el) return 0;
888
- const rect = el.getBoundingClientRect();
889
- return controller.fractionToTick(rect.width > 0 ? (clientX - rect.left) / rect.width : 0);
890
- },
891
- [controller]
892
- );
893
- const onScrubDown = useCallback(
894
- (e) => {
895
- e.currentTarget.setPointerCapture(e.pointerId);
896
- controller.beginScrub(tickFromPointer(e.clientX));
897
- },
898
- [controller, tickFromPointer]
899
- );
900
- const onScrubMove = useCallback(
901
- (e) => {
902
- if (controller.isScrubbing()) controller.updateScrub(tickFromPointer(e.clientX));
903
- },
904
- [controller, tickFromPointer]
905
- );
906
- const onScrubUp = useCallback(
907
- (e) => {
908
- try {
909
- e.currentTarget.releasePointerCapture(e.pointerId);
910
- } catch {
911
- }
912
- controller.endScrub();
913
- },
914
- [controller]
915
- );
1103
+ const view = transportPanelView(transportViewState(controller));
1104
+ const scrub = useCallback((fraction, phase) => applyTransportUi(controller, { kind: "scrub", fraction, phase }), [controller]);
916
1105
  const onKeyDown = useCallback(
917
1106
  (e) => {
918
1107
  if (controller.handleKey({ key: e.key, ctrl: e.ctrlKey, meta: e.metaKey, shift: e.shiftKey }, "down")) {
@@ -941,59 +1130,25 @@ function TransportPanelView({ controller }) {
941
1130
  /* @__PURE__ */ jsxs2(Stack, { direction: "row", gap: 3, pad: "gutter", children: [
942
1131
  /* @__PURE__ */ jsx2(IconButton, { size: "sm", icon: /* @__PURE__ */ jsx2(StartIcon, {}), label: "Go to start", onClick: () => controller.goToStart() }),
943
1132
  /* @__PURE__ */ jsx2(IconButton, { size: "sm", icon: /* @__PURE__ */ jsx2(StepBackIcon, {}), label: "Step back", onClick: () => controller.step(-1) }),
944
- controller.showsControl("shuttle") && /* @__PURE__ */ jsx2(IconButton, { size: "sm", icon: /* @__PURE__ */ jsx2(ShuttleLeftIcon, {}), label: "Shuttle reverse (J)", onClick: () => controller.shuttleStep(-1) }),
945
- /* @__PURE__ */ jsx2(
946
- IconButton,
947
- {
948
- size: "sm",
949
- variant: "primary",
950
- icon: playing ? /* @__PURE__ */ jsx2(PauseIcon, {}) : /* @__PURE__ */ jsx2(PlayIcon, {}),
951
- label: playing ? "Pause (Space)" : "Play (Space)",
952
- active: playing,
953
- onClick: () => controller.togglePlay()
954
- }
955
- ),
1133
+ view.show.shuttle && /* @__PURE__ */ jsx2(IconButton, { size: "sm", icon: /* @__PURE__ */ jsx2(ShuttleLeftIcon, {}), label: "Shuttle reverse (J)", onClick: () => controller.shuttleStep(-1) }),
1134
+ /* @__PURE__ */ jsx2(IconButton, { size: "sm", variant: "primary", icon: /* @__PURE__ */ jsx2(PlayPauseIcon, { playing: view.playing }), label: view.playLabel, active: view.playing, onClick: () => controller.togglePlay() }),
956
1135
  /* @__PURE__ */ jsx2(IconButton, { size: "sm", icon: /* @__PURE__ */ jsx2(StopIcon, {}), label: "Stop", onClick: () => controller.stop() }),
957
- controller.showsControl("shuttle") && /* @__PURE__ */ jsx2(IconButton, { size: "sm", icon: /* @__PURE__ */ jsx2(ShuttleRightIcon, {}), label: "Shuttle forward (L)", onClick: () => controller.shuttleStep(1) }),
1136
+ view.show.shuttle && /* @__PURE__ */ jsx2(IconButton, { size: "sm", icon: /* @__PURE__ */ jsx2(ShuttleRightIcon, {}), label: "Shuttle forward (L)", onClick: () => controller.shuttleStep(1) }),
958
1137
  /* @__PURE__ */ jsx2(IconButton, { size: "sm", icon: /* @__PURE__ */ jsx2(StepForwardIcon, {}), label: "Step forward", onClick: () => controller.step(1) }),
959
1138
  /* @__PURE__ */ jsx2(IconButton, { size: "sm", icon: /* @__PURE__ */ jsx2(EndIcon, {}), label: "Go to end", onClick: () => controller.goToEnd() }),
960
- controller.showsControl("record") && /* @__PURE__ */ jsx2(IconButton, { size: "sm", variant: "danger", icon: /* @__PURE__ */ jsx2(RecordIcon, {}), label: "Record", active: !!state.recording, onClick: () => controller.record() }),
961
- controller.showsControl("loop") && /* @__PURE__ */ jsx2(IconButton, { size: "sm", icon: /* @__PURE__ */ jsx2(LoopIcon, {}), label: "Loop", active: !!state.loopEnabled, onClick: () => controller.toggleLoop() }),
962
- controller.showsControl("metronome") && /* @__PURE__ */ jsx2(IconButton, { size: "sm", icon: /* @__PURE__ */ jsx2(MetronomeIcon, {}), label: "Metronome", active: !!state.metronome, onClick: () => controller.toggleMetronome() }),
963
- controller.showsControl("punch") && /* @__PURE__ */ jsx2(TextButton, { strong: !!state.punch, children: "PUNCH" }),
964
- /* @__PURE__ */ jsx2(
965
- Readout,
966
- {
967
- value: controller.getReadout(),
968
- unit: (0, format_exports.displayLabel)(controller.getDisplay()),
969
- label: `Position ${controller.getReadout()}`,
970
- title: "Click to change time display",
971
- onClick: () => controller.cycleDisplay()
972
- }
973
- ),
974
- /* @__PURE__ */ jsx2(Readout, { size: "sm", value: controller.getDurationReadout() }),
975
- controller.showsControl("bpm") && /* @__PURE__ */ jsx2(NumberInput, { caption: "BPM", label: "Tempo", value: state.bpm ?? 120, onChange: (bpm) => controller.setTempo(bpm) }),
976
- controller.showsControl("inOut") && /* @__PURE__ */ jsxs2(Stack, { direction: "row", gap: 2, pad: "none", children: [
1139
+ view.show.record && /* @__PURE__ */ jsx2(IconButton, { size: "sm", variant: "danger", icon: /* @__PURE__ */ jsx2(RecordIcon, {}), label: "Record", active: view.recording, onClick: () => controller.record() }),
1140
+ view.show.loop && /* @__PURE__ */ jsx2(IconButton, { size: "sm", icon: /* @__PURE__ */ jsx2(LoopIcon, {}), label: "Loop", active: view.loopEnabled, onClick: () => controller.toggleLoop() }),
1141
+ view.show.metronome && /* @__PURE__ */ jsx2(IconButton, { size: "sm", icon: /* @__PURE__ */ jsx2(MetronomeIcon, {}), label: "Metronome", active: view.metronome, onClick: () => controller.toggleMetronome() }),
1142
+ view.show.punch && /* @__PURE__ */ jsx2(TextButton, { strong: view.punch, children: "PUNCH" }),
1143
+ /* @__PURE__ */ jsx2(Readout, { value: view.readout, unit: view.displayLabel, label: view.positionLabel, title: "Click to change time display", onClick: () => controller.cycleDisplay() }),
1144
+ /* @__PURE__ */ jsx2(Readout, { size: "sm", value: view.durationReadout }),
1145
+ view.show.bpm && /* @__PURE__ */ jsx2(NumberInput, { caption: "BPM", label: "Tempo", value: view.bpm, onChange: (bpm) => controller.setTempo(bpm) }),
1146
+ view.show.inOut && /* @__PURE__ */ jsxs2(Stack, { direction: "row", gap: 2, pad: "none", children: [
977
1147
  /* @__PURE__ */ jsx2(TextButton, { onClick: () => controller.setInPoint(), children: "IN" }),
978
1148
  /* @__PURE__ */ jsx2(TextButton, { onClick: () => controller.setOutPoint(), children: "OUT" })
979
1149
  ] })
980
1150
  ] }),
981
- controller.showsControl("scrubber") && /* @__PURE__ */ jsx2(
982
- Slider,
983
- {
984
- trackRef: scrubRef,
985
- progress: controller.getProgress(),
986
- label: "Playhead",
987
- valueMin: 0,
988
- valueMax: controller.getDuration(),
989
- valueNow: controller.getCurrentTick(),
990
- valueText: controller.getReadout(),
991
- onPointerDown: onScrubDown,
992
- onPointerMove: onScrubMove,
993
- onPointerUp: onScrubUp,
994
- onPointerCancel: () => controller.cancelScrub()
995
- }
996
- )
1151
+ view.show.scrubber && /* @__PURE__ */ jsx2(Slider, { progress: view.progress, label: "Playhead", valueMin: 0, valueMax: view.duration, valueNow: view.currentTick, valueText: view.readout, onScrub: scrub })
997
1152
  ]
998
1153
  }
999
1154
  );
@@ -3338,10 +3493,10 @@ function gridStart(monthKey, firstDayOfWeek) {
3338
3493
  return addDays(first, -offset);
3339
3494
  }
3340
3495
  function statusTone(value) {
3341
- const text = String(value ?? "").toLowerCase();
3342
- if (/done|complete|published|paid|ok/.test(text)) return "success";
3343
- if (/pending|draft|scheduled|review/.test(text)) return "warning";
3344
- if (/fail|error|cancel|reject|overdue/.test(text)) return "error";
3496
+ const text2 = String(value ?? "").toLowerCase();
3497
+ if (/done|complete|published|paid|ok/.test(text2)) return "success";
3498
+ if (/pending|draft|scheduled|review/.test(text2)) return "warning";
3499
+ if (/fail|error|cancel|reject|overdue/.test(text2)) return "error";
3345
3500
  return "neutral";
3346
3501
  }
3347
3502
  function columnMap(result) {
@@ -4403,6 +4558,9 @@ export {
4403
4558
  MIN_MAJOR_PX,
4404
4559
  MIN_MINOR_PX,
4405
4560
  TIMELINE_PANEL_ID,
4561
+ BEHAVIOURS as TRANSPORT_BEHAVIOURS,
4562
+ TRANSPORT_DECLARATION,
4563
+ TRANSPORT_ICONS,
4406
4564
  TRIM_SNAP_PX,
4407
4565
  TimelineController,
4408
4566
  TimelinePanelView,
@@ -4413,6 +4571,7 @@ export {
4413
4571
  addDays,
4414
4572
  addMonths,
4415
4573
  alignToTileGrid,
4574
+ applyTransportUi,
4416
4575
  beginDrag,
4417
4576
  blitTile,
4418
4577
  buildCells,
@@ -4483,6 +4642,9 @@ export {
4483
4642
  todayKey,
4484
4643
  transportManifest,
4485
4644
  transportPanel,
4645
+ transportPanelView,
4646
+ transportViewManifest,
4647
+ transportViewState,
4486
4648
  trimModeFor,
4487
4649
  visibleClips,
4488
4650
  visibleRows,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xenosystem/blocks",
3
- "version": "0.11.5",
3
+ "version": "0.12.0",
4
4
  "description": "XENO Blocks — rung 3 of the front ladder: components composed into a contract-bearing unit (manifest: ports, intents, commands, agent surface), rendered by @xenosystem/workbench. ONE package, one subpath per family (/auth, /edit, /data, /time, /ops, /trust, /agent), one named export per block — XENO PACKAGE NAMING - STANDARD.md.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -59,8 +59,8 @@
59
59
  },
60
60
  "peerDependencies": {
61
61
  "@duckdb/duckdb-wasm": ">=1.28.0",
62
- "@xenosystem/block-sdk": "^0.3.0",
63
- "@xenosystem/components": ">=0.14.0 <1",
62
+ "@xenosystem/block-sdk": ">=0.3.2 <1",
63
+ "@xenosystem/components": ">=0.16.0 <1",
64
64
  "@xenosystem/data-core": "^0.1.0",
65
65
  "@xenosystem/elements-react": ">=0.0.1",
66
66
  "@xenosystem/time-core": "^0.1.0",
@@ -75,8 +75,8 @@
75
75
  "@types/node": "^22.0.0",
76
76
  "@types/react": "^19.0.0",
77
77
  "@types/react-dom": "^19.0.0",
78
- "@xenosystem/block-sdk": "^0.3.1",
79
- "@xenosystem/components": "^0.14.0",
78
+ "@xenosystem/block-sdk": "^0.3.2",
79
+ "@xenosystem/components": "^0.16.0",
80
80
  "@xenosystem/data-core": "^0.1.0",
81
81
  "@xenosystem/elements-react": "0.0.1",
82
82
  "@xenosystem/time-core": "^0.1.0",