@xenosystem/blocks 0.10.1 → 0.11.1

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,6 +1,7 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
3
  import { PanelModule, PanelManifest, BlockDeclaration } from '@xenosystem/block-sdk';
4
+ import { PanelTone } from '@xenosystem/components/chrome';
4
5
 
5
6
  interface CreateAuthGatePanelOptions {
6
7
  /** What renders behind the gate once it is `open`. A product passes its app; the harness a fixture. */
@@ -371,9 +372,6 @@ declare function plateCopy(phase: XenoAuthGatePhase, product: string, message: s
371
372
  body: string;
372
373
  note?: string;
373
374
  };
374
- declare const BEHAVIOURS: {
375
- 'auth/authGateView': (inputs: Record<string, unknown>, config: Readonly<Record<string, unknown>>) => AuthGateView;
376
- };
377
375
 
378
376
  /**
379
377
  * `mountXenoAuthGate` — the per-product plug, exactly as `XENO AUTH - AUTH GATE DELTA.md` §4 specifies:
@@ -611,7 +609,109 @@ interface AccountPanelViewProps {
611
609
  showEntitlements?: boolean;
612
610
  emptyHint?: string;
613
611
  }
614
- /** The Account panel view. */
612
+ /**
613
+ * The Account panel view — the code twin of `ACCOUNT_DECLARATION`, rendering the SAME model
614
+ * (`accountPanelView`) so the two cannot drift; `test/auth-account/declaration.test.tsx` holds the markup
615
+ * byte-identical in every status.
616
+ */
615
617
  declare function AccountPanelView({ controller, showLedger, showEntitlements, emptyHint, }: AccountPanelViewProps): ReactNode;
616
618
 
617
- export { ACCOUNT_PANEL_ID, AUTH_GATE_DECLARATION, AUTH_GATE_INITIAL, AUTH_GATE_PANEL_ID, AccountController, type AccountControllerOptions, type AccountHostBridge, type AccountPanelState, AccountPanelView, type AccountPanelViewProps, type AccountRenderContext, type AccountViewState, type AuthGateAction, AuthGateController, type AuthGateHostBridge, AuthGateView$1 as AuthGateView, type AuthGateView as AuthGateViewModel, type AuthGateViewProps, BEHAVIOURS, type CreateAccountPanelOptions, type CreateAuthGatePanelOptions, type MountXenoAuthGateOptions, type MountedXenoAuthGate, PHASE_INTENTS, STEP_UP_ACTIONS, type XenoAccountAction, type XenoAccountActionKind, type XenoAccountCredits, type XenoAccountIdentity, type XenoAccountPlan, type XenoAccountState, type XenoAccountStatus, type XenoAuthGateBridge, type XenoAuthGateIntent, type XenoAuthGateIntentKind, type XenoAuthGatePhase, type XenoAuthGateState, type XenoEntitlement, type XenoLedgerDelta, type XenoLedgerEntry, type XenoLicenceState, accountManifest, accountPanel, authGateManifest, authGatePanel, authGateView, createAccountPanel, createAuthGatePanel, creditsAge, deriveAuthGatePhase, formatAge, formatAmount, formatSince, hostContradiction, intentOffered, isSignedIn, mountXenoAuthGate, plateCopy, requiresStepUp };
619
+ /**
620
+ * The account panel AS A DECLARATION — the third block written the §8d way (auth gate, agent, account).
621
+ * Chrome units + bindings + emits over ONE behaviour, `auth/accountPanelView`, which derives every label,
622
+ * tone and row shape from `AccountViewState`. The code view renders the same model; the test holds the
623
+ * two byte-identical in every account status.
624
+ *
625
+ * Presentation contract: input `view` (an `AccountViewState` push from the controller), output `ui`
626
+ * (`{ kind, seatId?, expanded? }`), applied by the controller (`request(kind)`, `refresh()`,
627
+ * `setLedgerExpanded()`). The declaration holds no state.
628
+ */
629
+
630
+ type AccountUiIntent = {
631
+ kind: 'signIn';
632
+ } | {
633
+ kind: 'signOut';
634
+ } | {
635
+ kind: 'manageBilling';
636
+ } | {
637
+ kind: 'refresh';
638
+ } | {
639
+ kind: 'switchSeat';
640
+ seatId: string;
641
+ } | {
642
+ kind: 'ledgerCollapsed';
643
+ collapsed: boolean;
644
+ };
645
+ declare const accountViewManifest: PanelManifest;
646
+ declare const STATUS_LABEL: Record<XenoAccountStatus, string>;
647
+ declare const STATUS_TONE: Record<XenoAccountStatus, PanelTone>;
648
+ interface AccountPanelViewModel {
649
+ loading: boolean;
650
+ signedOut: boolean;
651
+ signedIn: boolean;
652
+ loadingHint: string;
653
+ statusLabel: string;
654
+ statusTone: PanelTone;
655
+ signedOutHint: string;
656
+ planLabel?: string;
657
+ planTitle?: string;
658
+ name?: string;
659
+ email?: string;
660
+ message?: string;
661
+ hasCredits: boolean;
662
+ creditsUnit: string;
663
+ creditsBalance: string;
664
+ creditsSub?: string;
665
+ creditsStale: boolean;
666
+ creditsTitle: string;
667
+ hasIncluded: boolean;
668
+ includedValue: string;
669
+ includedSub?: string;
670
+ showSeats: boolean;
671
+ seats: {
672
+ id: string;
673
+ label: string;
674
+ active: boolean;
675
+ inactive: boolean;
676
+ }[];
677
+ showEntitlements: boolean;
678
+ entitlements: {
679
+ id: string;
680
+ label: string;
681
+ detail?: string;
682
+ tone: PanelTone;
683
+ text: string;
684
+ }[];
685
+ showLedger: boolean;
686
+ ledgerCollapsed: boolean;
687
+ ledgerCount?: string;
688
+ hasLedger: boolean;
689
+ ledgerEmpty: boolean;
690
+ ledger: {
691
+ id: string;
692
+ description: string;
693
+ source?: string;
694
+ amount: string;
695
+ }[];
696
+ ledgerHasMore: boolean;
697
+ renews?: string;
698
+ offlineNote?: string;
699
+ }
700
+ interface AccountPanelViewOptions {
701
+ showLedger?: boolean;
702
+ showEntitlements?: boolean;
703
+ emptyHint?: string;
704
+ }
705
+ declare function accountPanelView(state: AccountViewState, options?: AccountPanelViewOptions): AccountPanelViewModel;
706
+ declare const BEHAVIOURS$1: {
707
+ 'auth/accountPanelView': (inputs: Record<string, unknown>, config: Readonly<Record<string, unknown>>) => AccountPanelViewModel;
708
+ };
709
+ declare const ACCOUNT_DECLARATION: BlockDeclaration;
710
+
711
+ /** Every behaviour of the auth family — the gate's and the account's — for a host's block registry. */
712
+ declare const BEHAVIOURS: {
713
+ 'auth/accountPanelView': (inputs: Record<string, unknown>, config: Readonly<Record<string, unknown>>) => AccountPanelViewModel;
714
+ 'auth/authGateView': (inputs: Record<string, unknown>, config: Readonly<Record<string, unknown>>) => AuthGateView;
715
+ };
716
+
717
+ export { BEHAVIOURS$1 as ACCOUNT_BEHAVIOURS, ACCOUNT_DECLARATION, ACCOUNT_PANEL_ID, AUTH_GATE_DECLARATION, AUTH_GATE_INITIAL, AUTH_GATE_PANEL_ID, AccountController, type AccountControllerOptions, type AccountHostBridge, type AccountPanelState, AccountPanelView, type AccountPanelViewModel, type AccountPanelViewOptions, type AccountPanelViewProps, type AccountRenderContext, type AccountUiIntent, type AccountViewState, type AuthGateAction, AuthGateController, type AuthGateHostBridge, AuthGateView$1 as AuthGateView, type AuthGateView as AuthGateViewModel, type AuthGateViewProps, BEHAVIOURS, type CreateAccountPanelOptions, type CreateAuthGatePanelOptions, type MountXenoAuthGateOptions, type MountedXenoAuthGate, PHASE_INTENTS, STATUS_LABEL, STATUS_TONE, STEP_UP_ACTIONS, type XenoAccountAction, type XenoAccountActionKind, type XenoAccountCredits, type XenoAccountIdentity, type XenoAccountPlan, type XenoAccountState, type XenoAccountStatus, type XenoAuthGateBridge, type XenoAuthGateIntent, type XenoAuthGateIntentKind, type XenoAuthGatePhase, type XenoAuthGateState, type XenoEntitlement, type XenoLedgerDelta, type XenoLedgerEntry, type XenoLicenceState, accountManifest, accountPanel, accountPanelView, accountViewManifest, authGateManifest, authGatePanel, authGateView, createAccountPanel, createAuthGatePanel, creditsAge, deriveAuthGatePhase, formatAge, formatAmount, formatSince, hostContradiction, intentOffered, isSignedIn, mountXenoAuthGate, plateCopy, requiresStepUp };
@@ -318,6 +318,365 @@ var AUTH_GATE_DECLARATION = {
318
318
  }
319
319
  };
