@crievo/omni 0.1.2 → 0.1.4

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.
Files changed (122) hide show
  1. package/docs/onboarding.md +69 -161
  2. package/omnivideo.config.example.json +1 -1
  3. package/package.json +2 -1
  4. package/reference/learnings.md +12 -0
  5. package/reference/omnichat.md +5 -4
  6. package/reference/publish.md +9 -9
  7. package/reference/studio.md +1 -1
  8. package/studio/api/app.ts +2 -2
  9. package/studio/api/routes/sound.ts +22 -3
  10. package/studio/api/services/onboarding.ts +242 -21
  11. package/studio/api/services/settings.ts +4 -2
  12. package/studio/api/services/sound.ts +45 -0
  13. package/studio/shared/types/api.ts +26 -1
  14. package/studio/studio.ts +1 -0
  15. package/studio/web/components/AppShell.tsx +3 -1
  16. package/studio/web/components/CloneVoiceDialog/RecordStep.tsx +3 -0
  17. package/studio/web/components/CloneVoiceDialog/RecorderBar.tsx +37 -20
  18. package/studio/web/components/CloneVoiceDialog/useCloneVoice.ts +39 -12
  19. package/studio/web/components/MissingDmsPanel/BatchSend.tsx +13 -4
  20. package/studio/web/components/Omni/OmniWidget.tsx +14 -5
  21. package/studio/web/components/Omni/PendingActions.tsx +25 -6
  22. package/studio/web/components/OmnichatRulePanel/RuleConfig.tsx +13 -3
  23. package/studio/web/components/OmnichatRulePanel/RuleEnableConfirm.tsx +14 -4
  24. package/studio/web/components/ResultMessage.tsx +12 -3
  25. package/studio/web/components/Sidebar.tsx +63 -24
  26. package/studio/web/components/StageStepper.tsx +105 -34
  27. package/studio/web/components/Toaster.tsx +25 -0
  28. package/studio/web/components/VoiceCarousel.tsx +19 -16
  29. package/studio/web/components/VoiceProfilesSettings.tsx +8 -3
  30. package/studio/web/components/bencho/ActionNode.tsx +456 -0
  31. package/studio/web/components/bencho/AssigneePicker.tsx +246 -0
  32. package/studio/web/components/bencho/BenchoScope.tsx +8 -0
  33. package/studio/web/components/bencho/Checklist.tsx +421 -0
  34. package/studio/web/components/bencho/CreateMenu.tsx +258 -0
  35. package/studio/web/components/bencho/GenerateImage.tsx +290 -0
  36. package/studio/web/components/bencho/ImageAccordion.tsx +273 -0
  37. package/studio/web/components/bencho/LiquidToggle.tsx +289 -0
  38. package/studio/web/components/bencho/MagnifyDock.tsx +200 -0
  39. package/studio/web/components/bencho/NowPlaying.tsx +403 -0
  40. package/studio/web/components/bencho/OneTimeCode.tsx +374 -0
  41. package/studio/web/components/bencho/PullToRefresh.tsx +527 -0
  42. package/studio/web/components/bencho/SlideToConfirm.tsx +385 -0
  43. package/studio/web/components/bencho/SloshSlider.tsx +394 -0
  44. package/studio/web/components/bencho/StackedBars.tsx +342 -0
  45. package/studio/web/components/bencho/ToastStack.tsx +263 -0
  46. package/studio/web/components/bencho/UploadDropzone.tsx +394 -0
  47. package/studio/web/components/bencho/VoiceNote.tsx +705 -0
  48. package/studio/web/components/icons.tsx +27 -0
  49. package/studio/web/components/ui/slider.tsx +34 -1
  50. package/studio/web/components/ui/switch.tsx +18 -5
  51. package/studio/web/hooks/useOnboarding.ts +24 -6
  52. package/studio/web/index.html +1 -1
  53. package/studio/web/lib/alerts.ts +10 -0
  54. package/studio/web/lib/omni-panel.ts +4 -1
  55. package/studio/web/lib/toast.ts +196 -0
  56. package/studio/web/lib/upload.ts +24 -0
  57. package/studio/web/pages/BenchoPage/index.tsx +74 -0
  58. package/studio/web/pages/BenchoPage/registry.ts +174 -0
  59. package/studio/web/pages/GettingStartedPage/components/ComputerCard.tsx +102 -0
  60. package/studio/web/pages/GettingStartedPage/index.tsx +84 -58
  61. package/studio/web/pages/NewVideoPage/index.tsx +6 -2
  62. package/studio/web/pages/ReferencesPage/index.tsx +4 -1
  63. package/studio/web/pages/SettingsPage/components/AgentAccounts.tsx +30 -3
  64. package/studio/web/pages/VideoPage/components/ChangedVoices.tsx +14 -17
  65. package/studio/web/pages/VideoPage/components/LineTextEditor.tsx +1 -1
  66. package/studio/web/pages/VideoPage/components/PublishForm.tsx +24 -17
  67. package/studio/web/pages/VideoPage/components/ScheduledInfo.tsx +2 -1
  68. package/studio/web/pages/VideoPage/components/SoundTimeline/AddSound.tsx +12 -14
  69. package/studio/web/pages/VideoPage/components/SoundTimeline/BedRow.tsx +27 -11
  70. package/studio/web/pages/VideoPage/components/SoundTimeline/CaptionSaveBar.tsx +22 -18
  71. package/studio/web/pages/VideoPage/components/SoundTimeline/TimelineFooter.tsx +3 -4
  72. package/studio/web/pages/VideoPage/components/SoundTimeline/UploadTrack.tsx +59 -0
  73. package/studio/web/pages/VideoPage/components/SoundTimeline/useSoundTimeline.ts +1 -0
  74. package/studio/web/pages/VideoPage/components/VoicePauseBanner.tsx +4 -6
  75. package/studio/web/pages/VideoPage/index.tsx +4 -0
  76. package/studio/web/pages/VideosPage/components/VideoRow.tsx +2 -4
  77. package/studio/web/router.tsx +8 -0
  78. package/studio/web/styles/components/bencho/action-node.css +227 -0
  79. package/studio/web/styles/components/bencho/checklist.css +182 -0
  80. package/studio/web/styles/components/bencho/dock.css +188 -0
  81. package/studio/web/styles/components/bencho/gallery.css +109 -0
  82. package/studio/web/styles/components/bencho/generate.css +151 -0
  83. package/studio/web/styles/components/bencho/image-accordion.css +37 -0
  84. package/studio/web/styles/components/bencho/liq-create.css +279 -0
  85. package/studio/web/styles/components/bencho/liq-toggle.css +106 -0
  86. package/studio/web/styles/components/bencho/one-time-code.css +148 -0
  87. package/studio/web/styles/components/bencho/picker.css +204 -0
  88. package/studio/web/styles/components/bencho/pull.css +313 -0
  89. package/studio/web/styles/components/bencho/scope.css +42 -0
  90. package/studio/web/styles/components/bencho/slide-confirm.css +139 -0
  91. package/studio/web/styles/components/bencho/slosh.css +137 -0
  92. package/studio/web/styles/components/bencho/sound.css +260 -0
  93. package/studio/web/styles/components/bencho/stacked-bars.css +334 -0
  94. package/studio/web/styles/components/bencho/toast-stack.css +192 -0
  95. package/studio/web/styles/components/bencho/tokens.css +243 -0
  96. package/studio/web/styles/components/bencho/upload-dropzone.css +352 -0
  97. package/studio/web/styles/components/bencho/voice-note.css +337 -0
  98. package/studio/web/styles/components/clone-voice.css +12 -2
  99. package/studio/web/styles/components/omni.css +6 -1
  100. package/studio/web/styles/components/stepper.css +123 -7
  101. package/studio/web/styles/components/toaster.css +10 -0
  102. package/studio/web/styles/components/ui/slider.css +6 -16
  103. package/studio/web/styles/components/ui/switch.css +2 -41
  104. package/studio/web/styles/components/voice-carousel.css +46 -4
  105. package/studio/web/styles/index.css +23 -0
  106. package/studio/web/styles/pages/getting-started.css +46 -0
  107. package/studio/web/styles/pages/settings/shell.css +4 -0
  108. package/studio/web/styles/pages/video/layout.css +1 -4
  109. package/studio/web/styles/pages/video/sound-timeline.css +8 -0
  110. package/studio/web/styles/theme/identity/shell.css +10 -26
  111. package/tools/lib/agents.mjs +24 -13
  112. package/tools/lib/claude-settings.mjs +32 -3
  113. package/tools/lib/deps.mjs +24 -0
  114. package/tools/lib/platforms/instagram.mjs +1 -1
  115. package/tools/lib/up.mjs +5 -2
  116. package/tools/omni.mjs +7 -5
  117. package/tools/omnichat.mjs +5 -7
  118. package/tools/package.mjs +5 -3
  119. package/tools/publish.mjs +40 -137
  120. package/tools/reference.mjs +1 -1
  121. package/tools/setup.mjs +16 -15
  122. package/tools/sound.mjs +67 -3
