@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.js CHANGED
@@ -580,6 +580,7 @@ var ScaleMuleClient = class {
580
580
  this.anonymousId = null;
581
581
  this.sessionPool = /* @__PURE__ */ new Map();
582
582
  this.knownAccounts = /* @__PURE__ */ new Map();
583
+ this.refreshPromise = null;
583
584
  this.apiKey = config.apiKey;
584
585
  this.applicationId = config.applicationId || null;
585
586
  this.baseUrl = config.baseUrl || GATEWAY_URLS[config.environment || "prod"];
@@ -598,6 +599,9 @@ var ScaleMuleClient = class {
598
599
  this.multiSessionEnabled = config.enableMultiSession || false;
599
600
  this.accountSwitcherEnabled = config.enableAccountSwitcher || false;
600
601
  this.accountSwitcherPrivacy = config.accountSwitcherPrivacy || "full";
602
+ this.onRefreshStart = config.onRefreshStart;
603
+ this.onRefreshEnd = config.onRefreshEnd;
604
+ this.onAutoRefreshFailed = config.onAutoRefreshFailed;
601
605
  }
602
606
  // --------------------------------------------------------------------------
603
607
  // Session Management
@@ -883,34 +887,28 @@ var ScaleMuleClient = class {
883
887
  const method = (init.method || "GET").toUpperCase();
884
888
  const timeout = init.timeout || this.defaultTimeout;
885
889
  const maxRetries = init.skipRetry ? 0 : init.retries ?? this.maxRetries;
886
- const headers = {
887
- "x-api-key": this.apiKey,
888
- "User-Agent": `ScaleMule-SDK-TypeScript/${SDK_VERSION}`,
889
- ...init.headers
890
- };
891
- if (!init.skipAuth && this.sessionToken) {
892
- headers["Authorization"] = `Bearer ${this.sessionToken}`;
893
- }
894
- if (this.workspaceId) {
895
- headers["x-sm-workspace-id"] = this.workspaceId;
896
- }
897
- if (!this.sessionToken && this.anonymousId) {
898
- headers["x-anonymous-id"] = this.anonymousId;
899
- }
900
- let bodyStr;
901
- if (init.body !== void 0 && init.body !== null) {
902
- bodyStr = typeof init.body === "string" ? init.body : JSON.stringify(init.body);
903
- if (!headers["Content-Type"]) {
904
- headers["Content-Type"] = "application/json";
905
- }
906
- }
907
- if (this.debug) {
908
- console.log(`[ScaleMule] ${method} ${path}`);
909
- }
910
- const idempotencyKey = method === "POST" ? generateIdempotencyKey() : void 0;
890
+ const bodyStr = init.body ? JSON.stringify(init.body) : void 0;
911
891
  let lastError = null;
892
+ let idempotencyKey = null;
912
893
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
913
- if (attempt > 0 && idempotencyKey) {
894
+ const headers = {
895
+ "x-api-key": this.apiKey,
896
+ "User-Agent": `ScaleMule-SDK-TypeScript/${SDK_VERSION}`,
897
+ ...init.headers
898
+ };
899
+ if (!init.skipAuth && this.sessionToken) {
900
+ headers["Authorization"] = `Bearer ${this.sessionToken}`;
901
+ }
902
+ if (this.workspaceId) {
903
+ headers["x-sm-workspace-id"] = this.workspaceId;
904
+ }
905
+ if (!this.sessionToken && this.anonymousId) {
906
+ headers["x-anonymous-id"] = this.anonymousId;
907
+ }
908
+ if (attempt > 0 && (method === "POST" || method === "PUT" || method === "PATCH")) {
909
+ if (!idempotencyKey) {
910
+ idempotencyKey = generateIdempotencyKey();
911
+ }
914
912
  headers["x-idempotency-key"] = idempotencyKey;
915
913
  }
916
914
  const controller = new AbortController();
@@ -930,6 +928,10 @@ var ScaleMuleClient = class {
930
928
  signal: controller.signal
931
929
  });
932
930
  clearTimeout(timeoutId);
931
+ const rotatedToken = response.headers.get("x-rotated-session-token");
932
+ if (rotatedToken && this.sessionToken && this.userId) {
933
+ this.setSession(rotatedToken, this.userId);
934
+ }
933
935
  if (this.rateLimitQueue) {
934
936
  this.rateLimitQueue.updateFromHeaders(response.headers);
935
937
  }
@@ -952,6 +954,45 @@ var ScaleMuleClient = class {
952
954
  status: response.status,
953
955
  details: responseData?.error?.details || responseData?.details
954
956
  };
957
+ if (response.status === 401 && this.sessionToken && !init.isAutoRefresh) {
958
+ if (this.debug) console.log("[ScaleMule] 401 received, attempting auto-refresh...");
959
+ let isPrimaryRefresh = false;
960
+ if (!this.refreshPromise) {
961
+ isPrimaryRefresh = true;
962
+ this.onRefreshStart?.();
963
+ this.refreshPromise = this.auth?.refreshSession ? this.auth.refreshSession() : this.post("/auth/refresh", {}, { isAutoRefresh: true });
964
+ }
965
+ const currentRefreshPromise = this.refreshPromise;
966
+ try {
967
+ const refreshResult = await currentRefreshPromise;
968
+ if (!refreshResult || refreshResult.error) {
969
+ if (this.debug) console.log("[ScaleMule] Auto-refresh failed:", refreshResult?.error);
970
+ const apiError = refreshResult?.error || {
971
+ code: "refresh_failed",
972
+ message: "Auto-refresh failed",
973
+ status: 400
974
+ };
975
+ init.onAutoRefreshFailed?.(apiError);
976
+ this.onAutoRefreshFailed?.(apiError);
977
+ return { data: null, error };
978
+ }
979
+ if (this.debug) console.log("[ScaleMule] Auto-refresh succeeded, retrying original request...");
980
+ const newToken = refreshResult.data?.session_token || refreshResult.data?.access_token;
981
+ if (newToken) {
982
+ this.sessionToken = newToken;
983
+ if (this.userId) {
984
+ await this.storage.setItem(SESSION_STORAGE_KEY, newToken);
985
+ }
986
+ }
987
+ attempt--;
988
+ continue;
989
+ } finally {
990
+ if (isPrimaryRefresh && this.refreshPromise === currentRefreshPromise) {
991
+ this.refreshPromise = null;
992
+ this.onRefreshEnd?.();
993
+ }
994
+ }
995
+ }
955
996
  if (response.status === 429) {
956
997
  const retryAfter = response.headers.get("Retry-After");
957
998
  if (retryAfter) {
@@ -5735,7 +5776,7 @@ var AudioService = class extends ServiceModule {
5735
5776
  constructor(client, storage) {
5736
5777
  super(client);
5737
5778
  this.storage = storage;
5738
- this.basePath = "/v1/audios";
5779
+ this.basePath = "/v1/audio";
5739
5780
  }
5740
5781
  /**
5741
5782
  * Register a storage-uploaded audio asset with the audio service.
@@ -5792,6 +5833,32 @@ var AudioService = class extends ServiceModule {
5792
5833
  };
5793
5834
  }
5794
5835
  };
5836
+ var TtsService = class extends ServiceModule {
5837
+ constructor() {
5838
+ super(...arguments);
5839
+ this.basePath = "/v1/tts";
5840
+ }
5841
+ async synthesize(params, options) {
5842
+ return this.post(
5843
+ "/synthesize",
5844
+ {
5845
+ text: params.text,
5846
+ voice: params.voice,
5847
+ model: params.model,
5848
+ provider: params.provider,
5849
+ async: params.async,
5850
+ access_mode: params.accessMode
5851
+ },
5852
+ options
5853
+ );
5854
+ }
5855
+ async getJob(id, options) {
5856
+ return this._get(`/jobs/${id}`, options);
5857
+ }
5858
+ async listVoices(params, options) {
5859
+ return this._get(this.withQuery("/voices", { provider: params?.provider }), options);
5860
+ }
5861
+ };
5795
5862
  var DeadLetterApi = class extends ServiceModule {
5796
5863
  constructor() {
5797
5864
  super(...arguments);
@@ -6612,6 +6679,7 @@ var ScaleMule = class {
6612
6679
  this.functions = new FunctionsService(this._client);
6613
6680
  this.photo = new PhotoService(this._client, this.storage);
6614
6681
  this.audio = new AudioService(this._client, this.storage);
6682
+ this.tts = new TtsService(this._client);
6615
6683
  this.flagContent = new FlagContentService(this._client);
6616
6684
  this.creatorMaker = new CreatorMakerService(this._client);
6617
6685
  this.compliance = new ComplianceService(this._client);
@@ -7178,6 +7246,24 @@ var ScaleMuleClient2 = class {
7178
7246
  console.log("[ScaleMule] Session set for user:", userId);
7179
7247
  }
7180
7248
  }
7249
+ /**
7250
+ * Token-only setter for member-auth surfaces.
7251
+ *
7252
+ * Unlike `setSession(token, userId)`, this does NOT persist a userId or
7253
+ * write to local storage — useful when the host platform owns identity
7254
+ * (e.g. ScaleMule's member dashboards use `MemberAuthProvider` and the
7255
+ * token comes from a `${env}_member_access_token` cookie). The token is
7256
+ * applied in-memory and used as a `Bearer` Authorization header on
7257
+ * subsequent requests.
7258
+ *
7259
+ * Pass `null` to clear without touching userId/storage.
7260
+ */
7261
+ setSessionToken(token) {
7262
+ this.sessionToken = token;
7263
+ if (this.debug) {
7264
+ console.log("[ScaleMule] Session token", token ? "set (token-only)" : "cleared (token-only)");
7265
+ }
7266
+ }
7181
7267
  /**
7182
7268
  * Clear session on logout
7183
7269
  */
@@ -7242,6 +7328,8 @@ var ScaleMuleClient2 = class {
7242
7328
  console.log(`[ScaleMule] ${options.method || "GET"} ${path}`);
7243
7329
  }
7244
7330
  let lastError = null;
7331
+ const MAX_REFRESH_ATTEMPTS = 1;
7332
+ let refreshAttempts = 0;
7245
7333
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
7246
7334
  const controller = new AbortController();
7247
7335
  const timeoutId = setTimeout(() => controller.abort(), timeout);
@@ -7265,7 +7353,8 @@ var ScaleMuleClient2 = class {
7265
7353
  if (!response.ok) {
7266
7354
  const rawError = responseData?.error;
7267
7355
  const error = rawError && typeof rawError === "object" ? rawError : { code: `HTTP_${response.status}`, message: typeof rawError === "string" ? rawError : responseData?.message || text || response.statusText };
7268
- if (response.status === 401 && this.sessionToken && !options.isAutoRefresh) {
7356
+ if (response.status === 401 && this.sessionToken && !options.isAutoRefresh && refreshAttempts < MAX_REFRESH_ATTEMPTS) {
7357
+ refreshAttempts++;
7269
7358
  if (this.debug) console.log("[ScaleMule] 401 received, attempting auto-refresh...");
7270
7359
  if (!this.refreshPromise) {
7271
7360
  this.onRefreshStart?.();
@@ -7634,8 +7723,12 @@ function ScaleMuleProvider({
7634
7723
  onLogout,
7635
7724
  onAuthError,
7636
7725
  bootstrapFlags,
7637
- mediaPolicy
7726
+ mediaPolicy,
7727
+ getToken,
7728
+ userResolver,
7729
+ memberTokenPollMs
7638
7730
  }) {
7731
+ const memberMode = typeof getToken === "function";
7639
7732
  const [user, setUser] = React.useState(null);
7640
7733
  const [initializing, setInitializing] = React.useState(true);
7641
7734
  const [error, setError] = React.useState(null);
@@ -7652,9 +7745,13 @@ function ScaleMuleProvider({
7652
7745
  gatewayUrl: resolvedGatewayUrl,
7653
7746
  debug,
7654
7747
  storage,
7655
- pendingSessionInit: !!authProxyUrl
7748
+ // Make outbound API calls wait for the first token to land in any
7749
+ // mode where the provider is responsible for populating the
7750
+ // session asynchronously: auth-proxy fetch, or member-mode
7751
+ // getToken() callback. Resolved in the init effect below.
7752
+ pendingSessionInit: !!authProxyUrl || memberMode
7656
7753
  }),
7657
- [apiKey, applicationId, environment, resolvedGatewayUrl, debug, storage, authProxyUrl]
7754
+ [apiKey, applicationId, environment, resolvedGatewayUrl, debug, storage, authProxyUrl, memberMode]
7658
7755
  );
7659
7756
  const money$1 = React.useMemo(
7660
7757
  () => money.createMoneyClient({
@@ -7676,8 +7773,10 @@ function ScaleMuleProvider({
7676
7773
  });
7677
7774
  }, [apiKey, applicationId, environment, resolvedGatewayUrl, debug]);
7678
7775
  const [fetchedPolicy, setFetchedPolicy] = React.useState(void 0);
7776
+ const [tokenVersion, setTokenVersion] = React.useState(0);
7679
7777
  React.useEffect(() => {
7680
7778
  if (mediaPolicy) return;
7779
+ if (memberMode && tokenVersion === 0) return;
7681
7780
  let mounted = true;
7682
7781
  void (async () => {
7683
7782
  try {
@@ -7695,7 +7794,7 @@ function ScaleMuleProvider({
7695
7794
  return () => {
7696
7795
  mounted = false;
7697
7796
  };
7698
- }, [baseClient, mediaPolicy]);
7797
+ }, [baseClient, mediaPolicy, memberMode, tokenVersion]);
7699
7798
  const effectiveMediaPolicy = mediaPolicy ?? fetchedPolicy;
7700
7799
  React.useEffect(() => {
7701
7800
  const token = client.getSessionToken();
@@ -7713,6 +7812,43 @@ function ScaleMuleProvider({
7713
7812
  try {
7714
7813
  await client.initialize();
7715
7814
  const cachedUser = getCachedUser();
7815
+ if (memberMode) {
7816
+ try {
7817
+ const token = await Promise.resolve(getToken()) ?? null;
7818
+ if (mounted) {
7819
+ if (token) {
7820
+ client.setSessionToken(token);
7821
+ baseClient.setAccessToken(token);
7822
+ money$1.setAccessToken(token);
7823
+ } else {
7824
+ client.setSessionToken(null);
7825
+ baseClient.clearAccessToken();
7826
+ money$1.setAccessToken(void 0);
7827
+ }
7828
+ setTokenVersion((v) => v + 1);
7829
+ }
7830
+ if (userResolver) {
7831
+ try {
7832
+ const resolvedUser = await userResolver();
7833
+ if (mounted) {
7834
+ setUser(resolvedUser);
7835
+ setCachedUser(resolvedUser);
7836
+ }
7837
+ } catch (resolveErr) {
7838
+ if (mounted && debug) {
7839
+ console.debug("[ScaleMule] userResolver() failed:", resolveErr);
7840
+ }
7841
+ }
7842
+ }
7843
+ } catch (memberErr) {
7844
+ if (mounted && debug) {
7845
+ console.debug("[ScaleMule] getToken() failed:", memberErr);
7846
+ }
7847
+ } finally {
7848
+ client.resolveSessionPending();
7849
+ }
7850
+ return;
7851
+ }
7716
7852
  if (authProxyUrl) {
7717
7853
  if (cachedUser && mounted) {
7718
7854
  setUser(cachedUser);
@@ -7784,7 +7920,43 @@ function ScaleMuleProvider({
7784
7920
  return () => {
7785
7921
  mounted = false;
7786
7922
  };
7787
- }, [client, debug, onAuthError, authProxyUrl]);
7923
+ }, [client, baseClient, money$1, debug, onAuthError, authProxyUrl, memberMode, getToken, userResolver]);
7924
+ React.useEffect(() => {
7925
+ if (!memberMode) return;
7926
+ if (memberTokenPollMs === null) return;
7927
+ const interval = memberTokenPollMs ?? 6e4;
7928
+ if (interval <= 0) return;
7929
+ let cancelled = false;
7930
+ const id = setInterval(async () => {
7931
+ try {
7932
+ const token = await Promise.resolve(getToken()) ?? null;
7933
+ if (cancelled) return;
7934
+ const current = client.getSessionToken();
7935
+ if (token === current) return;
7936
+ if (token) {
7937
+ client.setSessionToken(token);
7938
+ baseClient.setAccessToken(token);
7939
+ money$1.setAccessToken(token);
7940
+ } else {
7941
+ client.setSessionToken(null);
7942
+ baseClient.clearAccessToken();
7943
+ money$1.setAccessToken(void 0);
7944
+ if (debug) {
7945
+ console.debug("[ScaleMule] Member token cleared on refresh");
7946
+ }
7947
+ }
7948
+ setTokenVersion((v) => v + 1);
7949
+ } catch (err) {
7950
+ if (debug) {
7951
+ console.debug("[ScaleMule] Member token refresh failed:", err);
7952
+ }
7953
+ }
7954
+ }, interval);
7955
+ return () => {
7956
+ cancelled = true;
7957
+ clearInterval(id);
7958
+ };
7959
+ }, [memberMode, memberTokenPollMs, getToken, client, baseClient, money$1, debug]);
7788
7960
  const handleSetUser = React.useCallback(
7789
7961
  (newUser) => {
7790
7962
  setUser(newUser);
@@ -8266,8 +8438,8 @@ function useAuth() {
8266
8438
  const getLinkedAccounts = React.useCallback(async () => {
8267
8439
  setError(null);
8268
8440
  try {
8269
- const data = await client.get("/v1/auth/oauth/accounts");
8270
- return data.accounts;
8441
+ const data = await client.get("/v1/auth/oauth/providers");
8442
+ return data.providers;
8271
8443
  } catch (err) {
8272
8444
  if (err instanceof ScaleMuleApiError) {
8273
8445
  setError(err);
@@ -8276,41 +8448,30 @@ function useAuth() {
8276
8448
  }
8277
8449
  }, [client, setError]);
8278
8450
  const linkAccount = React.useCallback(
8279
- async (config) => {
8451
+ async (_config) => {
8280
8452
  setError(null);
8281
8453
  if (!user) {
8282
- const err = {
8454
+ const err2 = {
8283
8455
  code: "NOT_AUTHENTICATED",
8284
8456
  message: "Must be logged in to link accounts"
8285
8457
  };
8286
- setError(err);
8287
- throw err;
8458
+ setError(err2);
8459
+ throw err2;
8288
8460
  }
8289
- let linkData;
8290
- try {
8291
- linkData = await client.post("/v1/auth/oauth/link", {
8292
- provider: config.provider,
8293
- redirect_url: config.redirectUrl,
8294
- scopes: config.scopes
8295
- });
8296
- } catch (err) {
8297
- if (err instanceof ScaleMuleApiError) {
8298
- setError(err);
8299
- }
8300
- throw err;
8301
- }
8302
- if (typeof sessionStorage !== "undefined") {
8303
- sessionStorage.setItem("scalemule_oauth_state", linkData.state);
8304
- }
8305
- return linkData;
8461
+ const err = {
8462
+ code: "NOT_IMPLEMENTED",
8463
+ message: "OAuth account linking from a logged-in session is not yet supported. Use the standard OAuth sign-in flow instead."
8464
+ };
8465
+ setError(err);
8466
+ throw err;
8306
8467
  },
8307
- [client, user, setError]
8468
+ [user, setError]
8308
8469
  );
8309
8470
  const unlinkAccount = React.useCallback(
8310
8471
  async (provider) => {
8311
8472
  setError(null);
8312
8473
  try {
8313
- await client.delete(`/v1/auth/oauth/accounts/${provider}`);
8474
+ await client.delete(`/v1/auth/oauth/providers/${encodeURIComponent(provider)}`);
8314
8475
  } catch (err) {
8315
8476
  if (err instanceof ScaleMuleApiError) {
8316
8477
  setError(err);
@@ -9383,7 +9544,9 @@ function useMedia() {
9383
9544
  async (file, options) => {
9384
9545
  setUploading(true);
9385
9546
  setError(null);
9386
- const isPublic = options?.is_public ?? false;
9547
+ const visibility = options?.visibility ?? (options?.is_public === true ? "app_public" : options?.is_public === false ? "private" : "private");
9548
+ const isPublic = visibility !== "private";
9549
+ const isAnonymous = visibility === "anonymous_visible";
9387
9550
  const mimeType = file.type || "application/octet-stream";
9388
9551
  const policy = options?.policy ?? providerDefaultPolicy ?? "safe_visible";
9389
9552
  const gateOnPipeline = policy === "safe_public" || policy === "moderated" || policy === "compliance";
@@ -9393,7 +9556,31 @@ function useMedia() {
9393
9556
  onProgress: options?.onProgress,
9394
9557
  signal: options?.signal
9395
9558
  };
9559
+ const storageAnon = storage;
9396
9560
  try {
9561
+ if (isAnonymous) {
9562
+ const r2 = await storageAnon.uploadAnonymous(file, sharedOpts);
9563
+ if (r2.error || !r2.data) {
9564
+ throw r2.error ?? { code: "upload_error", message: "Upload failed", status: 0 };
9565
+ }
9566
+ const f2 = r2.data;
9567
+ return {
9568
+ file_id: f2.id,
9569
+ photo_id: null,
9570
+ // For anon files, original_view_url is the same unsigned
9571
+ // CDN URL — no signed alternative exists. Callers should
9572
+ // prefer `cdn_url` (more specific) but `original_view_url`
9573
+ // works too for back-compat with code that already reads
9574
+ // that field.
9575
+ original_view_url: f2.cdn_url ?? null,
9576
+ optimized_url_promise: Promise.resolve(null),
9577
+ hls_url_promise: Promise.resolve(null),
9578
+ mime_type: mimeType,
9579
+ is_public: true,
9580
+ visibility: "anonymous_visible",
9581
+ cdn_url: f2.cdn_url ?? null
9582
+ };
9583
+ }
9397
9584
  if (mimeType.startsWith("image/") && !options?.skipPhotoRegister && !isPublic) {
9398
9585
  const r2 = await photo.uploadViaStorage(file, sharedOpts);
9399
9586
  if (r2.error || !r2.data) {
@@ -9409,7 +9596,9 @@ function useMedia() {
9409
9596
  optimized_url_promise: r2.data.optimized_url_promise,
9410
9597
  hls_url_promise: Promise.resolve(null),
9411
9598
  mime_type: mimeType,
9412
- is_public: false
9599
+ is_public: false,
9600
+ visibility: "private",
9601
+ cdn_url: null
9413
9602
  };
9414
9603
  }
9415
9604
  if (mimeType.startsWith("video/") && !isPublic) {
@@ -9427,7 +9616,9 @@ function useMedia() {
9427
9616
  optimized_url_promise: Promise.resolve(null),
9428
9617
  hls_url_promise: r2.data.hls_url_promise,
9429
9618
  mime_type: mimeType,
9430
- is_public: false
9619
+ is_public: false,
9620
+ visibility: "private",
9621
+ cdn_url: null
9431
9622
  };
9432
9623
  }
9433
9624
  if (mimeType.startsWith("audio/") && !isPublic) {
@@ -9442,13 +9633,15 @@ function useMedia() {
9442
9633
  optimized_url_promise: Promise.resolve(null),
9443
9634
  hls_url_promise: Promise.resolve(null),
9444
9635
  mime_type: mimeType,
9445
- is_public: false
9636
+ is_public: false,
9637
+ visibility: "private",
9638
+ cdn_url: null
9446
9639
  };
9447
9640
  }
9448
9641
  if (mimeType.startsWith("image/") && isPublic) {
9449
9642
  const r2 = await storage.upload(file, {
9450
9643
  ...sharedOpts,
9451
- isPublic: true,
9644
+ visibility: "app_public",
9452
9645
  skipCompression: true
9453
9646
  });
9454
9647
  if (r2.error || !r2.data) {
@@ -9462,10 +9655,16 @@ function useMedia() {
9462
9655
  optimized_url_promise: Promise.resolve(null),
9463
9656
  hls_url_promise: Promise.resolve(null),
9464
9657
  mime_type: mimeType,
9465
- is_public: f2.is_public ?? true
9658
+ is_public: f2.is_public ?? true,
9659
+ visibility: f2.visibility ?? "app_public",
9660
+ cdn_url: f2.cdn_url ?? null
9466
9661
  };
9467
9662
  }
9468
- const r = isPublic ? await storage.upload(file, { ...sharedOpts, isPublic: true, skipCompression: true }) : await storage.uploadPrivate(file, sharedOpts);
9663
+ const r = isPublic ? await storage.upload(file, {
9664
+ ...sharedOpts,
9665
+ visibility: "app_public",
9666
+ skipCompression: true
9667
+ }) : await storage.uploadPrivate(file, sharedOpts);
9469
9668
  if (r.error || !r.data) {
9470
9669
  throw r.error ?? { code: "upload_error", message: "Upload failed", status: 0 };
9471
9670
  }
@@ -9477,7 +9676,9 @@ function useMedia() {
9477
9676
  optimized_url_promise: Promise.resolve(null),
9478
9677
  hls_url_promise: Promise.resolve(null),
9479
9678
  mime_type: mimeType,
9480
- is_public: f.is_public ?? isPublic
9679
+ is_public: f.is_public ?? isPublic,
9680
+ visibility: f.visibility ?? (isPublic ? "app_public" : "private"),
9681
+ cdn_url: f.cdn_url ?? null
9481
9682
  };
9482
9683
  } catch (err) {
9483
9684
  const e = err;
@@ -10920,6 +11121,75 @@ function useFeedback(options = {}) {
10920
11121
  );
10921
11122
  return { items, loading, error, submit, refresh };
10922
11123
  }
11124
+ var ZERO = { value: 0, up_count: 0, down_count: 0, score: 0 };
11125
+ function useVote({
11126
+ targetType,
11127
+ targetId,
11128
+ initialState,
11129
+ refetchOnMount = false,
11130
+ enabled = true
11131
+ }) {
11132
+ const { client } = useScaleMule();
11133
+ const [state, setState] = React.useState(initialState ?? ZERO);
11134
+ const [isLoading, setIsLoading] = React.useState(!initialState && enabled);
11135
+ const [error, setError] = React.useState(null);
11136
+ const inFlight = React.useRef(null);
11137
+ const path = `/v1/social/${encodeURIComponent(targetType)}/${encodeURIComponent(targetId)}/vote`;
11138
+ const refetch = React.useCallback(async () => {
11139
+ if (!enabled) return;
11140
+ setIsLoading(true);
11141
+ setError(null);
11142
+ try {
11143
+ const data = await client.get(path);
11144
+ setState(data);
11145
+ } catch (e) {
11146
+ setError(e);
11147
+ } finally {
11148
+ setIsLoading(false);
11149
+ }
11150
+ }, [client, path, enabled]);
11151
+ React.useEffect(() => {
11152
+ if (!enabled) return;
11153
+ if (initialState && !refetchOnMount) return;
11154
+ void refetch();
11155
+ }, [enabled, refetch, initialState, refetchOnMount]);
11156
+ const cast = React.useCallback(
11157
+ async (value) => {
11158
+ const prev = state;
11159
+ const optimistic = applyVote(prev, value);
11160
+ setState(optimistic);
11161
+ setError(null);
11162
+ const send = async () => {
11163
+ try {
11164
+ const data = await client.put(path, { value });
11165
+ setState(data);
11166
+ } catch (e) {
11167
+ setState(prev);
11168
+ setError(e);
11169
+ }
11170
+ };
11171
+ const p = inFlight.current ? inFlight.current.then(send) : send();
11172
+ inFlight.current = p;
11173
+ await p;
11174
+ if (inFlight.current === p) inFlight.current = null;
11175
+ },
11176
+ [client, path, state]
11177
+ );
11178
+ return { state, isLoading, error, cast, refetch };
11179
+ }
11180
+ function applyVote(prev, next) {
11181
+ let { up_count, down_count } = prev;
11182
+ if (prev.value === 1) up_count = Math.max(0, up_count - 1);
11183
+ if (prev.value === -1) down_count = Math.max(0, down_count - 1);
11184
+ if (next === 1) up_count += 1;
11185
+ if (next === -1) down_count += 1;
11186
+ return {
11187
+ value: next,
11188
+ up_count,
11189
+ down_count,
11190
+ score: up_count - down_count
11191
+ };
11192
+ }
10923
11193
  var TYPE_LABELS = {
10924
11194
  bug_report: "Bug",
10925
11195
  feature_request: "Feature request",
@@ -11288,449 +11558,128 @@ function FeedbackWidget(props) {
11288
11558
  ) })
11289
11559
  ] });
11290
11560
  }
11291
- var SPEEDS = [1, 1.25, 1.5, 2];
11292
- var EXPIRY_REFRESH_WINDOW_MS = 3e4;
11293
- var styles = {
11561
+ var ARROW_UP = "\u25B2";
11562
+ var ARROW_DOWN = "\u25BC";
11563
+ var STYLES2 = {
11294
11564
  root: {
11295
- borderRadius: "1.75rem",
11296
- border: "1px solid rgba(226, 232, 240, 0.8)",
11297
- background: "linear-gradient(180deg, #f8fbff 0%, #edf4ff 100%)",
11298
- padding: "1rem",
11299
- color: "#0f172b",
11300
- boxShadow: "0 22px 70px rgba(4, 12, 24, 0.28)"
11301
- },
11302
- layout: {
11303
- display: "flex",
11304
- flexWrap: "wrap",
11565
+ display: "inline-flex",
11305
11566
  alignItems: "center",
11306
- gap: "1rem"
11567
+ gap: 6
11307
11568
  },
11308
- playButton: {
11309
- display: "flex",
11310
- height: "4.5rem",
11311
- width: "4.5rem",
11312
- flexShrink: 0,
11313
- alignItems: "center",
11314
- justifyContent: "center",
11315
- borderRadius: "9999px",
11316
- border: "none",
11317
- backgroundColor: "#020618",
11318
- color: "#ffffff",
11319
- boxShadow: "0 10px 28px rgba(15, 23, 42, 0.25)",
11320
- cursor: "pointer"
11321
- },
11322
- playIcon: {
11323
- display: "block",
11324
- width: "1.75rem",
11325
- height: "1.75rem",
11326
- fill: "currentColor"
11327
- },
11328
- playIconOffset: {
11329
- marginLeft: "0.25rem"
11330
- },
11331
- content: {
11332
- minWidth: "16rem",
11333
- flex: "1 1 18rem"
11334
- },
11335
- metaRow: {
11336
- display: "flex",
11337
- alignItems: "center",
11338
- justifyContent: "space-between",
11339
- gap: "0.75rem",
11340
- fontSize: "0.8rem",
11341
- fontWeight: 500,
11342
- color: "#62748e"
11343
- },
11344
- metaRight: {
11345
- display: "flex",
11569
+ rootStacked: {
11570
+ display: "inline-flex",
11571
+ flexDirection: "column",
11346
11572
  alignItems: "center",
11347
- gap: "0.75rem"
11348
- },
11349
- badge: {
11350
- borderRadius: "9999px",
11351
- backgroundColor: "#dbeafe",
11352
- padding: "0.25rem 0.75rem",
11353
- fontSize: "0.65rem",
11354
- fontWeight: 600,
11355
- letterSpacing: "0.24em",
11356
- textTransform: "uppercase",
11357
- color: "#1447e6"
11573
+ gap: 2
11358
11574
  },
11359
- scrubber: {
11360
- position: "relative",
11361
- marginTop: "1rem",
11362
- height: "3rem",
11575
+ button: {
11576
+ border: "1px solid rgba(255,255,255,0.12)",
11577
+ background: "rgba(255,255,255,0.04)",
11578
+ color: "inherit",
11363
11579
  cursor: "pointer",
11364
- overflow: "hidden",
11365
- borderRadius: "1rem",
11366
- backgroundColor: "rgba(226, 232, 240, 0.9)",
11367
- paddingInline: "0.25rem"
11580
+ borderRadius: 9999,
11581
+ padding: "6px 10px",
11582
+ lineHeight: 1,
11583
+ fontSize: 14
11368
11584
  },
11369
- waveform: {
11370
- pointerEvents: "none",
11371
- position: "absolute",
11372
- inset: 0,
11373
- height: "100%",
11374
- width: "100%"
11585
+ buttonCompact: {
11586
+ padding: "3px 7px",
11587
+ fontSize: 12
11375
11588
  },
11376
- fallbackTrack: {
11377
- position: "absolute",
11378
- inset: 0,
11379
- backgroundColor: "rgba(226, 232, 240, 0.9)"
11380
- },
11381
- fallbackProgress: {
11382
- position: "absolute",
11383
- insetBlock: 0,
11384
- left: 0,
11385
- backgroundColor: "#155dfc"
11386
- },
11387
- chipsRow: {
11388
- marginTop: "1rem",
11389
- display: "flex",
11390
- flexWrap: "wrap",
11391
- alignItems: "center",
11392
- gap: "0.5rem",
11393
- fontSize: "0.72rem",
11394
- fontWeight: 500,
11395
- color: "#62748e"
11396
- },
11397
- chipStrong: {
11398
- borderRadius: "9999px",
11399
- border: "1px solid #e2e8f0",
11400
- backgroundColor: "rgba(255, 255, 255, 0.85)",
11401
- padding: "0.25rem 0.75rem",
11402
- color: "#334155"
11403
- },
11404
- chipMuted: {
11405
- borderRadius: "9999px",
11406
- border: "1px solid #e2e8f0",
11407
- backgroundColor: "rgba(255, 255, 255, 0.65)",
11408
- padding: "0.25rem 0.75rem"
11409
- },
11410
- controls: {
11411
- display: "flex",
11412
- flexShrink: 0,
11413
- alignItems: "center",
11414
- gap: "0.5rem",
11415
- alignSelf: "flex-start"
11416
- },
11417
- secondaryButton: {
11418
- borderRadius: "9999px",
11419
- border: "1px solid #cad5e2",
11420
- backgroundColor: "rgba(255, 255, 255, 0.9)",
11421
- padding: "0.5rem 1rem",
11422
- fontSize: "1rem",
11423
- fontWeight: 600,
11424
- color: "#334155",
11425
- boxShadow: "0 1px 3px rgba(15, 23, 42, 0.08)",
11426
- cursor: "pointer"
11589
+ buttonActiveUp: {
11590
+ background: "rgba(34,197,94,0.18)",
11591
+ borderColor: "rgba(34,197,94,0.45)",
11592
+ color: "#86efac"
11427
11593
  },
11428
- refreshButton: {
11429
- borderRadius: "9999px",
11430
- border: "1px solid #cad5e2",
11431
- backgroundColor: "rgba(255, 255, 255, 0.75)",
11432
- padding: "0.5rem 1rem",
11433
- fontSize: "0.875rem",
11434
- fontWeight: 600,
11435
- color: "#334155",
11436
- cursor: "pointer"
11594
+ buttonActiveDown: {
11595
+ background: "rgba(239,68,68,0.18)",
11596
+ borderColor: "rgba(239,68,68,0.45)",
11597
+ color: "#fca5a5"
11437
11598
  },
11438
- disabled: {
11439
- cursor: "not-allowed",
11440
- opacity: 0.6
11599
+ score: {
11600
+ minWidth: 24,
11601
+ textAlign: "center",
11602
+ fontVariantNumeric: "tabular-nums",
11603
+ fontWeight: 600
11441
11604
  }
11442
11605
  };
11443
- function formatTime(seconds) {
11444
- if (!Number.isFinite(seconds) || seconds <= 0) {
11445
- return "0:00";
11446
- }
11447
- const wholeSeconds = Math.floor(seconds);
11448
- const minutes = Math.floor(wholeSeconds / 60);
11449
- const remainder = wholeSeconds % 60;
11450
- return `${minutes}:${remainder.toString().padStart(2, "0")}`;
11451
- }
11452
- function getNextSpeed(currentSpeed) {
11453
- const currentIndex = SPEEDS.findIndex((speed) => speed === currentSpeed);
11454
- if (currentIndex === -1) {
11455
- return SPEEDS[0];
11456
- }
11457
- return SPEEDS[(currentIndex + 1) % SPEEDS.length];
11458
- }
11459
- function NarrationPlayer({
11460
- audio,
11461
- className,
11462
- providerLabel = "ScaleMule",
11463
- narrationLabel = "AI-Narrated",
11464
- refreshing = false,
11465
- onRefresh,
11466
- showRefreshButton = false,
11467
- onPlaybackError
11606
+ function VoteButton({
11607
+ targetType,
11608
+ targetId,
11609
+ initialState,
11610
+ layout = "horizontal",
11611
+ size = "regular",
11612
+ onSignInRequired,
11613
+ onError,
11614
+ classNames,
11615
+ upLabel = "Upvote",
11616
+ downLabel = "Downvote"
11468
11617
  }) {
11469
- const audioRef = React.useRef(null);
11470
- const scrubRef = React.useRef(null);
11471
- const pendingResumeRef = React.useRef(false);
11472
- const pendingResumeTimeRef = React.useRef(null);
11473
- const recoveredUrlRef = React.useRef(null);
11474
- const [playing, setPlaying] = React.useState(false);
11475
- const [currentTime, setCurrentTime] = React.useState(0);
11476
- const [duration, setDuration] = React.useState(audio.duration_ms ? audio.duration_ms / 1e3 : 0);
11477
- const [speed, setSpeed] = React.useState(1.5);
11478
- const peaks = React.useMemo(() => {
11479
- if (!Array.isArray(audio.waveform_peaks)) {
11480
- return [];
11481
- }
11482
- const sampled = audio.waveform_peaks.filter((_, index) => index % 3 === 0).slice(0, 220);
11483
- return sampled.length > 0 ? sampled : audio.waveform_peaks.slice(0, 220);
11484
- }, [audio.waveform_peaks]);
11485
- React.useEffect(() => {
11486
- const element = audioRef.current;
11487
- if (!element) return;
11488
- recoveredUrlRef.current = null;
11489
- const resumeTime = pendingResumeTimeRef.current;
11490
- setCurrentTime(resumeTime ?? 0);
11491
- setPlaying(false);
11492
- element.pause();
11493
- try {
11494
- element.currentTime = resumeTime ?? 0;
11495
- } catch {
11496
- element.currentTime = 0;
11497
- }
11498
- if (pendingResumeRef.current) {
11499
- pendingResumeRef.current = false;
11500
- pendingResumeTimeRef.current = null;
11501
- element.play().catch(() => {
11502
- setPlaying(false);
11503
- onPlaybackError?.();
11504
- });
11505
- return;
11506
- }
11507
- pendingResumeTimeRef.current = null;
11508
- }, [audio.url]);
11509
- React.useEffect(() => {
11510
- const element = audioRef.current;
11511
- if (!element) return;
11512
- element.playbackRate = speed;
11513
- element.preservesPitch = true;
11514
- }, [speed]);
11515
- function isUrlExpiringSoon() {
11516
- if (!audio.expires_at) {
11517
- return false;
11518
- }
11519
- const expiresAtMs = Date.parse(audio.expires_at);
11520
- if (!Number.isFinite(expiresAtMs)) {
11521
- return false;
11522
- }
11523
- return expiresAtMs - Date.now() <= EXPIRY_REFRESH_WINDOW_MS;
11524
- }
11525
- async function refreshAndResume() {
11526
- if (!onRefresh || refreshing) {
11527
- return false;
11528
- }
11529
- if (recoveredUrlRef.current === (audio.url ?? null)) {
11530
- return false;
11531
- }
11532
- recoveredUrlRef.current = audio.url ?? null;
11533
- pendingResumeRef.current = true;
11534
- pendingResumeTimeRef.current = currentTime;
11535
- try {
11536
- await Promise.resolve(onRefresh());
11537
- return true;
11538
- } catch {
11539
- pendingResumeRef.current = false;
11540
- pendingResumeTimeRef.current = null;
11541
- onPlaybackError?.();
11542
- return false;
11543
- }
11544
- }
11545
- function togglePlayback() {
11546
- const element = audioRef.current;
11547
- if (!element) return;
11548
- if (element.paused) {
11549
- if (refreshing) {
11550
- pendingResumeRef.current = true;
11551
- pendingResumeTimeRef.current = currentTime;
11618
+ const { isAuthenticated } = useAuth();
11619
+ const { state, cast, error } = useVote({
11620
+ targetType,
11621
+ targetId,
11622
+ initialState: initialState ?? null,
11623
+ enabled: isAuthenticated
11624
+ });
11625
+ const handleVote = React.useCallback(
11626
+ async (next) => {
11627
+ if (!isAuthenticated) {
11628
+ onSignInRequired?.();
11552
11629
  return;
11553
11630
  }
11554
- if (onRefresh && isUrlExpiringSoon()) {
11555
- void refreshAndResume();
11556
- return;
11631
+ const desired = state.value === next ? 0 : next;
11632
+ try {
11633
+ await cast(desired);
11634
+ } catch (e) {
11635
+ onError?.(e);
11557
11636
  }
11558
- element.play().catch(() => {
11559
- setPlaying(false);
11560
- onPlaybackError?.();
11561
- });
11562
- return;
11637
+ },
11638
+ [cast, isAuthenticated, onSignInRequired, onError, state.value]
11639
+ );
11640
+ React.useEffect(() => {
11641
+ if (error) onError?.(error);
11642
+ }, [error, onError]);
11643
+ const compactStyle = size === "compact" ? STYLES2.buttonCompact : {};
11644
+ const upActive = state.value === 1;
11645
+ const downActive = state.value === -1;
11646
+ const upBtn = /* @__PURE__ */ jsxRuntime.jsx(
11647
+ "button",
11648
+ {
11649
+ type: "button",
11650
+ "aria-label": upLabel,
11651
+ "aria-pressed": upActive,
11652
+ title: isAuthenticated ? upLabel : "Sign in to vote",
11653
+ className: [classNames?.upButton, upActive ? classNames?.upButtonActive : void 0].filter(Boolean).join(" ") || void 0,
11654
+ onClick: () => void handleVote(1),
11655
+ style: classNames?.upButton ? void 0 : { ...STYLES2.button, ...compactStyle, ...upActive ? STYLES2.buttonActiveUp : {} },
11656
+ children: ARROW_UP
11563
11657
  }
11564
- element.pause();
11565
- }
11566
- function seekTo(clientX) {
11567
- const element = audioRef.current;
11568
- const scrubber = scrubRef.current;
11569
- if (!element || !scrubber || !duration) return;
11570
- const bounds = scrubber.getBoundingClientRect();
11571
- const ratio = Math.max(0, Math.min(1, (clientX - bounds.left) / bounds.width));
11572
- const nextTime = ratio * duration;
11573
- element.currentTime = nextTime;
11574
- setCurrentTime(nextTime);
11575
- }
11576
- const progress = duration > 0 ? Math.min(1, currentTime / duration) : 0;
11577
- const expiresLabel = audio.expires_at ? new Date(audio.expires_at).toLocaleTimeString([], {
11578
- hour: "numeric",
11579
- minute: "2-digit"
11580
- }) : null;
11581
- if (!audio.url) {
11582
- return null;
11583
- }
11584
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className, style: styles.root, children: [
11585
- /* @__PURE__ */ jsxRuntime.jsx(
11586
- "audio",
11587
- {
11588
- ref: audioRef,
11589
- src: audio.url ?? void 0,
11590
- preload: "metadata",
11591
- onLoadedMetadata: (event) => {
11592
- setDuration(event.currentTarget.duration || 0);
11593
- },
11594
- onTimeUpdate: (event) => {
11595
- setCurrentTime(event.currentTarget.currentTime);
11596
- },
11597
- onPlay: () => setPlaying(true),
11598
- onPause: () => setPlaying(false),
11599
- onEnded: () => setPlaying(false),
11600
- onError: () => {
11601
- if (onRefresh) {
11602
- void refreshAndResume().then((didRefresh) => {
11603
- if (!didRefresh) {
11604
- onPlaybackError?.();
11605
- }
11606
- });
11607
- return;
11608
- }
11609
- onPlaybackError?.();
11610
- }
11611
- }
11612
- ),
11613
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles.layout, children: [
11614
- /* @__PURE__ */ jsxRuntime.jsx(
11615
- "button",
11616
- {
11617
- type: "button",
11618
- onClick: togglePlayback,
11619
- "aria-label": playing ? "Pause narration" : "Play narration",
11620
- style: styles.playButton,
11621
- children: playing ? /* @__PURE__ */ jsxRuntime.jsxs("svg", { viewBox: "0 0 24 24", style: styles.playIcon, "aria-hidden": "true", children: [
11622
- /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "6", y: "5", width: "4", height: "14", rx: "1.2" }),
11623
- /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "14", y: "5", width: "4", height: "14", rx: "1.2" })
11624
- ] }) : /* @__PURE__ */ jsxRuntime.jsx(
11625
- "svg",
11626
- {
11627
- viewBox: "0 0 24 24",
11628
- style: { ...styles.playIcon, ...styles.playIconOffset },
11629
- "aria-hidden": "true",
11630
- children: /* @__PURE__ */ jsxRuntime.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" })
11631
- }
11632
- )
11633
- }
11634
- ),
11635
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles.content, children: [
11636
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles.metaRow, children: [
11637
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: formatTime(currentTime / speed) }),
11638
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles.metaRight, children: [
11639
- /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles.badge, children: narrationLabel }),
11640
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: formatTime(duration / speed) })
11641
- ] })
11642
- ] }),
11643
- /* @__PURE__ */ jsxRuntime.jsx(
11644
- "div",
11645
- {
11646
- ref: scrubRef,
11647
- role: "slider",
11648
- "aria-label": "Narration progress",
11649
- "aria-valuemin": 0,
11650
- "aria-valuemax": Math.max(1, Math.floor(duration)),
11651
- "aria-valuenow": Math.floor(currentTime),
11652
- tabIndex: 0,
11653
- onClick: (event) => {
11654
- seekTo(event.clientX);
11655
- },
11656
- style: styles.scrubber,
11657
- children: peaks.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx(
11658
- "svg",
11659
- {
11660
- viewBox: `0 0 ${peaks.length} 40`,
11661
- preserveAspectRatio: "none",
11662
- style: styles.waveform,
11663
- children: peaks.map((peak, index) => {
11664
- const height = Math.max(2, Math.round(Math.abs(peak) * 34));
11665
- const y = (40 - height) / 2;
11666
- const filled = index / peaks.length < progress;
11667
- return /* @__PURE__ */ jsxRuntime.jsx(
11668
- "rect",
11669
- {
11670
- x: index,
11671
- y,
11672
- width: 0.82,
11673
- height,
11674
- rx: 0.4,
11675
- fill: filled ? "#155dfc" : "#90c5ff"
11676
- },
11677
- `${audio.id}-${index}`
11678
- );
11679
- })
11680
- }
11681
- ) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
11682
- /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles.fallbackTrack }),
11683
- /* @__PURE__ */ jsxRuntime.jsx(
11684
- "div",
11685
- {
11686
- style: { ...styles.fallbackProgress, width: `${progress * 100}%` }
11687
- }
11688
- )
11689
- ] })
11690
- }
11691
- ),
11692
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles.chipsRow, children: [
11693
- /* @__PURE__ */ jsxRuntime.jsxs("span", { style: styles.chipStrong, children: [
11694
- "Provider: ",
11695
- providerLabel
11696
- ] }),
11697
- duration > 0 && /* @__PURE__ */ jsxRuntime.jsxs("span", { style: styles.chipMuted, children: [
11698
- "Length ",
11699
- formatTime(duration / speed)
11700
- ] }),
11701
- expiresLabel && /* @__PURE__ */ jsxRuntime.jsxs("span", { style: styles.chipMuted, children: [
11702
- "Expires ",
11703
- expiresLabel
11704
- ] })
11705
- ] })
11706
- ] }),
11707
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles.controls, children: [
11708
- /* @__PURE__ */ jsxRuntime.jsxs(
11709
- "button",
11710
- {
11711
- type: "button",
11712
- onClick: () => {
11713
- setSpeed((currentSpeed) => getNextSpeed(currentSpeed));
11714
- },
11715
- style: styles.secondaryButton,
11716
- children: [
11717
- speed,
11718
- "\xD7"
11719
- ]
11720
- }
11721
- ),
11722
- onRefresh && showRefreshButton && /* @__PURE__ */ jsxRuntime.jsx(
11723
- "button",
11724
- {
11725
- type: "button",
11726
- onClick: onRefresh,
11727
- disabled: refreshing,
11728
- style: refreshing ? { ...styles.refreshButton, ...styles.disabled } : styles.refreshButton,
11729
- children: refreshing ? "Refreshing\u2026" : "Refresh"
11730
- }
11731
- )
11732
- ] })
11733
- ] })
11658
+ );
11659
+ const downBtn = /* @__PURE__ */ jsxRuntime.jsx(
11660
+ "button",
11661
+ {
11662
+ type: "button",
11663
+ "aria-label": downLabel,
11664
+ "aria-pressed": downActive,
11665
+ title: isAuthenticated ? downLabel : "Sign in to vote",
11666
+ className: [classNames?.downButton, downActive ? classNames?.downButtonActive : void 0].filter(Boolean).join(" ") || void 0,
11667
+ onClick: () => void handleVote(-1),
11668
+ style: classNames?.downButton ? void 0 : { ...STYLES2.button, ...compactStyle, ...downActive ? STYLES2.buttonActiveDown : {} },
11669
+ children: ARROW_DOWN
11670
+ }
11671
+ );
11672
+ const scoreClass = [
11673
+ classNames?.score,
11674
+ state.score > 0 ? classNames?.scorePositive : void 0,
11675
+ state.score < 0 ? classNames?.scoreNegative : void 0
11676
+ ].filter(Boolean).join(" ") || void 0;
11677
+ const scoreEl = /* @__PURE__ */ jsxRuntime.jsx("span", { className: scoreClass, style: classNames?.score ? void 0 : STYLES2.score, children: state.score });
11678
+ const rootStyle = layout === "stacked" ? STYLES2.rootStacked : STYLES2.root;
11679
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: classNames?.root, style: classNames?.root ? void 0 : rootStyle, children: [
11680
+ upBtn,
11681
+ scoreEl,
11682
+ downBtn
11734
11683
  ] });
11735
11684
  }
11736
11685
 
@@ -12034,11 +11983,11 @@ Object.defineProperty(exports, "createMoneyClient", {
12034
11983
  get: function () { return money.createMoneyClient; }
12035
11984
  });
12036
11985
  exports.FeedbackWidget = FeedbackWidget;
12037
- exports.NarrationPlayer = NarrationPlayer;
12038
11986
  exports.ScaleMuleApiError = ScaleMuleApiError;
12039
11987
  exports.ScaleMuleClient = ScaleMuleClient2;
12040
11988
  exports.ScaleMuleMedia = ScaleMuleMedia;
12041
11989
  exports.ScaleMuleProvider = ScaleMuleProvider;
11990
+ exports.VoteButton = VoteButton;
12042
11991
  exports.composePhone = composePhone;
12043
11992
  exports.createClient = createClient;
12044
11993
  exports.createSafeLogger = createSafeLogger;
@@ -12063,5 +12012,6 @@ exports.useScaleMuleClient = useScaleMuleClient;
12063
12012
  exports.useShare = useShare;
12064
12013
  exports.useTtsJob = useTtsJob;
12065
12014
  exports.useUser = useUser;
12015
+ exports.useVote = useVote;
12066
12016
  exports.validateForm = validateForm;
12067
12017
  exports.validators = validators;