320
320
 
321
+ // src/auth/account/manifest.ts
322
+ import { WELL_KNOWN_PORT_SCHEMAS } from "@xenosystem/block-sdk";
323
+ var ACCOUNT_PANEL_ID = "xeno.core.account";
324
+ var accountManifest = {
325
+ id: ACCOUNT_PANEL_ID,
326
+ version: "0.1.0",
327
+ title: "Account",
328
+ icon: "user-round",
329
+ description: "Identity, plan, credits, entitlements and ledger \u2014 all host-pushed, never fetched. Balances carry the moment they were true. Sensitive actions are emitted as step-up intents, never performed.",
330
+ defaultSlot: "inspector",
331
+ inputs: [
332
+ {
333
+ id: "account",
334
+ name: "Account",
335
+ type: "object",
336
+ schema: WELL_KNOWN_PORT_SCHEMAS.ACCOUNT,
337
+ description: "{status, identity?, plan?, credits?, entitlements?, seats?, message?}. The host is authoritative. Contains NO token \u2014 see AUTH L9.",
338
+ multiple: false
339
+ },
340
+ {
341
+ id: "ledger",
342
+ name: "Ledger",
343
+ type: "object",
344
+ schema: WELL_KNOWN_PORT_SCHEMAS.LEDGER,
345
+ description: "{rev?, append?, replace?, clear?, hasMore?}. Append-oriented; `rev` gates out-of-order deliveries.",
346
+ multiple: true
347
+ }
348
+ ],
349
+ outputs: [
350
+ {
351
+ id: "action",
352
+ name: "Action",
353
+ type: "object",
354
+ schema: WELL_KNOWN_PORT_SCHEMAS.ACCOUNT_ACTION,
355
+ description: "{kind, seatId?, productId?, stepUp?}. An INTENT. `stepUp: true` marks an action AUTH L12 requires a fresh interactive re-auth for; the host must not satisfy it silently from the broker."
356
+ }
357
+ ],
358
+ commands: [
359
+ {
360
+ id: "refresh",
361
+ title: "Refresh",
362
+ description: "Ask the host to re-read the balance. Idempotent; no step-up.",
363
+ parameters: {}
364
+ },
365
+ {
366
+ id: "get_account",
367
+ title: "Get Account",
368
+ description: "Return the rendered account state \u2014 status, plan, balance and its asOf. Never a token.",
369
+ parameters: {}
370
+ },
371
+ {
372
+ id: "get_ledger",
373
+ title: "Get Ledger",
374
+ description: "Return the held ledger entries, newest first.",
375
+ parameters: {}
376
+ }
377
+ // NOTE: `signOut`, `purchase` and `manageBilling` are deliberately NOT commands. They require a
378
+ // fresh interactive re-auth (AUTH L12), and an agent-invocable command is by definition not
379
+ // interactive. Exposing them would make this panel the bypass for the step-up requirement.
380
+ ],
381
+ config: [
382
+ {
383
+ key: "showLedger",
384
+ label: "Show ledger",
385
+ type: "boolean",
386
+ defaultValue: true,
387
+ description: "Show recent credit activity."
388
+ },
389
+ {
390
+ key: "showEntitlements",
391
+ label: "Show entitlements",
392
+ type: "boolean",
393
+ defaultValue: true,
394
+ description: "List what the plan includes. Display only \u2014 entitlement is enforced server-side (AUTH L10)."
395
+ },
396
+ {
397
+ key: "staleAfterMs",
398
+ label: "Stale after (ms)",
399
+ type: "number",
400
+ defaultValue: 6e4,
401
+ description: "How old a balance may be before it is labelled stale."
402
+ },
403
+ {
404
+ key: "maxLedger",
405
+ label: "Max ledger entries",
406
+ type: "number",
407
+ defaultValue: 500,
408
+ description: "Entries retained in memory."
409
+ },
410
+ {
411
+ key: "emptyHint",
412
+ label: "Empty hint",
413
+ type: "text",
414
+ description: "Shown when no account state has arrived.",
415
+ placeholder: "Waiting for account state\u2026"
416
+ }
417
+ ],
418
+ capabilities: ["storage.local"],
419
+ sdk: "^1.1.0"
420
+ };
421
+
422
+ // src/auth/account/types.ts
423
+ var STEP_UP_ACTIONS = /* @__PURE__ */ new Set([
424
+ "signOut",
425
+ "manageBilling",
426
+ "purchase"
427
+ ]);
428
+ function isSignedIn(status) {
429
+ return status === "signedIn" || status === "refreshing" || status === "offline";
430
+ }
431
+ function requiresStepUp(kind) {
432
+ return STEP_UP_ACTIONS.has(kind);
433
+ }
434
+ function creditsAge(credits, now) {
435
+ if (!credits) return null;
436
+ return Math.max(0, now - credits.asOf);
437
+ }
438
+ function formatAmount(amount, unit = "") {
439
+ const sign = amount > 0 ? "+" : "";
440
+ const suffix = unit ? ` ${unit}` : "";
441
+ return `${sign}${amount.toLocaleString()}${suffix}`;
442
+ }
443
+ function formatAge(ms) {
444
+ if (ms < 6e4) return `${Math.round(ms / 1e3)}s ago`;
445
+ if (ms < 36e5) return `${Math.round(ms / 6e4)}m ago`;
446
+ if (ms < 864e5) return `${Math.round(ms / 36e5)}h ago`;
447
+ return `${Math.round(ms / 864e5)}d ago`;
448
+ }
449
+
450
+ // src/auth/account/declaration.ts
451
+ var accountViewManifest = {
452
+ id: ACCOUNT_PANEL_ID,
453
+ version: "1.0.0",
454
+ title: "Account",
455
+ category: "account",
456
+ layout: { slot: "side" },
457
+ inputs: [{ id: "view", type: "object", label: "View state", description: "The AccountViewState the controller derives from the host ports." }],
458
+ outputs: [{ id: "ui", type: "object", label: "UI intent", description: "{ kind, seatId?, collapsed? } \u2014 applied by the controller." }],
459
+ commands: []
460
+ };
461
+ var STATUS_LABEL = {
462
+ unknown: "Loading",
463
+ signedOut: "Signed out",
464
+ signedIn: "Signed in",
465
+ refreshing: "Refreshing",
466
+ expired: "Session expired",
467
+ offline: "Offline"
468
+ };
469
+ var STATUS_TONE = {
470
+ unknown: "neutral",
471
+ signedOut: "neutral",
472
+ signedIn: "success",
473
+ refreshing: "neutral",
474
+ expired: "warning",
475
+ offline: "warning"
476
+ };
477
+ function accountPanelView(state, options = {}) {
478
+ const showLedger = options.showLedger ?? true;
479
+ const showEntitlements = options.showEntitlements ?? true;
480
+ const { identity, plan, credits } = state;
481
+ const signedIn = state.status !== "unknown" && isSignedIn(state.status);
482
+ return {
483
+ loading: state.status === "unknown",
484
+ signedOut: state.status !== "unknown" && !isSignedIn(state.status),
485
+ signedIn,
486
+ loadingHint: options.emptyHint ?? "Waiting for account state\u2026",
487
+ statusLabel: STATUS_LABEL[state.status],
488
+ statusTone: STATUS_TONE[state.status],
489
+ signedOutHint: state.message ?? "Sign in to see your plan and credits.",
490
+ planLabel: plan?.label,
491
+ planTitle: plan ? `Plan: ${plan.label}` : void 0,
492
+ name: identity?.name ?? identity?.sub,
493
+ email: identity?.email,
494
+ message: state.message ?? void 0,
495
+ hasCredits: credits !== null,
496
+ creditsUnit: credits?.unit ?? "credits",
497
+ creditsBalance: credits ? credits.balance.toLocaleString() : "",
498
+ creditsSub: credits && state.creditsAgeMs !== null ? state.creditsStale ? `stale \u2014 as of ${formatAge(state.creditsAgeMs)}` : `as of ${formatAge(state.creditsAgeMs)}` : void 0,
499
+ creditsStale: state.creditsStale,
500
+ creditsTitle: "Pushed by the host, not fetched by this panel.",
501
+ hasIncluded: credits?.included !== void 0,
502
+ includedValue: credits?.included !== void 0 ? credits.included.toLocaleString() : "",
503
+ includedSub: credits?.used === void 0 ? void 0 : `${credits.used.toLocaleString()} used`,
504
+ showSeats: state.seats.length > 1,
505
+ seats: state.seats.map((s) => ({ id: s.id, label: s.label, active: !!s.active, inactive: !s.active })),
506
+ showEntitlements: showEntitlements && state.entitlements.length > 0,
507
+ entitlements: state.entitlements.map((e) => ({ id: e.id, label: e.label, detail: e.detail, tone: e.included ? "success" : "neutral", text: e.included ? "yes" : "no" })),
508
+ showLedger,
509
+ ledgerCollapsed: !state.ledgerExpanded,
510
+ ledgerCount: state.ledger.length > 0 ? String(state.ledger.length) : void 0,
511
+ hasLedger: state.ledger.length > 0,
512
+ ledgerEmpty: state.ledger.length === 0,
513
+ ledger: state.ledger.map((e) => ({ id: e.id, description: e.description, source: e.source, amount: formatAmount(e.amount) })),
514
+ ledgerHasMore: state.ledgerHasMore,
515
+ renews: plan?.renewsAt ? `renews ${new Date(plan.renewsAt).toLocaleDateString()}` : void 0,
516
+ offlineNote: state.status === "offline" ? "offline \u2014 showing last known" : void 0
517
+ };
518
+ }
519
+ var EMPTY = { status: "unknown", identity: null, plan: null, credits: null, entitlements: [], seats: [], message: null, ledger: [], ledgerHasMore: false, ledgerExpanded: false, creditsAgeMs: null, creditsStale: false };
520
+ var BEHAVIOURS2 = {
521
+ "auth/accountPanelView": (inputs, config) => accountPanelView(inputs.view ?? EMPTY, {
522
+ showLedger: typeof config.showLedger === "boolean" ? config.showLedger : void 0,
523
+ showEntitlements: typeof config.showEntitlements === "boolean" ? config.showEntitlements : void 0,
524
+ emptyHint: typeof config.emptyHint === "string" ? config.emptyHint : void 0
525
+ })
526
+ };
527
+ var ui = (kind) => ({ emit: "ui", value: { object: { kind: { value: kind } } } });
528
+ var text = (b) => ({ component: "text", props: { text: b } });
529
+ var badge = (b, when, props) => ({ component: "chrome/Badge", ...when ? { when } : {}, ...props ? { props } : {}, children: [text(b)] });
530
+ var ACCOUNT_DECLARATION = {
531
+ format: "xeno.block@1",
532
+ manifest: accountViewManifest,
533
+ behaviour: "auth/accountPanelView",
534
+ composition: {
535
+ component: "fragment",
536
+ children: [
537
+ { component: "chrome/EmptyState", when: { input: "view.loading" }, props: { title: { value: "Account" }, hint: { input: "view.loadingHint" } } },
538
+ {
539
+ component: "chrome/EmptyState",
540
+ when: { input: "view.signedOut" },
541
+ props: {
542
+ title: { input: "view.statusLabel" },
543
+ hint: { input: "view.signedOutHint" },
544
+ action: { node: { component: "chrome/TextButton", props: { strong: { value: true } }, on: { onClick: ui("signIn") }, children: [text({ value: "Sign in" })] } }
545
+ }
546
+ },
547
+ {
548
+ component: "chrome/Column",
549
+ when: { input: "view.signedIn" },
550
+ children: [
551
+ {
552
+ component: "chrome/Toolbar",
553
+ props: {
554
+ left: { node: { component: "chrome/ToolbarGroup", children: [badge({ input: "view.statusLabel" }, void 0, { tone: { input: "view.statusTone" } }), badge({ input: "view.planLabel" }, { input: "view.planLabel" }, { title: { input: "view.planTitle" } })] } },
555
+ right: { node: { component: "chrome/ToolbarGroup", props: { end: { value: true } }, children: [{ component: "chrome/TextButton", on: { onClick: ui("refresh") }, children: [text({ value: "Refresh" })] }] } }
556
+ }
557
+ },
558
+ {
559
+ component: "chrome/ScrollArea",
560
+ children: [
561
+ {
562
+ component: "chrome/Stack",
563
+ children: [
564
+ { component: "chrome/Text", props: { role: { value: "title" } }, children: [text({ input: "view.name" })] },
565
+ { component: "chrome/Text", when: { input: "view.email" }, props: { role: { value: "meta" } }, children: [text({ input: "view.email" })] },
566
+ { component: "chrome/Text", when: { input: "view.message" }, props: { role: { value: "meta" } }, children: [text({ input: "view.message" })] }
567
+ ]
568
+ },
569
+ {
570
+ component: "chrome/Stack",
571
+ when: { input: "view.hasCredits" },
572
+ props: { direction: { value: "row" }, pad: { value: "inset" } },
573
+ children: [
574
+ {
575
+ component: "chrome/StatTile",
576
+ props: {
577
+ label: { input: "view.creditsUnit" },
578
+ value: { input: "view.creditsBalance" },
579
+ sub: { input: "view.creditsSub" },
580
+ icon: { node: badge({ value: "stale" }, { input: "view.creditsStale" }, { tone: { value: "warning" } }) },
581
+ title: { input: "view.creditsTitle" }
582
+ }
583
+ },
584
+ { component: "chrome/StatTile", when: { input: "view.hasIncluded" }, props: { label: { value: "included" }, value: { input: "view.includedValue" }, sub: { input: "view.includedSub" } } }
585
+ ]
586
+ },
587
+ {
588
+ component: "chrome/Section",
589
+ when: { input: "view.showSeats" },
590
+ props: { title: { value: "Seats" }, flush: { value: true } },
591
+ children: [
592
+ {
593
+ component: "chrome/RowList",
594
+ children: [
595
+ {
596
+ component: "chrome/Row",
597
+ each: { input: "view.seats", key: "id" },
598
+ props: {
599
+ noIcon: { value: true },
600
+ label: { item: "label" },
601
+ trailing: {
602
+ node: {
603
+ component: "fragment",
604
+ children: [
605
+ badge({ value: "active" }, { item: "active" }),
606
+ { component: "chrome/TextButton", when: { item: "inactive" }, on: { onClick: { emit: "ui", value: { object: { kind: { value: "switchSeat" }, seatId: { item: "id" } } } } }, children: [text({ value: "Switch" })] }
607
+ ]
608
+ }
609
+ }
610
+ }
611
+ }
612
+ ]
613
+ }
614
+ ]
615
+ },
616
+ {
617
+ component: "chrome/Section",
618
+ when: { input: "view.showEntitlements" },
619
+ props: { title: { value: "Included" }, flush: { value: true } },
620
+ children: [
621
+ {
622
+ component: "chrome/RowList",
623
+ children: [
624
+ {
625
+ component: "chrome/Row",
626
+ each: { input: "view.entitlements", key: "id" },
627
+ props: { noIcon: { value: true }, label: { item: "label" }, meta: { item: "detail" }, trailing: { node: badge({ item: "text" }, void 0, { tone: { item: "tone" } }) } }
628
+ }
629
+ ]
630
+ }
631
+ ]
632
+ },
633
+ {
634
+ component: "chrome/Section",
635
+ when: { input: "view.showLedger" },
636
+ props: { title: { value: "Recent activity" }, flush: { value: true }, collapsed: { input: "view.ledgerCollapsed" }, badge: { node: badge({ input: "view.ledgerCount" }, { input: "view.ledgerCount" }) } },
637
+ on: { onCollapsedChange: { emit: "ui", value: { object: { kind: { value: "ledgerCollapsed" }, collapsed: { arg: "" } } } } },
638
+ children: [
639
+ { component: "chrome/EmptyState", when: { input: "view.ledgerEmpty" }, props: { title: { value: "No activity" }, hint: { value: "Credit spends and grants appear here." } } },
640
+ {
641
+ component: "chrome/RowList",
642
+ when: { input: "view.hasLedger" },
643
+ children: [
644
+ {
645
+ component: "chrome/Row",
646
+ each: { input: "view.ledger", key: "id" },
647
+ props: { noIcon: { value: true }, label: { item: "description" }, meta: { item: "source" }, trailing: { node: { component: "chrome/Text", props: { role: { value: "number" } }, children: [text({ item: "amount" })] } } }
648
+ }
649
+ ]
650
+ },
651
+ { component: "chrome/Stack", when: { input: "view.ledgerHasMore" }, props: { pad: { value: "inset" } }, children: [{ component: "chrome/Text", props: { role: { value: "caption" } }, children: [text({ value: "Older entries are not held by this panel." })] }] }
652
+ ]
653
+ }
654
+ ]
655
+ },
656
+ { component: "chrome/Stack", props: { pad: { value: "inset" } }, children: [{ component: "chrome/Text", props: { role: { value: "caption" } }, children: [text({ value: "Billing and sign-out ask you to confirm who you are." })] }] },
657
+ {
658
+ component: "chrome/Toolbar",
659
+ props: {
660
+ divided: { value: false },
661
+ right: {
662
+ node: {
663
+ component: "chrome/ToolbarGroup",
664
+ props: { end: { value: true } },
665
+ children: [
666
+ { component: "chrome/TextButton", on: { onClick: ui("manageBilling") }, children: [text({ value: "Billing\u2026" })] },
667
+ { component: "chrome/TextButton", on: { onClick: ui("signOut") }, children: [text({ value: "Sign out\u2026" })] }
668
+ ]
669
+ }
670
+ }
671
+ }
672
+ },
673
+ { component: "chrome/StatusBar", props: { left: { input: "view.renews" }, right: { input: "view.offlineNote" } } }
674
+ ]
675
+ }
676
+ ]
677
+ }
678
+ };
679
+
321
680
  // src/auth/gate/mount.ts