@@ -96,6 +96,19 @@ import {
96
96
  CarouselHorizontalIcon,
97
97
  ArrowLeft02Icon,
98
98
  ArrowRight02Icon,
99
+ Folder01Icon,
100
+ Home01Icon,
101
+ Search01Icon,
102
+ UserIcon,
103
+ File01Icon,
104
+ Grid2x2Icon,
105
+ CheckmarkCircle02Icon,
106
+ CancelCircleIcon,
107
+ Alert02Icon,
108
+ PreviousIcon,
109
+ NextIcon,
110
+ Settings02Icon,
111
+ GitCommitHorizontalIcon,
99
112
  } from "@hugeicons/core-free-icons";
100
113
 
101
114
  type Props = Omit<SVGProps<SVGSVGElement>, "ref" | "strokeWidth" | "size"> & {
@@ -225,6 +238,20 @@ export const TrendUp = icon(ChartIncreaseIcon, "TrendUp");
225
238
  export const TrendDown = icon(ChartDecreaseIcon, "TrendDown");
226
239
  export const Columns = icon(ColumnInsertIcon, "Columns");
227
240
 
241
+ export const Folder = icon(Folder01Icon, "Folder");
242
+ export const House = icon(Home01Icon, "House");
243
+ export const MagnifyingGlass = icon(Search01Icon, "MagnifyingGlass");
244
+ export const User = icon(UserIcon, "User");
245
+ export const FileText = icon(File01Icon, "FileText");
246
+ export const GridFour = icon(Grid2x2Icon, "GridFour");
247
+ export const CheckCircle = icon(CheckmarkCircle02Icon, "CheckCircle");
248
+ export const XCircle = icon(CancelCircleIcon, "XCircle");
249
+ export const Warning = icon(Alert02Icon, "Warning");
250
+ export const SkipBack = icon(PreviousIcon, "SkipBack");
251
+ export const SkipForward = icon(NextIcon, "SkipForward");
252
+ export const Gear2 = icon(Settings02Icon, "Gear2");
253
+ export const GitCommit = icon(GitCommitHorizontalIcon, "GitCommit");
254
+
228
255
  /* the state of a queue run (ok, error, interrupted, waiting) */
229
256
  export const STATE_ICON: Record<string, ReactNode> = {
230
257
  ok: <Check weight="bold" />,
@@ -4,12 +4,15 @@
4
4
  The look comes from CSS (.tdc-slider in styles/components/ui/slider.css), keyed on `data-variant`:
5
5
  - `line`: the thin rule;
6
6
  - `fill`: the tall track where the fill is the value and the text (`label`) lives inside.
7
- It is channels' shape and that of every volume, gain and position control here.
7
+ It is channels' shape and that of every volume, gain and position control here. It is drawn by the Bencho
8
+ SloshSlider (liquid fill, rigid handle) behind the same props; the Radix root only renders the `line` variant.
8
9
  `markValue` draws a dashed line at a reference value (0 dB, 100 %), and
9
10
  `snapToMark` snaps the drag to it when close. */
10
11
 
11
12
  import { forwardRef, type ComponentPropsWithoutRef, type ElementRef, type ReactNode } from "react";
12
13
  import * as SliderPrimitive from "@radix-ui/react-slider";
14
+ import { BenchoScope } from "../bencho/BenchoScope";
15
+ import { SloshSlider } from "../bencho/SloshSlider";
13
16
 
14
17
  type SliderVariant = "line" | "fill";
15
18
 
@@ -25,6 +28,36 @@ interface SliderProps extends ComponentPropsWithoutRef<typeof SliderPrimitive.Ro
25
28
 
26
29
  export const Slider = forwardRef<ElementRef<typeof SliderPrimitive.Root>, SliderProps>(
27
30
  ({ className, variant = "line", label, markValue, snapToMark, min = 0, max = 100, onValueChange, ...props }, ref) => {
31
+ if (variant === "fill") {
32
+ /* One handle: every caller passes a one-entry array. The Radix props the liquid has no use for (orientation,
33
+ dir, inverted, minStepsBetweenThumbs, asChild) and the ref were never set on a fill slider. */
34
+ return (
35
+ <BenchoScope>
36
+ <SloshSlider
37
+ fluid
38
+ className={className ? `tdc-slider ${className}` : "tdc-slider"}
39
+ data-slot="slider"
40
+ data-variant="fill"
41
+ id={props.id}
42
+ value={props.value}
43
+ defaultValue={props.defaultValue}
44
+ min={min}
45
+ max={max}
46
+ step={props.step ?? 1}
47
+ disabled={props.disabled}
48
+ label={label}
49
+ markValue={markValue}
50
+ snapToMark={snapToMark}
51
+ onValueChange={onValueChange}
52
+ onValueCommit={props.onValueCommit}
53
+ ariaLabel={props["aria-label"] ?? ""}
54
+ ariaLabelledBy={props["aria-labelledby"]}
55
+ ariaDescribedBy={props["aria-describedby"]}
56
+ />
57
+ </BenchoScope>
58
+ );
59
+ }
60
+
28
61
  const markPercent = markValue !== undefined ? ((markValue - min) / (max - min)) * 100 : null;
29
62
 
30
63
  const handleValueChange = (values: number[]) => {
@@ -1,13 +1,26 @@
1
1
  import { forwardRef, type ComponentPropsWithoutRef, type ElementRef } from "react";
2
- import * as SwitchPrimitive from "@radix-ui/react-switch";
2
+ import type * as SwitchPrimitive from "@radix-ui/react-switch";
3
+ import { BenchoScope } from "../bencho/BenchoScope";
4
+ import { LiquidSwitch } from "../bencho/LiquidToggle";
3
5
  import { cx } from "./cx";
4
6
 
7
+ /* The footprint the studio's rows were laid out for: 20px tall, which is 20/46 of the liquid toggle's own 92x46. */
8
+ const SCALE = 20 / 46;
9
+
10
+ /* Same props as the Radix switch it replaced (callers did not change); only what it draws is Bencho's liquid toggle. */
5
11
  export const Switch = forwardRef<
6
12
  ElementRef<typeof SwitchPrimitive.Root>,
7
13
  ComponentPropsWithoutRef<typeof SwitchPrimitive.Root>
8
- >(({ className, ...props }, ref) => (
9
- <SwitchPrimitive.Root ref={ref} data-slot="switch" className={cx("ui-switch", className)} {...props}>
10
- <SwitchPrimitive.Thumb className="ui-switch-thumb" />
11
- </SwitchPrimitive.Root>
14
+ >(({ className, asChild: _asChild, children: _children, value, ...props }, ref) => (
15
+ <BenchoScope>
16
+ <LiquidSwitch
17
+ ref={ref}
18
+ data-slot="switch"
19
+ className={cx("ui-switch", className)}
20
+ scale={SCALE}
21
+ value={value === undefined ? undefined : String(value)}
22
+ {...props}
23
+ />
24
+ </BenchoScope>
12
25
  ));
13
26
  Switch.displayName = "Switch";
@@ -1,15 +1,27 @@
1
- /* Getting started (/api/onboarding): the docs/onboarding.md guide in steps, each with the
2
- tools/setup.mjs check. `reload(true)` asks for a fresh check (?fresh=1). */
1
+ /* Getting started (/api/onboarding): the docs/onboarding.md guide in steps, each with its state, and
2
+ this computer (what the CLI is still downloading, the skills' permission). While the computer isn't
3
+ ready the page asks again every 5 s, so "preparando…" turns into "Computador pronto" on its own.
4
+ `reload(true)` asks for a fresh check (?fresh=1). */
3
5
 
4
6
  import { useCallback, useState } from "react";
7
+ import { useQuery } from "@tanstack/react-query";
5
8
  import { api, errorText, read } from "../lib/api";
6
9
  import { queryClient, queryKeys } from "../lib/query-client";
7
10
  import type { Onboarding } from "../types/api";
8
- import { useLiveQuery } from "./useLiveQuery";
11
+
12
+ const POLL_MS = 5_000;
13
+
14
+ /** the computer still has something to answer for (a program downloading, the skills' rules) */
15
+ export const computerPending = (o: Onboarding | null | undefined) =>
16
+ !!o?.computer && (!o.computer.ready || (!o.computer.dev && o.computer.missing_rules.length > 0));
9
17
 
10
18
  export function useOnboarding() {
11
- const { data, error, isFetching } = useLiveQuery(queryKeys.onboarding, () => api.onboarding.$get({ query: {} }));
12
- const onboarding: Onboarding | null = data;
19
+ const q = useQuery({
20
+ queryKey: queryKeys.onboarding,
21
+ queryFn: (): Promise<Onboarding> => read(api.onboarding.$get({ query: {} })),
22
+ refetchInterval: query => (computerPending(query.state.data) ? POLL_MS : false),
23
+ });
24
+ const onboarding: Onboarding | null = q.data ?? null;
13
25
  const [freshError, setFreshError] = useState<string | null>(null);
14
26
  const [checking, setChecking] = useState(false);
15
27
  const reload = useCallback(async (fresh = false) => {
@@ -23,5 +35,11 @@ export function useOnboarding() {
23
35
  }
24
36
  setChecking(false);
25
37
  }, []);
26
- return { onboarding, error: freshError ?? error, checking: checking || isFetching, reload };
38
+ return {
39
+ onboarding,
40
+ error: freshError ?? (q.error ? errorText(q.error) : null),
41
+ /* the 5 s poll doesn't count as "Conferindo…" */
42
+ checking,
43
+ reload,
44
+ };
27
45
  }
@@ -9,7 +9,7 @@
9
9
  <link rel="icon" type="image/png" media="(prefers-color-scheme: dark)" href="data:image/png;base64,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">
10
10
  <link rel="preconnect" href="https://fonts.googleapis.com">
11
11
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
12
- <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Schibsted+Grotesk:wght@400;500;600;700&family=Geist+Mono:wght@400;500&display=swap">
12
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Schibsted+Grotesk:wght@400;500;600;700&family=Geist+Mono:wght@400;500&family=Inter:wght@300..700&display=swap">
13
13
  <link rel="stylesheet" href="./styles/index.css">
14
14
  </head>
15
15
  <body>
@@ -119,6 +119,16 @@ function agentAlerts(agents: AgentsSettings, now: number): Alert[] {
119
119
  if (on.some(a => a.login && !limited(a))) return [];
120
120
  const until = on.filter(a => a.login && limited(a)).map(a => a.limited_until!);
121
121
  if (!until.length) {
122
+ /* the default account (~/.claude) without a login: the Claude Code may live in another folder */
123
+ if (on.length === 1 && on[0].implicit && on[0].provider === "claude")
124
+ return [
125
+ {
126
+ key: "agents:no-claude",
127
+ label: "Não achei o login do Claude Code",
128
+ message: `Em ${on[0].dir}: entre, ou coloque a pasta dele em Configurações`,
129
+ link,
130
+ },
131
+ ];
122
132
  return [
123
133
  {
124
134
  key: "agents:no-login",
@@ -13,11 +13,13 @@ export type OmniPanelState = {
13
13
  available: boolean;
14
14
  face: string | null;
15
15
  open: boolean;
16
+ /* a call is on (even minimized): the sidebar card shows the expand icon only then */
17
+ active: boolean;
16
18
  ring: OmniStatus;
17
19
  glow: OmniGlowState;
18
20
  };
19
21
 
20
- let state: OmniPanelState = { available: false, face: null, open: false, ring: "idle", glow: "off" };
22
+ let state: OmniPanelState = { available: false, face: null, open: false, active: false, ring: "idle", glow: "off" };
21
23
  let toggler: (() => void) | null = null;
22
24
  let leveler: (() => number) | null = null;
23
25
  const listeners = new Set<() => void>();
@@ -27,6 +29,7 @@ export function publishOmniPanel(next: OmniPanelState) {
27
29
  next.available === state.available &&
28
30
  next.face === state.face &&
29
31
  next.open === state.open &&
32
+ next.active === state.active &&
30
33
  next.ring === state.ring &&
31
34
  next.glow === state.glow
32
35
  )
@@ -0,0 +1,196 @@
1
+ /* The studio's toasts: a short result that shows up in a corner and goes away on its own, instead of an inline
2
+ `.response` line that stays until the next save. `toast.success("Salvo")` works anywhere (a hook, a callback, a
3
+ module), because the list lives here and not in React; components/Toaster.tsx, mounted once in the app shell, draws it.
4
+
5
+ The list is a queue: the newest toast is in front, three are shown, the older ones wait behind and come up as the
6
+ front ones go. A toast's timer only runs while it is one of the three shown, and every timer stops while the stack
7
+ is open (the reader is looking at it), then picks up with the time each one had left. */
8
+
9
+ import { useSyncExternalStore } from "react";
10
+ import type { ActionResult } from "./api";
11
+
12
+ export type ToastKind = "success" | "info" | "error" | "warning";
13
+ export type ToastAction = { label: string; onClick: () => void };
14
+ export type ToastOptions = {
15
+ body?: string;
16
+ /* a button on the toast; clicking it runs `onClick` and dismisses the toast */
17
+ action?: ToastAction;
18
+ /* ms on screen; Infinity stays until dismissed. The default depends on the kind: an error is read slower */
19
+ duration?: number;
20
+ /* a toast with the same id is replaced in place (and brought to the front) instead of stacking a copy */
21
+ id?: string;
22
+ };
23
+ export type ToastItem = {
24
+ id: string;
25
+ kind: ToastKind;
26
+ title: string;
27
+ body?: string;
28
+ action?: ToastAction;
29
+ duration: number;
30
+ };
31
+
32
+ /* the stack draws this many; the rest of the queue waits */
33
+ export const TOAST_LIMIT = 3;
34
+
35
+ export const TOAST_DURATION: Record<ToastKind, number> = {
36
+ success: 4000,
37
+ info: 5000,
38
+ warning: 7000,
39
+ error: 9000,
40
+ };
41
+
42
+ /* the clock and the timers, injectable so a test can drive time by hand */
43
+ export type ToastClock = {
44
+ now: () => number;
45
+ setTimer: (fn: () => void, ms: number) => unknown;
46
+ clearTimer: (handle: unknown) => void;
47
+ };
48
+
49
+ const realClock: ToastClock = {
50
+ now: () => Date.now(),
51
+ setTimer: (fn, ms) => setTimeout(fn, ms),
52
+ clearTimer: handle => clearTimeout(handle as ReturnType<typeof setTimeout>),
53
+ };
54
+
55
+ export function createToastStore(clock: ToastClock = realClock) {
56
+ /* newest first */
57
+ let items: ToastItem[] = [];
58
+ let shown: ToastItem[] = [];
59
+ let paused = false;
60
+ let seq = 0;
61
+ const listeners = new Set<() => void>();
62
+ /* ms still to go for each toast, and the timer of the ones counting down right now */
63
+ const remaining = new Map<string, number>();
64
+ const running = new Map<string, { handle: unknown; since: number }>();
65
+
66
+ const stop = (id: string) => {
67
+ const r = running.get(id);
68
+ if (!r) return;
69
+ clock.clearTimer(r.handle);
70
+ running.delete(id);
71
+ remaining.set(id, Math.max(0, (remaining.get(id) ?? 0) - (clock.now() - r.since)));
72
+ };
73
+
74
+ /* after any change: the timers of the toasts not shown (or all of them, while paused) stop; the shown ones start */
75
+ const sync = () => {
76
+ const visible = new Set(items.slice(0, TOAST_LIMIT).map(t => t.id));
77
+ for (const id of [...running.keys()]) if (paused || !visible.has(id)) stop(id);
78
+ if (!paused) {
79
+ for (const id of visible) {
80
+ const left = remaining.get(id) ?? Number.POSITIVE_INFINITY;
81
+ if (running.has(id) || !Number.isFinite(left)) continue;
82
+ running.set(id, { handle: clock.setTimer(() => dismiss(id), left), since: clock.now() });
83
+ }
84
+ }
85
+ shown = items.slice(0, TOAST_LIMIT);
86
+ for (const l of listeners) l();
87
+ };
88
+
89
+ const dismiss = (id: string) => {
90
+ stop(id);
91
+ remaining.delete(id);
92
+ if (!items.some(t => t.id === id)) return;
93
+ items = items.filter(t => t.id !== id);
94
+ sync();
95
+ };
96
+
97
+ const add = (kind: ToastKind, title: string, opts: ToastOptions = {}): string => {
98
+ const id = opts.id ?? `toast-${++seq}`;
99
+ const action = opts.action && {
100
+ label: opts.action.label,
101
+ onClick: () => {
102
+ opts.action?.onClick();
103
+ dismiss(id);
104
+ },
105
+ };
106
+ const duration = opts.duration ?? TOAST_DURATION[kind];
107
+ stop(id);
108
+ items = [{ id, kind, title, body: opts.body, action, duration }, ...items.filter(t => t.id !== id)];
109
+ remaining.set(id, duration);
110
+ sync();
111
+ return id;
112
+ };
113
+
114
+ return {
115
+ success: (title: string, opts?: ToastOptions) => add("success", title, opts),
116
+ info: (title: string, opts?: ToastOptions) => add("info", title, opts),
117
+ error: (title: string, opts?: ToastOptions) => add("error", title, opts),
118
+ warning: (title: string, opts?: ToastOptions) => add("warning", title, opts),
119
+ dismiss,
120
+ clear() {
121
+ for (const id of [...running.keys()]) stop(id);
122
+ items = [];
123
+ remaining.clear();
124
+ sync();
125
+ },
126
+ /* the stack is open (hovered, focused): time stops for every toast */
127
+ pause() {
128
+ if (paused) return;
129
+ paused = true;
130
+ sync();
131
+ },
132
+ resume() {
133
+ if (!paused) return;
134
+ paused = false;
135
+ sync();
136
+ },
137
+ /* the toasts to draw, front first; the same array until something changes */
138
+ getSnapshot: () => shown,
139
+ /* how many are waiting behind the ones shown */
140
+ queued: () => items.length - shown.length,
141
+ subscribe(listener: () => void) {
142
+ listeners.add(listener);
143
+ return () => {
144
+ listeners.delete(listener);
145
+ };
146
+ },
147
+ };
148
+ }
149
+
150
+ /* the studio's one list */
151
+ export const toast = createToastStore();
152
+
153
+ const serverSnapshot: ToastItem[] = [];
154
+ export const useToasts = () => useSyncExternalStore(toast.subscribe, toast.getSnapshot, () => serverSnapshot);
155
+
156
+ /* A result is a one-line confirmation or a short error, or it is a long text the reader has to be able to come back to
157
+ (command output, a multi-line failure). Only the first becomes a toast. */
158
+ const SHORT = 140;
159
+ /* a title is one line of a 300px toast */
160
+ const TITLE_FIT = 34;
161
+
162
+ export const isToastable = (result: NonNullable<ActionResult>) => {
163
+ const text = (result.ok ? result.output : result.error)?.trim() ?? "";
164
+ return text.length <= SHORT && !text.includes("\n");
165
+ };
166
+
167
+ type ResultSink = {
168
+ success: (title: string, opts?: ToastOptions) => unknown;
169
+ error: (title: string, opts?: ToastOptions) => unknown;
170
+ };
171
+
172
+ const announced = new WeakSet<object>();
173
+
174
+ /* Shows the outcome of an action (what `send()` returns) as a toast: an ok as a success, an error as an error. The same
175
+ result object is announced once, however many components render it or how often they remount. Returns false when it
176
+ was not toasted (nothing, already shown, or a long text that stays inline unless `always`). */
177
+ export function toastResult(
178
+ result: ActionResult,
179
+ store: ResultSink = toast,
180
+ { always = false }: { always?: boolean } = {},
181
+ ): boolean {
182
+ if (!result || announced.has(result) || !(always || isToastable(result))) return false;
183
+ announced.add(result);
184
+ const raw = (result.ok ? result.output : result.error)?.trim() ?? "";
185
+ /* `always` is for a caller with no inline fallback: a long text is cut to its first line instead of being lost */
186
+ const first = raw.split("\n")[0];
187
+ const text = always && first.length > SHORT ? `${first.slice(0, SHORT - 1)}…` : always ? first : raw;
188
+ if (result.ok) {
189
+ /* the same text again (save, save, save) refreshes the toast it already has */
190
+ if (text.length <= TITLE_FIT) store.success(text || "Pronto", { id: `result:${text}` });
191
+ else store.success("Pronto", { body: text, id: `result:${text}` });
192
+ } else {
193
+ store.error("Algo deu errado", { body: text.charAt(0).toUpperCase() + text.slice(1), id: `result:${text}` });
194
+ }
195
+ return true;
196
+ }
@@ -22,3 +22,27 @@ export function uploadStyleFile(file: File, progress: (pct: number) => void): Pr
22
22
  x.send(file);
23
23
  });
24
24
  }
25
+
26
+ /* The user's own track for the editor (POST /api/sound/upload): the same raw body, with the video in x-video.
27
+ The answer's output starts with the name the track got in the video's sfx/. */
28
+ export type TrackUploadResult = { ok: boolean; output?: string; error?: string };
29
+
30
+ export function uploadTrackFile(slug: string, file: File, progress: (pct: number) => void): Promise<TrackUploadResult> {
31
+ return new Promise(resolve => {
32
+ const x = new XMLHttpRequest();
33
+ x.open("POST", "/api/sound/upload");
34
+ x.setRequestHeader("content-type", "application/octet-stream");
35
+ x.setRequestHeader("x-file-name", encodeURIComponent(file.name));
36
+ x.setRequestHeader("x-video", encodeURIComponent(slug));
37
+ x.upload.onprogress = e => e.lengthComputable && progress(Math.round((e.loaded / e.total) * 100));
38
+ x.onerror = () => resolve({ ok: false, error: "O envio falhou" });
39
+ x.onload = () => {
40
+ try {
41
+ resolve(JSON.parse(x.responseText));
42
+ } catch {
43
+ resolve({ ok: false, error: `${x.status}: ${x.statusText}` });
44
+ }
45
+ };
46
+ x.send(file);
47
+ });
48
+ }
@@ -0,0 +1,74 @@
1
+ /* /bencho: the components ported from bencho.dev side by side, in their own colors (styles/components/bencho/
2
+ tokens.css), to judge look and motion before deciding what the studio keeps. Not in the menu: reached by the
3
+ address. "Fundo" picks the card color every stage gives its component (data-fill); "Tema" the page's palette. */
4
+
5
+ import { Suspense, useState } from "react";
6
+ import { BENCHO } from "./registry";
7
+
8
+ type Fill = "light" | "dark";
9
+ type Theme = "light" | "dark";
10
+
11
+ function Choice<T extends string>({
12
+ label,
13
+ value,
14
+ options,
15
+ onChange,
16
+ }: {
17
+ label: string;
18
+ value: T;
19
+ options: { id: T; name: string }[];
20
+ onChange: (v: T) => void;
21
+ }) {
22
+ return (
23
+ <div className="bn-choice" role="group" aria-label={label}>
24
+ <span>{label}</span>
25
+ {options.map(o => (
26
+ <button key={o.id} type="button" aria-pressed={value === o.id} onClick={() => onChange(o.id)}>
27
+ {o.name}
28
+ </button>
29
+ ))}
30
+ </div>
31
+ );
32
+ }
33
+
34
+ export function BenchoPage() {
35
+ const [fill, setFill] = useState<Fill>("light");
36
+ const [theme, setTheme] = useState<Theme>("light");
37
+ /* ?only=<id> shows a single card, for looking at one port on its own */
38
+ const only = new URLSearchParams(window.location.search).get("only");
39
+ const shown = only ? BENCHO.filter(b => b.id === only) : BENCHO;
40
+ const choices = [
41
+ { id: "light" as const, name: "Claro" },
42
+ { id: "dark" as const, name: "Escuro" },
43
+ ];
44
+ return (
45
+ <div className="bencho" data-theme={theme}>
46
+ <header className="bn-top">
47
+ <div>
48
+ <h1>Componentes do Bencho</h1>
49
+ <p>{BENCHO.length} peças portadas para o código do studio (Motion e ícones nossos), nas cores originais.</p>
50
+ </div>
51
+ <div className="bn-controls">
52
+ <Choice label="Fundo" value={fill} options={choices} onChange={setFill} />
53
+ <Choice label="Tema" value={theme} options={choices} onChange={setTheme} />
54
+ </div>
55
+ </header>
56
+ <div className="bn-grid">
57
+ {shown.map(({ id, title, description, category, glass, Component }) => (
58
+ <section key={id} className="bn-card" aria-labelledby={`bn-${id}`}>
59
+ <header>
60
+ <h2 id={`bn-${id}`}>{title}</h2>
61
+ <span className="bn-cat">{category}</span>
62
+ </header>
63
+ <p>{description}</p>
64
+ <div className="bn-stage" data-fill={fill} data-surface={glass ? "glass" : undefined}>
65
+ <Suspense fallback={null}>
66
+ <Component />
67
+ </Suspense>
68
+ </div>
69
+ </section>
70
+ ))}
71
+ </div>
72
+ </div>
73
+ );
74
+ }