@scalemule/nextjs 0.1.24 → 0.1.27

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.
package/dist/index.mjs CHANGED
@@ -560,6 +560,7 @@ var ScaleMuleClient = class {
560
560
  this.anonymousId = null;
561
561
  this.sessionPool = /* @__PURE__ */ new Map();
562
562
  this.knownAccounts = /* @__PURE__ */ new Map();
563
+ this.refreshPromise = null;
563
564
  this.apiKey = config.apiKey;
564
565
  this.applicationId = config.applicationId || null;
565
566
  this.baseUrl = config.baseUrl || GATEWAY_URLS[config.environment || "prod"];
@@ -578,6 +579,9 @@ var ScaleMuleClient = class {
578
579
  this.multiSessionEnabled = config.enableMultiSession || false;
579
580
  this.accountSwitcherEnabled = config.enableAccountSwitcher || false;
580
581
  this.accountSwitcherPrivacy = config.accountSwitcherPrivacy || "full";
582
+ this.onRefreshStart = config.onRefreshStart;
583
+ this.onRefreshEnd = config.onRefreshEnd;
584
+ this.onAutoRefreshFailed = config.onAutoRefreshFailed;
581
585
  }
582
586
  // --------------------------------------------------------------------------
583
587
  // Session Management
@@ -863,34 +867,28 @@ var ScaleMuleClient = class {
863
867
  const method = (init.method || "GET").toUpperCase();
864
868
  const timeout = init.timeout || this.defaultTimeout;
865
869
  const maxRetries = init.skipRetry ? 0 : init.retries ?? this.maxRetries;
866
- const headers = {
867
- "x-api-key": this.apiKey,
868
- "User-Agent": `ScaleMule-SDK-TypeScript/${SDK_VERSION}`,
869
- ...init.headers
870
- };
871
- if (!init.skipAuth && this.sessionToken) {
872
- headers["Authorization"] = `Bearer ${this.sessionToken}`;
873
- }
874
- if (this.workspaceId) {
875
- headers["x-sm-workspace-id"] = this.workspaceId;
876
- }
877
- if (!this.sessionToken && this.anonymousId) {
878
- headers["x-anonymous-id"] = this.anonymousId;
879
- }
880
- let bodyStr;
881
- if (init.body !== void 0 && init.body !== null) {
882
- bodyStr = typeof init.body === "string" ? init.body : JSON.stringify(init.body);
883
- if (!headers["Content-Type"]) {
884
- headers["Content-Type"] = "application/json";
885
- }
886
- }
887
- if (this.debug) {
888
- console.log(`[ScaleMule] ${method} ${path}`);
889
- }
890
- const idempotencyKey = method === "POST" ? generateIdempotencyKey() : void 0;
870
+ const bodyStr = init.body ? JSON.stringify(init.body) : void 0;
891
871
  let lastError = null;
872
+ let idempotencyKey = null;
892
873
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
893
- if (attempt > 0 && idempotencyKey) {
874
+ const headers = {
875
+ "x-api-key": this.apiKey,
876
+ "User-Agent": `ScaleMule-SDK-TypeScript/${SDK_VERSION}`,
877
+ ...init.headers
878
+ };
879
+ if (!init.skipAuth && this.sessionToken) {
880
+ headers["Authorization"] = `Bearer ${this.sessionToken}`;
881
+ }
882
+ if (this.workspaceId) {
883
+ headers["x-sm-workspace-id"] = this.workspaceId;
884
+ }
885
+ if (!this.sessionToken && this.anonymousId) {
886
+ headers["x-anonymous-id"] = this.anonymousId;
887
+ }
888
+ if (attempt > 0 && (method === "POST" || method === "PUT" || method === "PATCH")) {
889
+ if (!idempotencyKey) {
890
+ idempotencyKey = generateIdempotencyKey();
891
+ }
894
892
  headers["x-idempotency-key"] = idempotencyKey;
895
893
  }
896
894
  const controller = new AbortController();
@@ -910,6 +908,10 @@ var ScaleMuleClient = class {
910
908
  signal: controller.signal
911
909
  });
912
910
  clearTimeout(timeoutId);
911
+ const rotatedToken = response.headers.get("x-rotated-session-token");
912
+ if (rotatedToken && this.sessionToken && this.userId) {
913
+ this.setSession(rotatedToken, this.userId);
914
+ }
913
915
  if (this.rateLimitQueue) {
914
916
  this.rateLimitQueue.updateFromHeaders(response.headers);
915
917
  }
@@ -932,6 +934,45 @@ var ScaleMuleClient = class {
932
934
  status: response.status,
933
935
  details: responseData?.error?.details || responseData?.details
934
936
  };
937
+ if (response.status === 401 && this.sessionToken && !init.isAutoRefresh) {
938
+ if (this.debug) console.log("[ScaleMule] 401 received, attempting auto-refresh...");
939
+ let isPrimaryRefresh = false;
940
+ if (!this.refreshPromise) {
941
+ isPrimaryRefresh = true;
942
+ this.onRefreshStart?.();
943
+ this.refreshPromise = this.auth?.refreshSession ? this.auth.refreshSession() : this.post("/auth/refresh", {}, { isAutoRefresh: true });
944
+ }
945
+ const currentRefreshPromise = this.refreshPromise;
946
+ try {
947
+ const refreshResult = await currentRefreshPromise;
948
+ if (!refreshResult || refreshResult.error) {
949
+ if (this.debug) console.log("[ScaleMule] Auto-refresh failed:", refreshResult?.error);
950
+ const apiError = refreshResult?.error || {
951
+ code: "refresh_failed",
952
+ message: "Auto-refresh failed",
953
+ status: 400
954
+ };
955
+ init.onAutoRefreshFailed?.(apiError);
956
+ this.onAutoRefreshFailed?.(apiError);
957
+ return { data: null, error };
958
+ }
959
+ if (this.debug) console.log("[ScaleMule] Auto-refresh succeeded, retrying original request...");
960
+ const newToken = refreshResult.data?.session_token || refreshResult.data?.access_token;
961
+ if (newToken) {
962
+ this.sessionToken = newToken;
963
+ if (this.userId) {
964
+ await this.storage.setItem(SESSION_STORAGE_KEY, newToken);
965
+ }
966
+ }
967
+ attempt--;
968
+ continue;
969
+ } finally {
970
+ if (isPrimaryRefresh && this.refreshPromise === currentRefreshPromise) {
971
+ this.refreshPromise = null;
972
+ this.onRefreshEnd?.();
973
+ }
974
+ }
975
+ }
935
976
  if (response.status === 429) {
936
977
  const retryAfter = response.headers.get("Retry-After");
937
978
  if (retryAfter) {
@@ -5715,7 +5756,7 @@ var AudioService = class extends ServiceModule {
5715
5756
  constructor(client, storage) {
5716
5757
  super(client);
5717
5758
  this.storage = storage;
5718
- this.basePath = "/v1/audios";
5759
+ this.basePath = "/v1/audio";
5719
5760
  }
5720
5761
  /**
5721
5762
  * Register a storage-uploaded audio asset with the audio service.
@@ -5772,6 +5813,32 @@ var AudioService = class extends ServiceModule {
5772
5813
  };
5773
5814
  }
5774
5815
  };
5816
+ var TtsService = class extends ServiceModule {
5817
+ constructor() {
5818
+ super(...arguments);
5819
+ this.basePath = "/v1/tts";
5820
+ }
5821
+ async synthesize(params, options) {
5822
+ return this.post(
5823
+ "/synthesize",
5824
+ {
5825
+ text: params.text,
5826
+ voice: params.voice,
5827
+ model: params.model,
5828
+ provider: params.provider,
5829
+ async: params.async,
5830
+ access_mode: params.accessMode
5831
+ },
5832
+ options
5833
+ );
5834
+ }
5835
+ async getJob(id, options) {
5836
+ return this._get(`/jobs/${id}`, options);
5837
+ }
5838
+ async listVoices(params, options) {
5839
+ return this._get(this.withQuery("/voices", { provider: params?.provider }), options);
5840
+ }
5841
+ };
5775
5842
  var DeadLetterApi = class extends ServiceModule {
5776
5843
  constructor() {
5777
5844
  super(...arguments);
@@ -6592,6 +6659,7 @@ var ScaleMule = class {
6592
6659
  this.functions = new FunctionsService(this._client);
6593
6660
  this.photo = new PhotoService(this._client, this.storage);
6594
6661
  this.audio = new AudioService(this._client, this.storage);
6662
+ this.tts = new TtsService(this._client);
6595
6663
  this.flagContent = new FlagContentService(this._client);
6596
6664
  this.creatorMaker = new CreatorMakerService(this._client);
6597
6665
  this.compliance = new ComplianceService(this._client);
@@ -7158,6 +7226,24 @@ var ScaleMuleClient2 = class {
7158
7226
  console.log("[ScaleMule] Session set for user:", userId);
7159
7227
  }
7160
7228
  }
7229
+ /**
7230
+ * Token-only setter for member-auth surfaces.
7231
+ *
7232
+ * Unlike `setSession(token, userId)`, this does NOT persist a userId or
7233
+ * write to local storage — useful when the host platform owns identity
7234
+ * (e.g. ScaleMule's member dashboards use `MemberAuthProvider` and the
7235
+ * token comes from a `${env}_member_access_token` cookie). The token is
7236
+ * applied in-memory and used as a `Bearer` Authorization header on
7237
+ * subsequent requests.
7238
+ *
7239
+ * Pass `null` to clear without touching userId/storage.
7240
+ */
7241
+ setSessionToken(token) {
7242
+ this.sessionToken = token;
7243
+ if (this.debug) {
7244
+ console.log("[ScaleMule] Session token", token ? "set (token-only)" : "cleared (token-only)");
7245
+ }
7246
+ }
7161
7247
  /**
7162
7248
  * Clear session on logout
7163
7249
  */
@@ -7222,6 +7308,8 @@ var ScaleMuleClient2 = class {
7222
7308
  console.log(`[ScaleMule] ${options.method || "GET"} ${path}`);
7223
7309
  }
7224
7310
  let lastError = null;
7311
+ const MAX_REFRESH_ATTEMPTS = 1;
7312
+ let refreshAttempts = 0;
7225
7313
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
7226
7314
  const controller = new AbortController();
7227
7315
  const timeoutId = setTimeout(() => controller.abort(), timeout);
@@ -7245,7 +7333,8 @@ var ScaleMuleClient2 = class {
7245
7333
  if (!response.ok) {
7246
7334
  const rawError = responseData?.error;
7247
7335
  const error = rawError && typeof rawError === "object" ? rawError : { code: `HTTP_${response.status}`, message: typeof rawError === "string" ? rawError : responseData?.message || text || response.statusText };
7248
- if (response.status === 401 && this.sessionToken && !options.isAutoRefresh) {
7336
+ if (response.status === 401 && this.sessionToken && !options.isAutoRefresh && refreshAttempts < MAX_REFRESH_ATTEMPTS) {
7337
+ refreshAttempts++;
7249
7338
  if (this.debug) console.log("[ScaleMule] 401 received, attempting auto-refresh...");
7250
7339
  if (!this.refreshPromise) {
7251
7340
  this.onRefreshStart?.();
@@ -7614,8 +7703,12 @@ function ScaleMuleProvider({
7614
7703
  onLogout,
7615
7704
  onAuthError,
7616
7705
  bootstrapFlags,
7617
- mediaPolicy
7706
+ mediaPolicy,
7707
+ getToken,
7708
+ userResolver,
7709
+ memberTokenPollMs
7618
7710
  }) {
7711
+ const memberMode = typeof getToken === "function";
7619
7712
  const [user, setUser] = useState(null);
7620
7713
  const [initializing, setInitializing] = useState(true);
7621
7714
  const [error, setError] = useState(null);
@@ -7632,9 +7725,13 @@ function ScaleMuleProvider({
7632
7725
  gatewayUrl: resolvedGatewayUrl,
7633
7726
  debug,
7634
7727
  storage,
7635
- pendingSessionInit: !!authProxyUrl
7728
+ // Make outbound API calls wait for the first token to land in any
7729
+ // mode where the provider is responsible for populating the
7730
+ // session asynchronously: auth-proxy fetch, or member-mode
7731
+ // getToken() callback. Resolved in the init effect below.
7732
+ pendingSessionInit: !!authProxyUrl || memberMode
7636
7733
  }),
7637
- [apiKey, applicationId, environment, resolvedGatewayUrl, debug, storage, authProxyUrl]
7734
+ [apiKey, applicationId, environment, resolvedGatewayUrl, debug, storage, authProxyUrl, memberMode]
7638
7735
  );
7639
7736
  const money = useMemo(
7640
7737
  () => createMoneyClient({
@@ -7656,8 +7753,10 @@ function ScaleMuleProvider({
7656
7753
  });
7657
7754
  }, [apiKey, applicationId, environment, resolvedGatewayUrl, debug]);
7658
7755
  const [fetchedPolicy, setFetchedPolicy] = useState(void 0);
7756
+ const [tokenVersion, setTokenVersion] = useState(0);
7659
7757
  useEffect(() => {
7660
7758
  if (mediaPolicy) return;
7759
+ if (memberMode && tokenVersion === 0) return;
7661
7760
  let mounted = true;
7662
7761
  void (async () => {
7663
7762
  try {
@@ -7675,7 +7774,7 @@ function ScaleMuleProvider({
7675
7774
  return () => {
7676
7775
  mounted = false;
7677
7776
  };
7678
- }, [baseClient, mediaPolicy]);
7777
+ }, [baseClient, mediaPolicy, memberMode, tokenVersion]);
7679
7778
  const effectiveMediaPolicy = mediaPolicy ?? fetchedPolicy;
7680
7779
  useEffect(() => {
7681
7780
  const token = client.getSessionToken();
@@ -7693,6 +7792,43 @@ function ScaleMuleProvider({
7693
7792
  try {
7694
7793
  await client.initialize();
7695
7794
  const cachedUser = getCachedUser();
7795
+ if (memberMode) {
7796
+ try {
7797
+ const token = await Promise.resolve(getToken()) ?? null;
7798
+ if (mounted) {
7799
+ if (token) {
7800
+ client.setSessionToken(token);
7801
+ baseClient.setAccessToken(token);
7802
+ money.setAccessToken(token);
7803
+ } else {
7804
+ client.setSessionToken(null);
7805
+ baseClient.clearAccessToken();
7806
+ money.setAccessToken(void 0);
7807
+ }
7808
+ setTokenVersion((v) => v + 1);
7809
+ }
7810
+ if (userResolver) {
7811
+ try {
7812
+ const resolvedUser = await userResolver();
7813
+ if (mounted) {
7814
+ setUser(resolvedUser);
7815
+ setCachedUser(resolvedUser);
7816
+ }
7817
+ } catch (resolveErr) {
7818
+ if (mounted && debug) {
7819
+ console.debug("[ScaleMule] userResolver() failed:", resolveErr);
7820
+ }
7821
+ }
7822
+ }
7823
+ } catch (memberErr) {
7824
+ if (mounted && debug) {
7825
+ console.debug("[ScaleMule] getToken() failed:", memberErr);
7826
+ }
7827
+ } finally {
7828
+ client.resolveSessionPending();
7829
+ }
7830
+ return;
7831
+ }
7696
7832
  if (authProxyUrl) {
7697
7833
  if (cachedUser && mounted) {
7698
7834
  setUser(cachedUser);
@@ -7764,7 +7900,43 @@ function ScaleMuleProvider({
7764
7900
  return () => {
7765
7901
  mounted = false;
7766
7902
  };
7767
- }, [client, debug, onAuthError, authProxyUrl]);
7903
+ }, [client, baseClient, money, debug, onAuthError, authProxyUrl, memberMode, getToken, userResolver]);
7904
+ useEffect(() => {
7905
+ if (!memberMode) return;
7906
+ if (memberTokenPollMs === null) return;
7907
+ const interval = memberTokenPollMs ?? 6e4;
7908
+ if (interval <= 0) return;
7909
+ let cancelled = false;
7910
+ const id = setInterval(async () => {
7911
+ try {
7912
+ const token = await Promise.resolve(getToken()) ?? null;
7913
+ if (cancelled) return;
7914
+ const current = client.getSessionToken();
7915
+ if (token === current) return;
7916
+ if (token) {
7917
+ client.setSessionToken(token);
7918
+ baseClient.setAccessToken(token);
7919
+ money.setAccessToken(token);
7920
+ } else {
7921
+ client.setSessionToken(null);
7922
+ baseClient.clearAccessToken();
7923
+ money.setAccessToken(void 0);
7924
+ if (debug) {
7925
+ console.debug("[ScaleMule] Member token cleared on refresh");
7926
+ }
7927
+ }
7928
+ setTokenVersion((v) => v + 1);
7929
+ } catch (err) {
7930
+ if (debug) {
7931
+ console.debug("[ScaleMule] Member token refresh failed:", err);
7932
+ }
7933
+ }
7934
+ }, interval);
7935
+ return () => {
7936
+ cancelled = true;
7937
+ clearInterval(id);
7938
+ };
7939
+ }, [memberMode, memberTokenPollMs, getToken, client, baseClient, money, debug]);
7768
7940
  const handleSetUser = useCallback(
7769
7941
  (newUser) => {
7770
7942
  setUser(newUser);
@@ -8246,8 +8418,8 @@ function useAuth() {
8246
8418
  const getLinkedAccounts = useCallback(async () => {
8247
8419
  setError(null);
8248
8420
  try {
8249
- const data = await client.get("/v1/auth/oauth/accounts");
8250
- return data.accounts;
8421
+ const data = await client.get("/v1/auth/oauth/providers");
8422
+ return data.providers;
8251
8423
  } catch (err) {
8252
8424
  if (err instanceof ScaleMuleApiError) {
8253
8425
  setError(err);
@@ -8256,41 +8428,30 @@ function useAuth() {
8256
8428
  }
8257
8429
  }, [client, setError]);
8258
8430
  const linkAccount = useCallback(
8259
- async (config) => {
8431
+ async (_config) => {
8260
8432
  setError(null);
8261
8433
  if (!user) {
8262
- const err = {
8434
+ const err2 = {
8263
8435
  code: "NOT_AUTHENTICATED",
8264
8436
  message: "Must be logged in to link accounts"
8265
8437
  };
8266
- setError(err);
8267
- throw err;
8438
+ setError(err2);
8439
+ throw err2;
8268
8440
  }
8269
- let linkData;
8270
- try {
8271
- linkData = await client.post("/v1/auth/oauth/link", {
8272
- provider: config.provider,
8273
- redirect_url: config.redirectUrl,
8274
- scopes: config.scopes
8275
- });
8276
- } catch (err) {
8277
- if (err instanceof ScaleMuleApiError) {
8278
- setError(err);
8279
- }
8280
- throw err;
8281
- }
8282
- if (typeof sessionStorage !== "undefined") {
8283
- sessionStorage.setItem("scalemule_oauth_state", linkData.state);
8284
- }
8285
- return linkData;
8441
+ const err = {
8442
+ code: "NOT_IMPLEMENTED",
8443
+ message: "OAuth account linking from a logged-in session is not yet supported. Use the standard OAuth sign-in flow instead."
8444
+ };
8445
+ setError(err);
8446
+ throw err;
8286
8447
  },
8287
- [client, user, setError]
8448
+ [user, setError]
8288
8449
  );
8289
8450
  const unlinkAccount = useCallback(
8290
8451
  async (provider) => {
8291
8452
  setError(null);
8292
8453
  try {
8293
- await client.delete(`/v1/auth/oauth/accounts/${provider}`);
8454
+ await client.delete(`/v1/auth/oauth/providers/${encodeURIComponent(provider)}`);
8294
8455
  } catch (err) {
8295
8456
  if (err instanceof ScaleMuleApiError) {
8296
8457
  setError(err);
@@ -9363,7 +9524,9 @@ function useMedia() {
9363
9524
  async (file, options) => {
9364
9525
  setUploading(true);
9365
9526
  setError(null);
9366
- const isPublic = options?.is_public ?? false;
9527
+ const visibility = options?.visibility ?? (options?.is_public === true ? "app_public" : options?.is_public === false ? "private" : "private");
9528
+ const isPublic = visibility !== "private";
9529
+ const isAnonymous = visibility === "anonymous_visible";
9367
9530
  const mimeType = file.type || "application/octet-stream";
9368
9531
  const policy = options?.policy ?? providerDefaultPolicy ?? "safe_visible";
9369
9532
  const gateOnPipeline = policy === "safe_public" || policy === "moderated" || policy === "compliance";
@@ -9373,7 +9536,31 @@ function useMedia() {
9373
9536
  onProgress: options?.onProgress,
9374
9537
  signal: options?.signal
9375
9538
  };
9539
+ const storageAnon = storage;
9376
9540
  try {
9541
+ if (isAnonymous) {
9542
+ const r2 = await storageAnon.uploadAnonymous(file, sharedOpts);
9543
+ if (r2.error || !r2.data) {
9544
+ throw r2.error ?? { code: "upload_error", message: "Upload failed", status: 0 };
9545
+ }
9546
+ const f2 = r2.data;
9547
+ return {
9548
+ file_id: f2.id,
9549
+ photo_id: null,
9550
+ // For anon files, original_view_url is the same unsigned
9551
+ // CDN URL — no signed alternative exists. Callers should
9552
+ // prefer `cdn_url` (more specific) but `original_view_url`
9553
+ // works too for back-compat with code that already reads
9554
+ // that field.
9555
+ original_view_url: f2.cdn_url ?? null,
9556
+ optimized_url_promise: Promise.resolve(null),
9557
+ hls_url_promise: Promise.resolve(null),
9558
+ mime_type: mimeType,
9559
+ is_public: true,
9560
+ visibility: "anonymous_visible",
9561
+ cdn_url: f2.cdn_url ?? null
9562
+ };
9563
+ }
9377
9564
  if (mimeType.startsWith("image/") && !options?.skipPhotoRegister && !isPublic) {
9378
9565
  const r2 = await photo.uploadViaStorage(file, sharedOpts);
9379
9566
  if (r2.error || !r2.data) {
@@ -9389,7 +9576,9 @@ function useMedia() {
9389
9576
  optimized_url_promise: r2.data.optimized_url_promise,
9390
9577
  hls_url_promise: Promise.resolve(null),
9391
9578
  mime_type: mimeType,
9392
- is_public: false
9579
+ is_public: false,
9580
+ visibility: "private",
9581
+ cdn_url: null
9393
9582
  };
9394
9583
  }
9395
9584
  if (mimeType.startsWith("video/") && !isPublic) {
@@ -9407,7 +9596,9 @@ function useMedia() {
9407
9596
  optimized_url_promise: Promise.resolve(null),
9408
9597
  hls_url_promise: r2.data.hls_url_promise,
9409
9598
  mime_type: mimeType,
9410
- is_public: false
9599
+ is_public: false,
9600
+ visibility: "private",
9601
+ cdn_url: null
9411
9602
  };
9412
9603
  }
9413
9604
  if (mimeType.startsWith("audio/") && !isPublic) {
@@ -9422,13 +9613,15 @@ function useMedia() {
9422
9613
  optimized_url_promise: Promise.resolve(null),
9423
9614
  hls_url_promise: Promise.resolve(null),
9424
9615
  mime_type: mimeType,
9425
- is_public: false
9616
+ is_public: false,
9617
+ visibility: "private",
9618
+ cdn_url: null
9426
9619
  };
9427
9620
  }
9428
9621
  if (mimeType.startsWith("image/") && isPublic) {
9429
9622
  const r2 = await storage.upload(file, {
9430
9623
  ...sharedOpts,
9431
- isPublic: true,
9624
+ visibility: "app_public",
9432
9625
  skipCompression: true
9433
9626
  });
9434
9627
  if (r2.error || !r2.data) {
@@ -9442,10 +9635,16 @@ function useMedia() {
9442
9635
  optimized_url_promise: Promise.resolve(null),
9443
9636
  hls_url_promise: Promise.resolve(null),
9444
9637
  mime_type: mimeType,
9445
- is_public: f2.is_public ?? true
9638
+ is_public: f2.is_public ?? true,
9639
+ visibility: f2.visibility ?? "app_public",
9640
+ cdn_url: f2.cdn_url ?? null
9446
9641
  };
9447
9642
  }
9448
- const r = isPublic ? await storage.upload(file, { ...sharedOpts, isPublic: true, skipCompression: true }) : await storage.uploadPrivate(file, sharedOpts);
9643
+ const r = isPublic ? await storage.upload(file, {
9644
+ ...sharedOpts,
9645
+ visibility: "app_public",
9646
+ skipCompression: true
9647
+ }) : await storage.uploadPrivate(file, sharedOpts);
9449
9648
  if (r.error || !r.data) {
9450
9649
  throw r.error ?? { code: "upload_error", message: "Upload failed", status: 0 };
9451
9650
  }
@@ -9457,7 +9656,9 @@ function useMedia() {
9457
9656
  optimized_url_promise: Promise.resolve(null),
9458
9657
  hls_url_promise: Promise.resolve(null),
9459
9658
  mime_type: mimeType,
9460
- is_public: f.is_public ?? isPublic
9659
+ is_public: f.is_public ?? isPublic,
9660
+ visibility: f.visibility ?? (isPublic ? "app_public" : "private"),
9661
+ cdn_url: f.cdn_url ?? null
9461
9662
  };
9462
9663
  } catch (err) {
9463
9664
  const e = err;
@@ -10900,6 +11101,75 @@ function useFeedback(options = {}) {
10900
11101
  );
10901
11102
  return { items, loading, error, submit, refresh };
10902
11103
  }
11104
+ var ZERO = { value: 0, up_count: 0, down_count: 0, score: 0 };
11105
+ function useVote({
11106
+ targetType,
11107
+ targetId,
11108
+ initialState,
11109
+ refetchOnMount = false,
11110
+ enabled = true
11111
+ }) {
11112
+ const { client } = useScaleMule();
11113
+ const [state, setState] = useState(initialState ?? ZERO);
11114
+ const [isLoading, setIsLoading] = useState(!initialState && enabled);
11115
+ const [error, setError] = useState(null);
11116
+ const inFlight = useRef(null);
11117
+ const path = `/v1/social/${encodeURIComponent(targetType)}/${encodeURIComponent(targetId)}/vote`;
11118
+ const refetch = useCallback(async () => {
11119
+ if (!enabled) return;
11120
+ setIsLoading(true);
11121
+ setError(null);
11122
+ try {
11123
+ const data = await client.get(path);
11124
+ setState(data);
11125
+ } catch (e) {
11126
+ setError(e);
11127
+ } finally {
11128
+ setIsLoading(false);
11129
+ }
11130
+ }, [client, path, enabled]);
11131
+ useEffect(() => {
11132
+ if (!enabled) return;
11133
+ if (initialState && !refetchOnMount) return;
11134
+ void refetch();
11135
+ }, [enabled, refetch, initialState, refetchOnMount]);
11136
+ const cast = useCallback(
11137
+ async (value) => {
11138
+ const prev = state;
11139
+ const optimistic = applyVote(prev, value);
11140
+ setState(optimistic);
11141
+ setError(null);
11142
+ const send = async () => {
11143
+ try {
11144
+ const data = await client.put(path, { value });
11145
+ setState(data);
11146
+ } catch (e) {
11147
+ setState(prev);
11148
+ setError(e);
11149
+ }
11150
+ };
11151
+ const p = inFlight.current ? inFlight.current.then(send) : send();
11152
+ inFlight.current = p;
11153
+ await p;
11154
+ if (inFlight.current === p) inFlight.current = null;
11155
+ },
11156
+ [client, path, state]
11157
+ );
11158
+ return { state, isLoading, error, cast, refetch };
11159
+ }
11160
+ function applyVote(prev, next) {
11161
+ let { up_count, down_count } = prev;
11162
+ if (prev.value === 1) up_count = Math.max(0, up_count - 1);
11163
+ if (prev.value === -1) down_count = Math.max(0, down_count - 1);
11164
+ if (next === 1) up_count += 1;
11165
+ if (next === -1) down_count += 1;
11166
+ return {
11167
+ value: next,
11168
+ up_count,
11169
+ down_count,
11170
+ score: up_count - down_count
11171
+ };
11172
+ }
10903
11173
  var TYPE_LABELS = {
10904
11174
  bug_report: "Bug",
10905
11175
  feature_request: "Feature request",
@@ -11268,449 +11538,128 @@ function FeedbackWidget(props) {
11268
11538
  ) })
11269
11539
  ] });
11270
11540
  }
11271
- var SPEEDS = [1, 1.25, 1.5, 2];
11272
- var EXPIRY_REFRESH_WINDOW_MS = 3e4;
11273
- var styles = {
11541
+ var ARROW_UP = "\u25B2";
11542
+ var ARROW_DOWN = "\u25BC";
11543
+ var STYLES2 = {
11274
11544
  root: {
11275
- borderRadius: "1.75rem",
11276
- border: "1px solid rgba(226, 232, 240, 0.8)",
11277
- background: "linear-gradient(180deg, #f8fbff 0%, #edf4ff 100%)",
11278
- padding: "1rem",
11279
- color: "#0f172b",
11280
- boxShadow: "0 22px 70px rgba(4, 12, 24, 0.28)"
11281
- },
11282
- layout: {
11283
- display: "flex",
11284
- flexWrap: "wrap",
11545
+ display: "inline-flex",
11285
11546
  alignItems: "center",
11286
- gap: "1rem"
11547
+ gap: 6
11287
11548
  },
11288
- playButton: {
11289
- display: "flex",
11290
- height: "4.5rem",
11291
- width: "4.5rem",
11292
- flexShrink: 0,
11293
- alignItems: "center",
11294
- justifyContent: "center",
11295
- borderRadius: "9999px",
11296
- border: "none",
11297
- backgroundColor: "#020618",
11298
- color: "#ffffff",
11299
- boxShadow: "0 10px 28px rgba(15, 23, 42, 0.25)",
11300
- cursor: "pointer"
11301
- },
11302
- playIcon: {
11303
- display: "block",
11304
- width: "1.75rem",
11305
- height: "1.75rem",
11306
- fill: "currentColor"
11307
- },
11308
- playIconOffset: {
11309
- marginLeft: "0.25rem"
11310
- },
11311
- content: {
11312
- minWidth: "16rem",
11313
- flex: "1 1 18rem"
11314
- },
11315
- metaRow: {
11316
- display: "flex",
11317
- alignItems: "center",
11318
- justifyContent: "space-between",
11319
- gap: "0.75rem",
11320
- fontSize: "0.8rem",
11321
- fontWeight: 500,
11322
- color: "#62748e"
11323
- },
11324
- metaRight: {
11325
- display: "flex",
11549
+ rootStacked: {
11550
+ display: "inline-flex",
11551
+ flexDirection: "column",
11326
11552
  alignItems: "center",
11327
- gap: "0.75rem"
11328
- },
11329
- badge: {
11330
- borderRadius: "9999px",
11331
- backgroundColor: "#dbeafe",
11332
- padding: "0.25rem 0.75rem",
11333
- fontSize: "0.65rem",
11334
- fontWeight: 600,
11335
- letterSpacing: "0.24em",
11336
- textTransform: "uppercase",
11337
- color: "#1447e6"
11553
+ gap: 2
11338
11554
  },
11339
- scrubber: {
11340
- position: "relative",
11341
- marginTop: "1rem",
11342
- height: "3rem",
11555
+ button: {
11556
+ border: "1px solid rgba(255,255,255,0.12)",
11557
+ background: "rgba(255,255,255,0.04)",
11558
+ color: "inherit",
11343
11559
  cursor: "pointer",
11344
- overflow: "hidden",
11345
- borderRadius: "1rem",
11346
- backgroundColor: "rgba(226, 232, 240, 0.9)",
11347
- paddingInline: "0.25rem"
11560
+ borderRadius: 9999,
11561
+ padding: "6px 10px",
11562
+ lineHeight: 1,
11563
+ fontSize: 14
11348
11564
  },
11349
- waveform: {
11350
- pointerEvents: "none",
11351
- position: "absolute",
11352
- inset: 0,
11353
- height: "100%",
11354
- width: "100%"
11565
+ buttonCompact: {
11566
+ padding: "3px 7px",
11567
+ fontSize: 12
11355
11568
  },
11356
- fallbackTrack: {
11357
- position: "absolute",
11358
- inset: 0,
11359
- backgroundColor: "rgba(226, 232, 240, 0.9)"
11360
- },
11361
- fallbackProgress: {
11362
- position: "absolute",
11363
- insetBlock: 0,
11364
- left: 0,
11365
- backgroundColor: "#155dfc"
11366
- },
11367
- chipsRow: {
11368
- marginTop: "1rem",
11369
- display: "flex",
11370
- flexWrap: "wrap",
11371
- alignItems: "center",
11372
- gap: "0.5rem",
11373
- fontSize: "0.72rem",
11374
- fontWeight: 500,
11375
- color: "#62748e"
11376
- },
11377
- chipStrong: {
11378
- borderRadius: "9999px",
11379
- border: "1px solid #e2e8f0",
11380
- backgroundColor: "rgba(255, 255, 255, 0.85)",
11381
- padding: "0.25rem 0.75rem",
11382
- color: "#334155"
11383
- },
11384
- chipMuted: {
11385
- borderRadius: "9999px",
11386
- border: "1px solid #e2e8f0",
11387
- backgroundColor: "rgba(255, 255, 255, 0.65)",
11388
- padding: "0.25rem 0.75rem"
11389
- },
11390
- controls: {
11391
- display: "flex",
11392
- flexShrink: 0,
11393
- alignItems: "center",
11394
- gap: "0.5rem",
11395
- alignSelf: "flex-start"
11396
- },
11397
- secondaryButton: {
11398
- borderRadius: "9999px",
11399
- border: "1px solid #cad5e2",
11400
- backgroundColor: "rgba(255, 255, 255, 0.9)",
11401
- padding: "0.5rem 1rem",
11402
- fontSize: "1rem",
11403
- fontWeight: 600,
11404
- color: "#334155",
11405
- boxShadow: "0 1px 3px rgba(15, 23, 42, 0.08)",
11406
- cursor: "pointer"
11569
+ buttonActiveUp: {
11570
+ background: "rgba(34,197,94,0.18)",
11571
+ borderColor: "rgba(34,197,94,0.45)",
11572
+ color: "#86efac"
11407
11573
  },
11408
- refreshButton: {
11409
- borderRadius: "9999px",
11410
- border: "1px solid #cad5e2",
11411
- backgroundColor: "rgba(255, 255, 255, 0.75)",
11412
- padding: "0.5rem 1rem",
11413
- fontSize: "0.875rem",
11414
- fontWeight: 600,
11415
- color: "#334155",
11416
- cursor: "pointer"
11574
+ buttonActiveDown: {
11575
+ background: "rgba(239,68,68,0.18)",
11576
+ borderColor: "rgba(239,68,68,0.45)",
11577
+ color: "#fca5a5"
11417
11578
  },
11418
- disabled: {
11419
- cursor: "not-allowed",
11420
- opacity: 0.6
11579
+ score: {
11580
+ minWidth: 24,
11581
+ textAlign: "center",
11582
+ fontVariantNumeric: "tabular-nums",
11583
+ fontWeight: 600
11421
11584
  }
11422
11585
  };
11423
- function formatTime(seconds) {
11424
- if (!Number.isFinite(seconds) || seconds <= 0) {
11425
- return "0:00";
11426
- }
11427
- const wholeSeconds = Math.floor(seconds);
11428
- const minutes = Math.floor(wholeSeconds / 60);
11429
- const remainder = wholeSeconds % 60;
11430
- return `${minutes}:${remainder.toString().padStart(2, "0")}`;
11431
- }
11432
- function getNextSpeed(currentSpeed) {
11433
- const currentIndex = SPEEDS.findIndex((speed) => speed === currentSpeed);
11434
- if (currentIndex === -1) {
11435
- return SPEEDS[0];
11436
- }
11437
- return SPEEDS[(currentIndex + 1) % SPEEDS.length];
11438
- }
11439
- function NarrationPlayer({
11440
- audio,
11441
- className,
11442
- providerLabel = "ScaleMule",
11443
- narrationLabel = "AI-Narrated",
11444
- refreshing = false,
11445
- onRefresh,
11446
- showRefreshButton = false,
11447
- onPlaybackError
11586
+ function VoteButton({
11587
+ targetType,
11588
+ targetId,
11589
+ initialState,
11590
+ layout = "horizontal",
11591
+ size = "regular",
11592
+ onSignInRequired,
11593
+ onError,
11594
+ classNames,
11595
+ upLabel = "Upvote",
11596
+ downLabel = "Downvote"
11448
11597
  }) {
11449
- const audioRef = useRef(null);
11450
- const scrubRef = useRef(null);
11451
- const pendingResumeRef = useRef(false);
11452
- const pendingResumeTimeRef = useRef(null);
11453
- const recoveredUrlRef = useRef(null);
11454
- const [playing, setPlaying] = useState(false);
11455
- const [currentTime, setCurrentTime] = useState(0);
11456
- const [duration, setDuration] = useState(audio.duration_ms ? audio.duration_ms / 1e3 : 0);
11457
- const [speed, setSpeed] = useState(1.5);
11458
- const peaks = useMemo(() => {
11459
- if (!Array.isArray(audio.waveform_peaks)) {
11460
- return [];
11461
- }
11462
- const sampled = audio.waveform_peaks.filter((_, index) => index % 3 === 0).slice(0, 220);
11463
- return sampled.length > 0 ? sampled : audio.waveform_peaks.slice(0, 220);
11464
- }, [audio.waveform_peaks]);
11465
- useEffect(() => {
11466
- const element = audioRef.current;
11467
- if (!element) return;
11468
- recoveredUrlRef.current = null;
11469
- const resumeTime = pendingResumeTimeRef.current;
11470
- setCurrentTime(resumeTime ?? 0);
11471
- setPlaying(false);
11472
- element.pause();
11473
- try {
11474
- element.currentTime = resumeTime ?? 0;
11475
- } catch {
11476
- element.currentTime = 0;
11477
- }
11478
- if (pendingResumeRef.current) {
11479
- pendingResumeRef.current = false;
11480
- pendingResumeTimeRef.current = null;
11481
- element.play().catch(() => {
11482
- setPlaying(false);
11483
- onPlaybackError?.();
11484
- });
11485
- return;
11486
- }
11487
- pendingResumeTimeRef.current = null;
11488
- }, [audio.url]);
11489
- useEffect(() => {
11490
- const element = audioRef.current;
11491
- if (!element) return;
11492
- element.playbackRate = speed;
11493
- element.preservesPitch = true;
11494
- }, [speed]);
11495
- function isUrlExpiringSoon() {
11496
- if (!audio.expires_at) {
11497
- return false;
11498
- }
11499
- const expiresAtMs = Date.parse(audio.expires_at);
11500
- if (!Number.isFinite(expiresAtMs)) {
11501
- return false;
11502
- }
11503
- return expiresAtMs - Date.now() <= EXPIRY_REFRESH_WINDOW_MS;
11504
- }
11505
- async function refreshAndResume() {
11506
- if (!onRefresh || refreshing) {
11507
- return false;
11508
- }
11509
- if (recoveredUrlRef.current === (audio.url ?? null)) {
11510
- return false;
11511
- }
11512
- recoveredUrlRef.current = audio.url ?? null;
11513
- pendingResumeRef.current = true;
11514
- pendingResumeTimeRef.current = currentTime;
11515
- try {
11516
- await Promise.resolve(onRefresh());
11517
- return true;
11518
- } catch {
11519
- pendingResumeRef.current = false;
11520
- pendingResumeTimeRef.current = null;
11521
- onPlaybackError?.();
11522
- return false;
11523
- }
11524
- }
11525
- function togglePlayback() {
11526
- const element = audioRef.current;
11527
- if (!element) return;
11528
- if (element.paused) {
11529
- if (refreshing) {
11530
- pendingResumeRef.current = true;
11531
- pendingResumeTimeRef.current = currentTime;
11598
+ const { isAuthenticated } = useAuth();
11599
+ const { state, cast, error } = useVote({
11600
+ targetType,
11601
+ targetId,
11602
+ initialState: initialState ?? null,
11603
+ enabled: isAuthenticated
11604
+ });
11605
+ const handleVote = useCallback(
11606
+ async (next) => {
11607
+ if (!isAuthenticated) {
11608
+ onSignInRequired?.();
11532
11609
  return;
11533
11610
  }
11534
- if (onRefresh && isUrlExpiringSoon()) {
11535
- void refreshAndResume();
11536
- return;
11611
+ const desired = state.value === next ? 0 : next;
11612
+ try {
11613
+ await cast(desired);
11614
+ } catch (e) {
11615
+ onError?.(e);
11537
11616
  }
11538
- element.play().catch(() => {
11539
- setPlaying(false);
11540
- onPlaybackError?.();
11541
- });
11542
- return;
11617
+ },
11618
+ [cast, isAuthenticated, onSignInRequired, onError, state.value]
11619
+ );
11620
+ useEffect(() => {
11621
+ if (error) onError?.(error);
11622
+ }, [error, onError]);
11623
+ const compactStyle = size === "compact" ? STYLES2.buttonCompact : {};
11624
+ const upActive = state.value === 1;
11625
+ const downActive = state.value === -1;
11626
+ const upBtn = /* @__PURE__ */ jsx(
11627
+ "button",
11628
+ {
11629
+ type: "button",
11630
+ "aria-label": upLabel,
11631
+ "aria-pressed": upActive,
11632
+ title: isAuthenticated ? upLabel : "Sign in to vote",
11633
+ className: [classNames?.upButton, upActive ? classNames?.upButtonActive : void 0].filter(Boolean).join(" ") || void 0,
11634
+ onClick: () => void handleVote(1),
11635
+ style: classNames?.upButton ? void 0 : { ...STYLES2.button, ...compactStyle, ...upActive ? STYLES2.buttonActiveUp : {} },
11636
+ children: ARROW_UP
11543
11637
  }
11544
- element.pause();
11545
- }
11546
- function seekTo(clientX) {
11547
- const element = audioRef.current;
11548
- const scrubber = scrubRef.current;
11549
- if (!element || !scrubber || !duration) return;
11550
- const bounds = scrubber.getBoundingClientRect();
11551
- const ratio = Math.max(0, Math.min(1, (clientX - bounds.left) / bounds.width));
11552
- const nextTime = ratio * duration;
11553
- element.currentTime = nextTime;
11554
- setCurrentTime(nextTime);
11555
- }
11556
- const progress = duration > 0 ? Math.min(1, currentTime / duration) : 0;
11557
- const expiresLabel = audio.expires_at ? new Date(audio.expires_at).toLocaleTimeString([], {
11558
- hour: "numeric",
11559
- minute: "2-digit"
11560
- }) : null;
11561
- if (!audio.url) {
11562
- return null;
11563
- }
11564
- return /* @__PURE__ */ jsxs("div", { className, style: styles.root, children: [
11565
- /* @__PURE__ */ jsx(
11566
- "audio",
11567
- {
11568
- ref: audioRef,
11569
- src: audio.url ?? void 0,
11570
- preload: "metadata",
11571
- onLoadedMetadata: (event) => {
11572
- setDuration(event.currentTarget.duration || 0);
11573
- },
11574
- onTimeUpdate: (event) => {
11575
- setCurrentTime(event.currentTarget.currentTime);
11576
- },
11577
- onPlay: () => setPlaying(true),
11578
- onPause: () => setPlaying(false),
11579
- onEnded: () => setPlaying(false),
11580
- onError: () => {
11581
- if (onRefresh) {
11582
- void refreshAndResume().then((didRefresh) => {
11583
- if (!didRefresh) {
11584
- onPlaybackError?.();
11585
- }
11586
- });
11587
- return;
11588
- }
11589
- onPlaybackError?.();
11590
- }
11591
- }
11592
- ),
11593
- /* @__PURE__ */ jsxs("div", { style: styles.layout, children: [
11594
- /* @__PURE__ */ jsx(
11595
- "button",
11596
- {
11597
- type: "button",
11598
- onClick: togglePlayback,
11599
- "aria-label": playing ? "Pause narration" : "Play narration",
11600
- style: styles.playButton,
11601
- children: playing ? /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", style: styles.playIcon, "aria-hidden": "true", children: [
11602
- /* @__PURE__ */ jsx("rect", { x: "6", y: "5", width: "4", height: "14", rx: "1.2" }),
11603
- /* @__PURE__ */ jsx("rect", { x: "14", y: "5", width: "4", height: "14", rx: "1.2" })
11604
- ] }) : /* @__PURE__ */ jsx(
11605
- "svg",
11606
- {
11607
- viewBox: "0 0 24 24",
11608
- style: { ...styles.playIcon, ...styles.playIconOffset },
11609
- "aria-hidden": "true",
11610
- children: /* @__PURE__ */ jsx("path", { d: "M7 5.2c0-1.02 1.12-1.65 2-.98l10.2 7.78a1.24 1.24 0 0 1 0 1.98L9 21.76c-.88.67-2-.04-2-1.08V5.2Z" })
11611
- }
11612
- )
11613
- }
11614
- ),
11615
- /* @__PURE__ */ jsxs("div", { style: styles.content, children: [
11616
- /* @__PURE__ */ jsxs("div", { style: styles.metaRow, children: [
11617
- /* @__PURE__ */ jsx("span", { children: formatTime(currentTime / speed) }),
11618
- /* @__PURE__ */ jsxs("div", { style: styles.metaRight, children: [
11619
- /* @__PURE__ */ jsx("span", { style: styles.badge, children: narrationLabel }),
11620
- /* @__PURE__ */ jsx("span", { children: formatTime(duration / speed) })
11621
- ] })
11622
- ] }),
11623
- /* @__PURE__ */ jsx(
11624
- "div",
11625
- {
11626
- ref: scrubRef,
11627
- role: "slider",
11628
- "aria-label": "Narration progress",
11629
- "aria-valuemin": 0,
11630
- "aria-valuemax": Math.max(1, Math.floor(duration)),
11631
- "aria-valuenow": Math.floor(currentTime),
11632
- tabIndex: 0,
11633
- onClick: (event) => {
11634
- seekTo(event.clientX);
11635
- },
11636
- style: styles.scrubber,
11637
- children: peaks.length > 0 ? /* @__PURE__ */ jsx(
11638
- "svg",
11639
- {
11640
- viewBox: `0 0 ${peaks.length} 40`,
11641
- preserveAspectRatio: "none",
11642
- style: styles.waveform,
11643
- children: peaks.map((peak, index) => {
11644
- const height = Math.max(2, Math.round(Math.abs(peak) * 34));
11645
- const y = (40 - height) / 2;
11646
- const filled = index / peaks.length < progress;
11647
- return /* @__PURE__ */ jsx(
11648
- "rect",
11649
- {
11650
- x: index,
11651
- y,
11652
- width: 0.82,
11653
- height,
11654
- rx: 0.4,
11655
- fill: filled ? "#155dfc" : "#90c5ff"
11656
- },
11657
- `${audio.id}-${index}`
11658
- );
11659
- })
11660
- }
11661
- ) : /* @__PURE__ */ jsxs(Fragment, { children: [
11662
- /* @__PURE__ */ jsx("div", { style: styles.fallbackTrack }),
11663
- /* @__PURE__ */ jsx(
11664
- "div",
11665
- {
11666
- style: { ...styles.fallbackProgress, width: `${progress * 100}%` }
11667
- }
11668
- )
11669
- ] })
11670
- }
11671
- ),
11672
- /* @__PURE__ */ jsxs("div", { style: styles.chipsRow, children: [
11673
- /* @__PURE__ */ jsxs("span", { style: styles.chipStrong, children: [
11674
- "Provider: ",
11675
- providerLabel
11676
- ] }),
11677
- duration > 0 && /* @__PURE__ */ jsxs("span", { style: styles.chipMuted, children: [
11678
- "Length ",
11679
- formatTime(duration / speed)
11680
- ] }),
11681
- expiresLabel && /* @__PURE__ */ jsxs("span", { style: styles.chipMuted, children: [
11682
- "Expires ",
11683
- expiresLabel
11684
- ] })
11685
- ] })
11686
- ] }),
11687
- /* @__PURE__ */ jsxs("div", { style: styles.controls, children: [
11688
- /* @__PURE__ */ jsxs(
11689
- "button",
11690
- {
11691
- type: "button",
11692
- onClick: () => {
11693
- setSpeed((currentSpeed) => getNextSpeed(currentSpeed));
11694
- },
11695
- style: styles.secondaryButton,
11696
- children: [
11697
- speed,
11698
- "\xD7"
11699
- ]
11700
- }
11701
- ),
11702
- onRefresh && showRefreshButton && /* @__PURE__ */ jsx(
11703
- "button",
11704
- {
11705
- type: "button",
11706
- onClick: onRefresh,
11707
- disabled: refreshing,
11708
- style: refreshing ? { ...styles.refreshButton, ...styles.disabled } : styles.refreshButton,
11709
- children: refreshing ? "Refreshing\u2026" : "Refresh"
11710
- }
11711
- )
11712
- ] })
11713
- ] })
11638
+ );
11639
+ const downBtn = /* @__PURE__ */ jsx(
11640
+ "button",
11641
+ {
11642
+ type: "button",
11643
+ "aria-label": downLabel,
11644
+ "aria-pressed": downActive,
11645
+ title: isAuthenticated ? downLabel : "Sign in to vote",
11646
+ className: [classNames?.downButton, downActive ? classNames?.downButtonActive : void 0].filter(Boolean).join(" ") || void 0,
11647
+ onClick: () => void handleVote(-1),
11648
+ style: classNames?.downButton ? void 0 : { ...STYLES2.button, ...compactStyle, ...downActive ? STYLES2.buttonActiveDown : {} },
11649
+ children: ARROW_DOWN
11650
+ }
11651
+ );
11652
+ const scoreClass = [
11653
+ classNames?.score,
11654
+ state.score > 0 ? classNames?.scorePositive : void 0,
11655
+ state.score < 0 ? classNames?.scoreNegative : void 0
11656
+ ].filter(Boolean).join(" ") || void 0;
11657
+ const scoreEl = /* @__PURE__ */ jsx("span", { className: scoreClass, style: classNames?.score ? void 0 : STYLES2.score, children: state.score });
11658
+ const rootStyle = layout === "stacked" ? STYLES2.rootStacked : STYLES2.root;
11659
+ return /* @__PURE__ */ jsxs("div", { className: classNames?.root, style: classNames?.root ? void 0 : rootStyle, children: [
11660
+ upBtn,
11661
+ scoreEl,
11662
+ downBtn
11714
11663
  ] });
11715
11664
  }
11716
11665
 
@@ -12005,4 +11954,4 @@ function createSafeLogger(prefix) {
12005
11954
  };
12006
11955
  }
12007
11956
 
12008
- export { FeedbackWidget, NarrationPlayer, ScaleMuleApiError, ScaleMuleClient2 as ScaleMuleClient, ScaleMuleMedia, ScaleMuleProvider, composePhone, createClient, createSafeLogger, normalizePhone, phoneCountries, sanitizeForLog, useAnalytics, useAudio, useAuth, useBilling, useContent, useFeatureFlags, useFeedback, useFileStatus, useMedia, useMoney, useMoneyClient, usePushNotifications, useRealtime, useScaleMule, useScaleMuleClient, useShare, useTtsJob, useUser, validateForm, validators };
11957
+ export { FeedbackWidget, ScaleMuleApiError, ScaleMuleClient2 as ScaleMuleClient, ScaleMuleMedia, ScaleMuleProvider, VoteButton, composePhone, createClient, createSafeLogger, normalizePhone, phoneCountries, sanitizeForLog, useAnalytics, useAudio, useAuth, useBilling, useContent, useFeatureFlags, useFeedback, useFileStatus, useMedia, useMoney, useMoneyClient, usePushNotifications, useRealtime, useScaleMule, useScaleMuleClient, useShare, useTtsJob, useUser, useVote, validateForm, validators };