322
681
  import { createElement as createElement2 } from "react";
323
682
  import { createRoot as createRoot2 } from "react-dom/client";
@@ -355,34 +714,6 @@ import {
355
714
  bindConfig as bindConfig2
356
715
  } from "@xenosystem/block-sdk";
357
716
 
358
- // src/auth/account/types.ts
359
- var STEP_UP_ACTIONS = /* @__PURE__ */ new Set([
360
- "signOut",
361
- "manageBilling",
362
- "purchase"
363
- ]);
364
- function isSignedIn(status) {
365
- return status === "signedIn" || status === "refreshing" || status === "offline";
366
- }
367
- function requiresStepUp(kind) {
368
- return STEP_UP_ACTIONS.has(kind);
369
- }
370
- function creditsAge(credits, now) {
371
- if (!credits) return null;
372
- return Math.max(0, now - credits.asOf);
373
- }
374
- function formatAmount(amount, unit = "") {
375
- const sign = amount > 0 ? "+" : "";
376
- const suffix = unit ? ` ${unit}` : "";
377
- return `${sign}${amount.toLocaleString()}${suffix}`;
378
- }
379
- function formatAge(ms) {
380
- if (ms < 6e4) return `${Math.round(ms / 1e3)}s ago`;
381
- if (ms < 36e5) return `${Math.round(ms / 6e4)}m ago`;
382
- if (ms < 864e5) return `${Math.round(ms / 36e5)}h ago`;
383
- return `${Math.round(ms / 864e5)}d ago`;
384
- }
385
-
386
717
  // src/auth/account/controller.ts
387
718
  var EMPTY_STATE = { status: "unknown" };
388
719
  function ageBucket(ms) {
@@ -593,107 +924,6 @@ var AccountController = class {
593
924
  }
594
925
  };
595
926
 
596
- // src/auth/account/manifest.ts
597
- import { WELL_KNOWN_PORT_SCHEMAS } from "@xenosystem/block-sdk";
598
- var ACCOUNT_PANEL_ID = "xeno.core.account";
599
- var accountManifest = {
600
- id: ACCOUNT_PANEL_ID,
601
- version: "0.1.0",
602
- title: "Account",
603
- icon: "user-round",
604
- description: "Identity, plan, credits, entitlements and ledger \u2014 all host-pushed, never fetched. Balances carry the moment they were true. Sensitive actions are emitted as step-up intents, never performed.",
605
- defaultSlot: "inspector",
606
- inputs: [
607
- {
608
- id: "account",
609
- name: "Account",
610
- type: "object",
611
- schema: WELL_KNOWN_PORT_SCHEMAS.ACCOUNT,
612
- description: "{status, identity?, plan?, credits?, entitlements?, seats?, message?}. The host is authoritative. Contains NO token \u2014 see AUTH L9.",
613
- multiple: false
614
- },
615
- {
616
- id: "ledger",
617
- name: "Ledger",
618
- type: "object",
619
- schema: WELL_KNOWN_PORT_SCHEMAS.LEDGER,
620
- description: "{rev?, append?, replace?, clear?, hasMore?}. Append-oriented; `rev` gates out-of-order deliveries.",
621
- multiple: true
622
- }
623
- ],
624
- outputs: [
625
- {
626
- id: "action",
627
- name: "Action",
628
- type: "object",
629
- schema: WELL_KNOWN_PORT_SCHEMAS.ACCOUNT_ACTION,
630
- description: "{kind, seatId?, productId?, stepUp?}. An INTENT. `stepUp: true` marks an action AUTH L12 requires a fresh interactive re-auth for; the host must not satisfy it silently from the broker."
631
- }
632
- ],
633
- commands: [
634
- {
635
- id: "refresh",
636
- title: "Refresh",
637
- description: "Ask the host to re-read the balance. Idempotent; no step-up.",
638
- parameters: {}
639
- },
640
- {
641
- id: "get_account",
642
- title: "Get Account",
643
- description: "Return the rendered account state \u2014 status, plan, balance and its asOf. Never a token.",
644
- parameters: {}
645
- },
646
- {
647
- id: "get_ledger",
648
- title: "Get Ledger",
649
- description: "Return the held ledger entries, newest first.",
650
- parameters: {}
651
- }
652
- // NOTE: `signOut`, `purchase` and `manageBilling` are deliberately NOT commands. They require a
653
- // fresh interactive re-auth (AUTH L12), and an agent-invocable command is by definition not
654
- // interactive. Exposing them would make this panel the bypass for the step-up requirement.
655
- ],
656
- config: [
657
- {
658
- key: "showLedger",
659
- label: "Show ledger",
660
- type: "boolean",
661
- defaultValue: true,
662
- description: "Show recent credit activity."
663
- },
664
- {
665
- key: "showEntitlements",
666
- label: "Show entitlements",
667
- type: "boolean",
668
- defaultValue: true,
669
- description: "List what the plan includes. Display only \u2014 entitlement is enforced server-side (AUTH L10)."
670
- },
671
- {
672
- key: "staleAfterMs",
673
- label: "Stale after (ms)",
674
- type: "number",
675
- defaultValue: 6e4,
676
- description: "How old a balance may be before it is labelled stale."
677
- },
678
- {
679
- key: "maxLedger",
680
- label: "Max ledger entries",
681
- type: "number",
682
- defaultValue: 500,
683
- description: "Entries retained in memory."
684
- },
685
- {
686
- key: "emptyHint",
687
- label: "Empty hint",
688
- type: "text",
689
- description: "Shown when no account state has arrived.",
690
- placeholder: "Waiting for account state\u2026"
691
- }
692
- ],
693
- capabilities: ["storage.local"],
694
- sdk: "^1.1.0"
695
- };
696
-
697
927
  // src/auth/account/react/AccountPanelView.tsx
698
928
  import { useSyncExternalStore } from "react";
699
929
  import {
@@ -712,23 +942,7 @@ import {
712
942
  Toolbar,
713
943
  ToolbarGroup
714
944
  } from "@xenosystem/components/chrome";
715
- import { jsx as jsx2, jsxs } from "react/jsx-runtime";
716
- var STATUS_LABEL = {
717
- unknown: "Loading",
718
- signedOut: "Signed out",
719
- signedIn: "Signed in",
720
- refreshing: "Refreshing",
721
- expired: "Session expired",
722
- offline: "Offline"
723
- };
724
- var STATUS_TONE = {
725
- unknown: "neutral",
726
- signedOut: "neutral",
727
- signedIn: "success",
728
- refreshing: "neutral",
729
- expired: "warning",
730
- offline: "warning"
731
- };
945
+ import { Fragment as Fragment2, jsx as jsx2, jsxs } from "react/jsx-runtime";
732
946
  function AccountPanelView({
733
947
  controller,
734
948
  showLedger = true,
@@ -736,103 +950,76 @@ function AccountPanelView({
736
950
  emptyHint
737
951
  }) {
738
952
  const state = useSyncExternalStore(controller.subscribe, controller.getState, controller.getState);
739
- if (state.status === "unknown") {
740
- return /* @__PURE__ */ jsx2(EmptyState, { title: "Account", hint: emptyHint ?? "Waiting for account state\u2026" });
741
- }
742
- if (!isSignedIn(state.status)) {
953
+ const v = accountPanelView(state, { showLedger, showEntitlements, emptyHint });
954
+ if (v.loading) return /* @__PURE__ */ jsx2(EmptyState, { title: "Account", hint: v.loadingHint });
955
+ if (v.signedOut) {
743
956
  return /* @__PURE__ */ jsx2(
744
957
  EmptyState,
745
958
  {
746
- title: STATUS_LABEL[state.status],
747
- hint: state.message ?? "Sign in to see your plan and credits.",
959
+ title: v.statusLabel,
960
+ hint: v.signedOutHint,
748
961
  action: /* @__PURE__ */ jsx2(TextButton, { strong: true, onClick: () => controller.request("signIn"), children: "Sign in" })
749
962
  }
750
963
  );
751
964
  }
752
- const { identity, plan, credits } = state;
753
- const unit = credits?.unit ?? "credits";
754
965
  return /* @__PURE__ */ jsxs(Column, { children: [
755
966
  /* @__PURE__ */ jsx2(
756
967
  Toolbar,
757
968
  {
758
969
  left: /* @__PURE__ */ jsxs(ToolbarGroup, { children: [
759
- /* @__PURE__ */ jsx2(Badge, { tone: STATUS_TONE[state.status], children: STATUS_LABEL[state.status] }),
760
- plan ? /* @__PURE__ */ jsx2(Badge, { title: `Plan: ${plan.label}`, children: plan.label }) : null
970
+ /* @__PURE__ */ jsx2(Badge, { tone: v.statusTone, children: v.statusLabel }),
971
+ v.planLabel ? /* @__PURE__ */ jsx2(Badge, { title: v.planTitle, children: v.planLabel }) : null
761
972
  ] }),
762
973
  right: /* @__PURE__ */ jsx2(ToolbarGroup, { end: true, children: /* @__PURE__ */ jsx2(TextButton, { onClick: () => controller.refresh(), children: "Refresh" }) })
763
974
  }
764
975
  ),
765
976
  /* @__PURE__ */ jsxs(ScrollArea, { children: [
766
977
  /* @__PURE__ */ jsxs(Stack, { children: [
767
- /* @__PURE__ */ jsx2(Text, { role: "title", children: identity?.name ?? identity?.sub }),
768
- identity?.email ? /* @__PURE__ */ jsx2(Text, { role: "meta", children: identity.email }) : null,
769
- state.message ? /* @__PURE__ */ jsx2(Text, { role: "meta", children: state.message }) : null
978
+ /* @__PURE__ */ jsx2(Text, { role: "title", children: v.name }),
979
+ v.email ? /* @__PURE__ */ jsx2(Text, { role: "meta", children: v.email }) : null,
980
+ v.message ? /* @__PURE__ */ jsx2(Text, { role: "meta", children: v.message }) : null
770
981
  ] }),
771
- credits ? /* @__PURE__ */ jsxs(Stack, { direction: "row", pad: "inset", children: [
982
+ v.hasCredits ? /* @__PURE__ */ jsxs(Stack, { direction: "row", pad: "inset", children: [
772
983
  /* @__PURE__ */ jsx2(
773
984
  StatTile,
774
985
  {
775
- label: unit,
776
- value: credits.balance.toLocaleString(),
777
- sub: state.creditsAgeMs === null ? void 0 : state.creditsStale ? `as of ${formatAge(state.creditsAgeMs)} \u2014 may be out of date` : `as of ${formatAge(state.creditsAgeMs)}`,
778
- icon: state.creditsStale ? /* @__PURE__ */ jsx2(Badge, { tone: "warning", children: "stale" }) : void 0,
779
- title: "Pushed by the host, not fetched by this panel."
986
+ label: v.creditsUnit,
987
+ value: v.creditsBalance,
988
+ sub: v.creditsSub,
989
+ icon: v.creditsStale ? /* @__PURE__ */ jsx2(Badge, { tone: "warning", children: "stale" }) : null,
990
+ title: v.creditsTitle
780
991
  }
781
992
  ),
782
- credits.included !== void 0 ? /* @__PURE__ */ jsx2(
783
- StatTile,
784
- {
785
- label: "included",
786
- value: credits.included.toLocaleString(),
787
- sub: credits.used === void 0 ? void 0 : `${credits.used.toLocaleString()} used`
788
- }
789
- ) : null
993
+ v.hasIncluded ? /* @__PURE__ */ jsx2(StatTile, { label: "included", value: v.includedValue, sub: v.includedSub }) : null
790
994
  ] }) : null,
791
- state.seats.length > 1 ? /* @__PURE__ */ jsx2(Section, { title: "Seats", flush: true, children: /* @__PURE__ */ jsx2(RowList, { children: state.seats.map((seat) => /* @__PURE__ */ jsx2(
995
+ v.showSeats ? /* @__PURE__ */ jsx2(Section, { title: "Seats", flush: true, children: /* @__PURE__ */ jsx2(RowList, { children: v.seats.map((seat) => /* @__PURE__ */ jsx2(
792
996
  Row,
793
997
  {
794
998
  noIcon: true,
795
999
  label: seat.label,
796
- trailing: seat.active ? /* @__PURE__ */ jsx2(Badge, { children: "active" }) : /* @__PURE__ */ jsx2(
797
- TextButton,
798
- {
799
- onClick: () => controller.request("switchSeat", { seatId: seat.id }),
800
- children: "Switch"
801
- }
802
- )
1000
+ trailing: /* @__PURE__ */ jsxs(Fragment2, { children: [
1001
+ seat.active ? /* @__PURE__ */ jsx2(Badge, { children: "active" }) : null,
1002
+ seat.inactive ? /* @__PURE__ */ jsx2(TextButton, { onClick: () => controller.request("switchSeat", { seatId: seat.id }), children: "Switch" }) : null
1003
+ ] })
803
1004
  },
804
1005
  seat.id
805
1006
  )) }) }) : null,
806
- showEntitlements && state.entitlements.length > 0 ? /* @__PURE__ */ jsx2(Section, { title: "Included", flush: true, children: /* @__PURE__ */ jsx2(RowList, { children: state.entitlements.map((ent) => /* @__PURE__ */ jsx2(
807
- Row,
808
- {
809
- noIcon: true,
810
- label: ent.label,
811
- meta: ent.detail,
812
- trailing: /* @__PURE__ */ jsx2(Badge, { tone: ent.included ? "success" : "neutral", children: ent.included ? "yes" : "no" })
813
- },
814
- ent.id
1007
+ v.showEntitlements ? /* @__PURE__ */ jsx2(Section, { title: "Included", flush: true, children: /* @__PURE__ */ jsx2(RowList, { children: v.entitlements.map((ent) => (
1008
+ // A fact about the plan, not a permission check. The gate is server-side (L10).
1009
+ /* @__PURE__ */ jsx2(Row, { noIcon: true, label: ent.label, meta: ent.detail, trailing: /* @__PURE__ */ jsx2(Badge, { tone: ent.tone, children: ent.text }) }, ent.id)
815
1010
  )) }) }) : null,
816
- showLedger ? /* @__PURE__ */ jsxs(
1011
+ v.showLedger ? /* @__PURE__ */ jsxs(
817
1012
  Section,
818
1013
  {
819
1014
  title: "Recent activity",
820
1015
  flush: true,
821
- collapsed: !state.ledgerExpanded,
1016
+ collapsed: v.ledgerCollapsed,
822
1017
  onCollapsedChange: (collapsed) => controller.setLedgerExpanded(!collapsed),
823
- badge: state.ledger.length > 0 ? /* @__PURE__ */ jsx2(Badge, { children: state.ledger.length }) : void 0,
1018
+ badge: v.ledgerCount ? /* @__PURE__ */ jsx2(Badge, { children: v.ledgerCount }) : null,
824
1019
  children: [
825
- state.ledger.length === 0 ? /* @__PURE__ */ jsx2(EmptyState, { title: "No activity", hint: "Credit spends and grants appear here." }) : /* @__PURE__ */ jsx2(RowList, { children: state.ledger.map((entry) => /* @__PURE__ */ jsx2(
826
- Row,
827
- {
828
- noIcon: true,
829
- label: entry.description,
830
- meta: entry.source,
831
- trailing: /* @__PURE__ */ jsx2(Text, { role: "number", children: formatAmount(entry.amount) })
832
- },
833
- entry.id
834
- )) }),
835
- state.ledgerHasMore ? /* @__PURE__ */ jsx2(Stack, { pad: "inset", children: /* @__PURE__ */ jsx2(Text, { role: "caption", children: "Older entries are not held by this panel." }) }) : null
1020
+ v.ledgerEmpty ? /* @__PURE__ */ jsx2(EmptyState, { title: "No activity", hint: "Credit spends and grants appear here." }) : null,
1021
+ v.hasLedger ? /* @__PURE__ */ jsx2(RowList, { children: v.ledger.map((entry) => /* @__PURE__ */ jsx2(Row, { noIcon: true, label: entry.description, meta: entry.source, trailing: /* @__PURE__ */ jsx2(Text, { role: "number", children: entry.amount }) }, entry.id)) }) : null,
1022
+ v.ledgerHasMore ? /* @__PURE__ */ jsx2(Stack, { pad: "inset", children: /* @__PURE__ */ jsx2(Text, { role: "caption", children: "Older entries are not held by this panel." }) }) : null
836
1023
  ]
837
1024
  }
838
1025
  ) : null
@@ -848,13 +1035,7 @@ function AccountPanelView({
848
1035
  ] })
849
1036
  }
850
1037
  ),
851
- /* @__PURE__ */ jsx2(
852
- StatusBar,
853
- {
854
- left: plan?.renewsAt ? `renews ${new Date(plan.renewsAt).toLocaleDateString()}` : void 0,
855
- right: state.status === "offline" ? "offline \u2014 showing last known" : void 0
856
- }
857
- )
1038
+ /* @__PURE__ */ jsx2(StatusBar, { left: v.renews, right: v.offlineNote })
858
1039
  ] });
859
1040
  }
860
1041
 
@@ -958,7 +1139,12 @@ function createAccountPanel(options = {}) {
958
1139
  };
959
1140
  }
960
1141
  var accountPanel = createAccountPanel();
1142
+
1143
+ // src/auth/index.ts
1144
+ var BEHAVIOURS3 = { ...BEHAVIOURS, ...BEHAVIOURS2 };
961
1145
  export {
1146
+ BEHAVIOURS2 as ACCOUNT_BEHAVIOURS,
1147
+ ACCOUNT_DECLARATION,
962
1148
  ACCOUNT_PANEL_ID,
963
1149
  AUTH_GATE_DECLARATION,
964
1150
  AUTH_GATE_INITIAL,
@@ -967,11 +1153,15 @@ export {
967
1153
  AccountPanelView,
968
1154
  AuthGateController,
969
1155
  AuthGateView,
970
- BEHAVIOURS,
1156
+ BEHAVIOURS3 as BEHAVIOURS,
971
1157
  PHASE_INTENTS,
1158
+ STATUS_LABEL,
1159
+ STATUS_TONE,
972
1160
  STEP_UP_ACTIONS,
973
1161
  accountManifest,
974
1162
  accountPanel,
1163
+ accountPanelView,
1164
+ accountViewManifest,
975
1165
  authGateManifest,
976
1166
  authGatePanel,
977
1167
  authGateView,
@@ -780,6 +780,7 @@ function ticksPerStep(unit, timebase) {
780
780
 
781
781
  // src/time/transport/react/TransportPanelView.tsx
782
782
  import { useCallback, useEffect, useRef, useSyncExternalStore } from "react";
783
+ import { Column, IconButton, NumberInput, Readout, Slider, Stack, TextButton } from "@xenosystem/components/chrome";
783
784
 
784
785
  // src/time/transport/react/icons.tsx
785
786
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -928,9 +929,8 @@ function TransportPanelView({ controller }) {
928
929
  [controller]
929
930
  );
930
931
  return /* @__PURE__ */ jsxs2(
931
- "div",
932
+ Column,
932
933
  {
933
- style: S.root,
934
934
  className: "xeno-transport-panel",
935
935
  onKeyDown,
936
936
  onKeyUp,
@@ -938,228 +938,66 @@ function TransportPanelView({ controller }) {
938
938
  role: "group",
939
939
  "aria-label": "Transport",
940
940
  children: [
941
- /* @__PURE__ */ jsxs2("div", { style: S.row, children: [
942
- /* @__PURE__ */ jsx2("button", { type: "button", style: S.iconBtn, onClick: () => controller.goToStart(), title: "Go to start", "aria-label": "Go to start", children: /* @__PURE__ */ jsx2(StartIcon, {}) }),
943
- /* @__PURE__ */ jsx2("button", { type: "button", style: S.iconBtn, onClick: () => controller.step(-1), title: "Step back", "aria-label": "Step back", children: /* @__PURE__ */ jsx2(StepBackIcon, {}) }),
944
- controller.showsControl("shuttle") && /* @__PURE__ */ jsx2("button", { type: "button", style: S.iconBtn, onClick: () => controller.shuttleStep(-1), title: "Shuttle reverse (J)", "aria-label": "Shuttle reverse", children: /* @__PURE__ */ jsx2(ShuttleLeftIcon, {}) }),
941
+ /* @__PURE__ */ jsxs2(Stack, { direction: "row", gap: 3, pad: "gutter", children: [
942
+ /* @__PURE__ */ jsx2(IconButton, { size: "sm", icon: /* @__PURE__ */ jsx2(StartIcon, {}), label: "Go to start", onClick: () => controller.goToStart() }),
943
+ /* @__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
945
  /* @__PURE__ */ jsx2(
946
- "button",
947
- {
948
- type: "button",
949
- style: { ...S.iconBtn, ...S.primaryBtn },
950
- onClick: () => controller.togglePlay(),
951
- title: playing ? "Pause (Space)" : "Play (Space)",
952
- "aria-label": playing ? "Pause" : "Play",
953
- "aria-pressed": playing,
954
- children: playing ? /* @__PURE__ */ jsx2(PauseIcon, {}) : /* @__PURE__ */ jsx2(PlayIcon, {})
955
- }
956
- ),
957
- /* @__PURE__ */ jsx2("button", { type: "button", style: S.iconBtn, onClick: () => controller.stop(), title: "Stop", "aria-label": "Stop", children: /* @__PURE__ */ jsx2(StopIcon, {}) }),
958
- controller.showsControl("shuttle") && /* @__PURE__ */ jsx2("button", { type: "button", style: S.iconBtn, onClick: () => controller.shuttleStep(1), title: "Shuttle forward (L)", "aria-label": "Shuttle forward", children: /* @__PURE__ */ jsx2(ShuttleRightIcon, {}) }),
959
- /* @__PURE__ */ jsx2("button", { type: "button", style: S.iconBtn, onClick: () => controller.step(1), title: "Step forward", "aria-label": "Step forward", children: /* @__PURE__ */ jsx2(StepForwardIcon, {}) }),
960
- /* @__PURE__ */ jsx2("button", { type: "button", style: S.iconBtn, onClick: () => controller.goToEnd(), title: "Go to end", "aria-label": "Go to end", children: /* @__PURE__ */ jsx2(EndIcon, {}) }),
961
- controller.showsControl("record") && /* @__PURE__ */ jsx2(
962
- "button",
946
+ IconButton,
963
947
  {
964
- type: "button",
965
- style: { ...S.iconBtn, color: state.recording ? "var(--xeno-danger)" : void 0, opacity: state.recording ? 1 : 0.7 },
966
- onClick: () => controller.record(),
967
- title: "Record",
968
- "aria-label": "Record",
969
- "aria-pressed": !!state.recording,
970
- children: /* @__PURE__ */ jsx2(RecordIcon, {})
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()
971
954
  }
972
955
  ),
973
- controller.showsControl("loop") && /* @__PURE__ */ jsx2(
974
- "button",
975
- {
976
- type: "button",
977
- style: { ...S.iconBtn, opacity: state.loopEnabled ? 1 : 0.4 },
978
- onClick: () => controller.toggleLoop(),
979
- title: "Loop",
980
- "aria-label": "Loop",
981
- "aria-pressed": !!state.loopEnabled,
982
- children: /* @__PURE__ */ jsx2(LoopIcon, {})
983
- }
984
- ),
985
- controller.showsControl("metronome") && /* @__PURE__ */ jsx2(
986
- "button",
987
- {
988
- type: "button",
989
- style: { ...S.iconBtn, opacity: state.metronome ? 1 : 0.4 },
990
- onClick: () => controller.toggleMetronome(),
991
- title: "Metronome",
992
- "aria-label": "Metronome",
993
- "aria-pressed": !!state.metronome,
994
- children: /* @__PURE__ */ jsx2(MetronomeIcon, {})
995
- }
996
- ),
997
- controller.showsControl("punch") && /* @__PURE__ */ jsx2("button", { type: "button", style: { ...S.chipBtn, opacity: state.punch ? 1 : 0.4 }, "aria-pressed": !!state.punch, title: "Punch", children: "PUNCH" }),
998
- /* @__PURE__ */ jsxs2(
999
- "button",
956
+ /* @__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) }),
958
+ /* @__PURE__ */ jsx2(IconButton, { size: "sm", icon: /* @__PURE__ */ jsx2(StepForwardIcon, {}), label: "Step forward", onClick: () => controller.step(1) }),
959
+ /* @__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,
1000
966
  {
1001
- type: "button",
1002
- style: S.readout,
1003
- onClick: () => controller.cycleDisplay(),
967
+ value: controller.getReadout(),
968
+ unit: (0, format_exports.displayLabel)(controller.getDisplay()),
969
+ label: `Position ${controller.getReadout()}`,
1004
970
  title: "Click to change time display",
1005
- "aria-label": `Position ${controller.getReadout()}`,
1006
- children: [
1007
- /* @__PURE__ */ jsx2("span", { style: S.readoutValue, children: controller.getReadout() }),
1008
- /* @__PURE__ */ jsx2("span", { style: S.readoutUnit, children: (0, format_exports.displayLabel)(controller.getDisplay()) })
1009
- ]
971
+ onClick: () => controller.cycleDisplay()
1010
972
  }
1011
973
  ),
1012
- /* @__PURE__ */ jsx2("span", { style: S.duration, children: controller.getDurationReadout() }),
1013
- controller.showsControl("bpm") && /* @__PURE__ */ jsxs2("label", { style: S.bpm, children: [
1014
- /* @__PURE__ */ jsx2("span", { style: S.bpmLabel, children: "BPM" }),
1015
- /* @__PURE__ */ jsx2(
1016
- "input",
1017
- {
1018
- type: "number",
1019
- value: state.bpm ?? 120,
1020
- onChange: (e) => controller.setTempo(Number(e.target.value)),
1021
- style: S.bpmInput,
1022
- "aria-label": "Tempo"
1023
- }
1024
- )
1025
- ] }),
1026
- controller.showsControl("inOut") && /* @__PURE__ */ jsxs2("span", { style: S.inOut, children: [
1027
- /* @__PURE__ */ jsx2("button", { type: "button", style: S.chipBtn, onClick: () => controller.setInPoint(), title: "Set in point", children: "IN" }),
1028
- /* @__PURE__ */ jsx2("button", { type: "button", style: S.chipBtn, onClick: () => controller.setOutPoint(), title: "Set out point", children: "OUT" })
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: [
977
+ /* @__PURE__ */ jsx2(TextButton, { onClick: () => controller.setInPoint(), children: "IN" }),
978
+ /* @__PURE__ */ jsx2(TextButton, { onClick: () => controller.setOutPoint(), children: "OUT" })
1029
979
  ] })
1030
980
  ] }),
1031
- controller.showsControl("scrubber") && /* @__PURE__ */ jsxs2(
1032
- "div",
981
+ controller.showsControl("scrubber") && /* @__PURE__ */ jsx2(
982
+ Slider,
1033
983
  {
1034
- ref: scrubRef,
1035
- style: S.scrubTrack,
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(),
1036
991
  onPointerDown: onScrubDown,
1037
992
  onPointerMove: onScrubMove,
1038
993
  onPointerUp: onScrubUp,
1039
- onPointerCancel: () => controller.cancelScrub(),
1040
- role: "slider",
1041
- "aria-label": "Playhead",
1042
- "aria-valuemin": 0,
1043
- "aria-valuemax": controller.getDuration(),
1044
- "aria-valuenow": controller.getCurrentTick(),
1045
- "aria-valuetext": controller.getReadout(),
1046
- tabIndex: 0,
1047
- children: [
1048
- /* @__PURE__ */ jsx2("span", { style: { ...S.scrubFill, width: `${controller.getProgress() * 100}%` } }),
1049
- /* @__PURE__ */ jsx2("span", { style: { ...S.scrubKnob, left: `${controller.getProgress() * 100}%` } })
1050
- ]
994
+ onPointerCancel: () => controller.cancelScrub()
1051
995
  }
1052
996
  )
1053
997
  ]
1054
998
  }
1055
999
  );
1056
1000
  }
1057
- var S = {
1058
- root: {
1059
- display: "flex",
1060
- flexDirection: "column",
1061
- gap: 3,
1062
- padding: "4px 6px",
1063
- fontSize: 11,
1064
- color: "var(--xeno-text)",
1065
- background: "var(--xeno-canvas)",
1066
- userSelect: "none",
1067
- outline: "none"
1068
- },
1069
- row: { display: "flex", alignItems: "center", gap: 3, flexWrap: "wrap" },
1070
- iconBtn: {
1071
- display: "flex",
1072
- alignItems: "center",
1073
- justifyContent: "center",
1074
- width: 20,
1075
- height: 18,
1076
- padding: 0,
1077
- border: "none",
1078
- borderRadius: 2,
1079
- background: "var(--xeno-border-subtle)",
1080
- color: "inherit",
1081
- cursor: "pointer",
1082
- flex: "0 0 auto"
1083
- },
1084
- primaryBtn: { background: "var(--xeno-control)", width: 26 },
1085
- chipBtn: {
1086
- height: 18,
1087
- padding: "0 5px",
1088
- border: "none",
1089
- borderRadius: 2,
1090
- background: "var(--xeno-border-subtle)",
1091
- color: "inherit",
1092
- cursor: "pointer",
1093
- fontSize: 9,
1094
- letterSpacing: "0.05em",
1095
- flex: "0 0 auto"
1096
- },
1097
- readout: {
1098
- display: "flex",
1099
- alignItems: "baseline",
1100
- gap: 4,
1101
- height: 18,
1102
- padding: "0 6px",
1103
- marginLeft: 4,
1104
- border: "1px solid var(--xeno-border)",
1105
- borderRadius: 2,
1106
- background: "var(--xeno-canvas)",
1107
- color: "inherit",
1108
- cursor: "pointer",
1109
- flex: "0 0 auto"
1110
- },
1111
- readoutValue: { fontVariantNumeric: "tabular-nums", fontSize: 12, letterSpacing: "0.02em" },
1112
- readoutUnit: { fontSize: 8, color: "var(--xeno-muted)", letterSpacing: "0.06em" },
1113
- duration: {
1114
- fontVariantNumeric: "tabular-nums",
1115
- fontSize: 10,
1116
- color: "var(--xeno-muted)",
1117
- flex: "0 0 auto"
1118
- },
1119
- bpm: { display: "flex", alignItems: "center", gap: 3, flex: "0 0 auto" },
1120
- bpmLabel: { fontSize: 8, color: "var(--xeno-muted)", letterSpacing: "0.06em" },
1121
- bpmInput: {
1122
- width: 42,
1123
- height: 18,
1124
- background: "var(--xeno-canvas)",
1125
- color: "inherit",
1126
- border: "1px solid var(--xeno-border)",
1127
- borderRadius: 2,
1128
- fontSize: 11,
1129
- padding: "0 4px",
1130
- outline: "none",
1131
- fontVariantNumeric: "tabular-nums"
1132
- },
1133
- inOut: { display: "flex", gap: 2, flex: "0 0 auto" },
1134
- scrubTrack: {
1135
- position: "relative",
1136
- height: 8,
1137
- borderRadius: 2,
1138
- background: "var(--xeno-border-subtle)",
1139
- cursor: "pointer",
1140
- touchAction: "none",
1141
- outline: "none"
1142
- },
1143
- scrubFill: {
1144
- position: "absolute",
1145
- left: 0,
1146
- top: 0,
1147
- bottom: 0,
1148
- background: "var(--xeno-control)",
1149
- borderRadius: 2,
1150
- pointerEvents: "none"
1151
- },
1152
- scrubKnob: {
1153
- position: "absolute",
1154
- top: -1,
1155
- width: 3,
1156
- height: 10,
1157
- marginLeft: -1,
1158
- background: "var(--xeno-text)",
1159
- borderRadius: 1,
1160
- pointerEvents: "none"
1161
- }
1162
- };
1163
1001
 
1164
1002
  // src/time/transport/panel.ts
1165
1003
  function createTransportPanel(options = {}) {
@@ -4177,13 +4015,13 @@ import {
4177
4015
  Badge,
4178
4016
  EmptyState as EmptyState2,
4179
4017
  ErrorState,
4180
- IconButton,
4018
+ IconButton as IconButton2,
4181
4019
  Row,
4182
4020
  RowList,
4183
4021
  ScrollArea,
4184
4022
  SegmentedControl,
4185
4023
  StatusBar as StatusBar2,
4186
- TextButton,
4024
+ TextButton as TextButton2,
4187
4025
  Toolbar as Toolbar2,
4188
4026
  ToolbarGroup as ToolbarGroup2
4189
4027
  } from "@xenosystem/components/chrome";
@@ -4222,7 +4060,7 @@ function CalendarPanelView({
4222
4060
  {
4223
4061
  left: /* @__PURE__ */ jsxs4(Fragment, { children: [
4224
4062
  /* @__PURE__ */ jsx5(
4225
- IconButton,
4063
+ IconButton2,
4226
4064
  {
4227
4065
  icon: /* @__PURE__ */ jsx5(ChevronGlyph, { direction: "left" }),
4228
4066
  label: "Previous",
@@ -4230,14 +4068,14 @@ function CalendarPanelView({
4230
4068
  }
4231
4069
  ),
4232
4070
  /* @__PURE__ */ jsx5(
4233
- IconButton,
4071
+ IconButton2,
4234
4072
  {
4235
4073
  icon: /* @__PURE__ */ jsx5(ChevronGlyph, { direction: "right" }),
4236
4074
  label: "Next",
4237
4075
  onClick: () => controller.navigate(1)
4238
4076
  }
4239
4077
  ),
4240
- /* @__PURE__ */ jsx5(TextButton, { onClick: () => controller.goToToday(), children: "Today" }),
4078
+ /* @__PURE__ */ jsx5(TextButton2, { onClick: () => controller.goToToday(), children: "Today" }),
4241
4079
  /* @__PURE__ */ jsx5("span", { style: { fontSize: 11, opacity: 0.8 }, children: controller.anchorKey().slice(0, 7) })
4242
4080
  ] }),
4243
4081
  right: /* @__PURE__ */ jsxs4(ToolbarGroup2, { end: true, children: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xenosystem/blocks",
3
- "version": "0.10.1",
3
+ "version": "0.11.1",
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",
@@ -60,7 +60,7 @@
60
60
  "peerDependencies": {
61
61
  "@duckdb/duckdb-wasm": ">=1.28.0",
62
62
  "@xenosystem/block-sdk": "^0.3.0",
63
- "@xenosystem/components": ">=0.9.3 <1",
63
+ "@xenosystem/components": ">=0.10.2 <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",
@@ -76,7 +76,7 @@
76
76
  "@types/react": "^19.0.0",
77
77
  "@types/react-dom": "^19.0.0",
78
78
  "@xenosystem/block-sdk": "^0.3.1",
79
- "@xenosystem/components": "^0.9.3",
79
+ "@xenosystem/components": "^0.10.2",
80
80
  "@xenosystem/data-core": "^0.1.0",
81
81
  "@xenosystem/elements-react": "0.0.1",
82
82
  "@xenosystem/time-core": "^0.1.0",