@zoreal/oauth2-react 0.2.15 → 0.2.17

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.d.cts CHANGED
@@ -243,25 +243,6 @@ declare function useZorealOAuth(): ZorealOAuthContextProps;
243
243
 
244
244
  declare function ZorealLogin(props: ZorealLoginProps): react.JSX.Element;
245
245
 
246
- /**
247
- * The light of the pairing dialog's QR well, run around whatever this wraps
248
- * while `busy` is true: the well's colours, line, halo and lap, on a dash
249
- * that travels the outline at one speed whatever the shape.
250
- *
251
- * The SDK's own button uses it for the gap on a phone between the tap and the
252
- * hand-over to the app. A site with its own sign-in button MAY wrap that
253
- * button the same way: set your busy state on the tap, clear it when
254
- * `onSuccess` or `onError` fires. `radius` is the wrapped control's corner
255
- * radius in pixels, so the light follows its shape; `theme` picks the light
256
- * and dark strengths the dialog uses, following the OS by default.
257
- *
258
- * It is a wrapper, and a wrapper is a change to your markup, so know what it
259
- * does before you use it: it is inline by default and shrinks to the button,
260
- * so a full-width button needs `block`; the light sits outside the button,
261
- * so an ancestor with `overflow: hidden` clips it; and a selector that relies
262
- * on the button's parent (`.card > button`) no longer matches. A site that
263
- * would rather draw its own busy state needs nothing from here.
264
- */
265
246
  declare function ZorealBusyRing({ busy, radius, theme, block, className, style, children, }: {
266
247
  busy: boolean;
267
248
  radius?: number;
package/dist/index.d.ts CHANGED
@@ -243,25 +243,6 @@ declare function useZorealOAuth(): ZorealOAuthContextProps;
243
243
 
244
244
  declare function ZorealLogin(props: ZorealLoginProps): react.JSX.Element;
245
245
 
246
- /**
247
- * The light of the pairing dialog's QR well, run around whatever this wraps
248
- * while `busy` is true: the well's colours, line, halo and lap, on a dash
249
- * that travels the outline at one speed whatever the shape.
250
- *
251
- * The SDK's own button uses it for the gap on a phone between the tap and the
252
- * hand-over to the app. A site with its own sign-in button MAY wrap that
253
- * button the same way: set your busy state on the tap, clear it when
254
- * `onSuccess` or `onError` fires. `radius` is the wrapped control's corner
255
- * radius in pixels, so the light follows its shape; `theme` picks the light
256
- * and dark strengths the dialog uses, following the OS by default.
257
- *
258
- * It is a wrapper, and a wrapper is a change to your markup, so know what it
259
- * does before you use it: it is inline by default and shrinks to the button,
260
- * so a full-width button needs `block`; the light sits outside the button,
261
- * so an ancestor with `overflow: hidden` clips it; and a selector that relies
262
- * on the button's parent (`.card > button`) no longer matches. A site that
263
- * would rather draw its own busy state needs nothing from here.
264
- */
265
246
  declare function ZorealBusyRing({ busy, radius, theme, block, className, style, children, }: {
266
247
  busy: boolean;
267
248
  radius?: number;
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import { createContext, useContext, useMemo, useState as useState2 } from "react
5
5
 
6
6
  // src/wire.ts
7
7
  var WIRE_VERSION = 1;
8
- var SDK_VERSION = "0.2.15";
8
+ var SDK_VERSION = "0.2.17";
9
9
  var DEFAULT_ISSUER = "https://id.zoreal.com";
10
10
  var POLL_INTERVAL_MS = 2e3;
11
11
  var POLL_INTERVAL_ENROLLING_MS = 5e3;
@@ -1334,10 +1334,15 @@ var CSS = `
1334
1334
  sides and lights two edges at once near the ends. So here the light is a
1335
1335
  dash on an SVG outline, which moves at one speed the whole way round
1336
1336
  whatever the shape, drawn with the well's tokens: its colour and head
1337
- tint, its line width, its halo, its four second lap. pathLength makes the
1338
- outline 100 units long, so every dash and offset is a percentage of the
1339
- way round; the three layers share one head at 30 and differ in length
1340
- and brightness, which is the comet's tail. Shown only while busy. */
1337
+ tint, its line width, its halo, its four second lap. The outline's length
1338
+ is measured by the component and set as --zrl-ring-len, and every dash
1339
+ and offset is a fraction of it, because pathLength does not scale dash
1340
+ values given from CSS. A stroke cannot fade along its length, so the tail
1341
+ is a stack of dashes sharing one head, each shorter and more opaque than
1342
+ the one under it, with opacities chosen so the stack composes to a
1343
+ straight fade from the head to nothing three tenths of the way back; the
1344
+ component sets each layer's length, offset and opacity. Shown only while
1345
+ busy. */
1341
1346
  .${PREFIX}-ring {
1342
1347
  position: relative;
1343
1348
  display: inline-flex;
@@ -1356,6 +1361,7 @@ var CSS = `
1356
1361
  }
1357
1362
  .${PREFIX}-ring[data-busy="true"] > .${PREFIX}-ring-svg { opacity: 1; }
1358
1363
  .${PREFIX}-ring-svg rect {
1364
+ --zrl-l: var(--zrl-ring-len, 600px);
1359
1365
  x: 2px;
1360
1366
  y: 2px;
1361
1367
  width: calc(100% - 4px);
@@ -1364,23 +1370,18 @@ var CSS = `
1364
1370
  stroke: var(--zrl-beam);
1365
1371
  stroke-width: var(--zrl-beam-line);
1366
1372
  stroke-linecap: round;
1373
+ stroke-dashoffset: var(--zrl-s, 0px);
1367
1374
  animation: ${PREFIX}-dash var(--zrl-beam-time) linear infinite;
1368
1375
  }
1369
- .${PREFIX}-ring-tail { stroke-dasharray: 30 70; stroke-dashoffset: 0; opacity: 0.35; }
1370
- .${PREFIX}-ring-body { stroke-dasharray: 16 84; stroke-dashoffset: -14; opacity: 0.8; }
1371
- .${PREFIX}-ring-head { stroke-dasharray: 5 95; stroke-dashoffset: -25; stroke: var(--zrl-beam-head); }
1376
+ .${PREFIX}-ring-head { stroke: var(--zrl-beam-head); }
1372
1377
  .${PREFIX}-ring-halo {
1373
- stroke-dasharray: 30 70;
1374
- stroke-dashoffset: 0;
1375
1378
  stroke-width: calc(var(--zrl-glow-core) * 2 + var(--zrl-beam-line));
1376
- opacity: calc(var(--zrl-glow-opacity) * 0.5);
1377
1379
  filter: blur(var(--zrl-glow-blur));
1378
1380
  }
1379
1381
  @keyframes ${PREFIX}-dash {
1380
- to { stroke-dashoffset: calc(var(--zrl-dash-start, 0) - 100); }
1382
+ from { stroke-dashoffset: var(--zrl-s, 0px); }
1383
+ to { stroke-dashoffset: calc(var(--zrl-s, 0px) - var(--zrl-l)); }
1381
1384
  }
1382
- .${PREFIX}-ring-body { --zrl-dash-start: -14; }
1383
- .${PREFIX}-ring-head { --zrl-dash-start: -25; }
1384
1385
  @media (prefers-reduced-motion: reduce) {
1385
1386
  .${PREFIX}-ring-svg rect { animation: none; stroke-dasharray: none; opacity: 0.45; }
1386
1387
  .${PREFIX}-ring-halo, .${PREFIX}-ring-head { display: none; }
@@ -1654,6 +1655,20 @@ async function startPairing(issuer, params) {
1654
1655
  }
1655
1656
  return body;
1656
1657
  }
1658
+ function sameDeviceStartUrl(issuer, params) {
1659
+ const query = new URLSearchParams();
1660
+ const all = {
1661
+ ...params,
1662
+ code_challenge_method: "S256",
1663
+ wire_version: WIRE_VERSION,
1664
+ sdk: `@zoreal/oauth2-react/${SDK_VERSION}`
1665
+ };
1666
+ for (const [key, value] of Object.entries(all)) {
1667
+ if (value === void 0 || value === null || value === "") continue;
1668
+ query.set(key, String(value));
1669
+ }
1670
+ return `${issuer}/pair/start?${query.toString()}`;
1671
+ }
1657
1672
  function qrRefreshSecondsOf(started) {
1658
1673
  const seconds = started.qr_refresh_seconds;
1659
1674
  return typeof seconds === "number" && seconds > 0 ? seconds : DEFAULT_QR_REFRESH_SECONDS;
@@ -1710,6 +1725,11 @@ async function pollUntilApproved(issuer, requestId, onState, signal, options = {
1710
1725
  signal
1711
1726
  });
1712
1727
  const body = await parseJson(response);
1728
+ if (response.status === 404 && (options.tolerateUnknownUntil ?? 0) > Date.now()) {
1729
+ emit({ status: "pending" });
1730
+ await sleep(POLL_INTERVAL_MS, signal);
1731
+ continue;
1732
+ }
1713
1733
  if (!response.ok) {
1714
1734
  throw new OAuthFlowError(
1715
1735
  body.error ?? "server_error",
@@ -1791,11 +1811,154 @@ async function challengeS256(verifier) {
1791
1811
  const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
1792
1812
  return base64url(new Uint8Array(digest));
1793
1813
  }
1814
+ function challengeS256Sync(verifier) {
1815
+ return base64url(sha256(new TextEncoder().encode(verifier)));
1816
+ }
1817
+ var K = new Uint32Array([
1818
+ 1116352408,
1819
+ 1899447441,
1820
+ 3049323471,
1821
+ 3921009573,
1822
+ 961987163,
1823
+ 1508970993,
1824
+ 2453635748,
1825
+ 2870763221,
1826
+ 3624381080,
1827
+ 310598401,
1828
+ 607225278,
1829
+ 1426881987,
1830
+ 1925078388,
1831
+ 2162078206,
1832
+ 2614888103,
1833
+ 3248222580,
1834
+ 3835390401,
1835
+ 4022224774,
1836
+ 264347078,
1837
+ 604807628,
1838
+ 770255983,
1839
+ 1249150122,
1840
+ 1555081692,
1841
+ 1996064986,
1842
+ 2554220882,
1843
+ 2821834349,
1844
+ 2952996808,
1845
+ 3210313671,
1846
+ 3336571891,
1847
+ 3584528711,
1848
+ 113926993,
1849
+ 338241895,
1850
+ 666307205,
1851
+ 773529912,
1852
+ 1294757372,
1853
+ 1396182291,
1854
+ 1695183700,
1855
+ 1986661051,
1856
+ 2177026350,
1857
+ 2456956037,
1858
+ 2730485921,
1859
+ 2820302411,
1860
+ 3259730800,
1861
+ 3345764771,
1862
+ 3516065817,
1863
+ 3600352804,
1864
+ 4094571909,
1865
+ 275423344,
1866
+ 430227734,
1867
+ 506948616,
1868
+ 659060556,
1869
+ 883997877,
1870
+ 958139571,
1871
+ 1322822218,
1872
+ 1537002063,
1873
+ 1747873779,
1874
+ 1955562222,
1875
+ 2024104815,
1876
+ 2227730452,
1877
+ 2361852424,
1878
+ 2428436474,
1879
+ 2756734187,
1880
+ 3204031479,
1881
+ 3329325298
1882
+ ]);
1883
+ var rotr = (x, n) => x >>> n | x << 32 - n;
1884
+ function sha256(message) {
1885
+ const H = new Uint32Array([
1886
+ 1779033703,
1887
+ 3144134277,
1888
+ 1013904242,
1889
+ 2773480762,
1890
+ 1359893119,
1891
+ 2600822924,
1892
+ 528734635,
1893
+ 1541459225
1894
+ ]);
1895
+ const length = message.length;
1896
+ const padded = new Uint8Array(length + 9 + 63 >> 6 << 6);
1897
+ padded.set(message);
1898
+ padded[length] = 128;
1899
+ const view = new DataView(padded.buffer);
1900
+ const bits = length * 8;
1901
+ view.setUint32(padded.length - 8, Math.floor(bits / 4294967296));
1902
+ view.setUint32(padded.length - 4, bits >>> 0);
1903
+ const W = new Uint32Array(64);
1904
+ for (let offset = 0; offset < padded.length; offset += 64) {
1905
+ for (let i = 0; i < 16; i++) W[i] = view.getUint32(offset + i * 4);
1906
+ for (let i = 16; i < 64; i++) {
1907
+ const w15 = W[i - 15];
1908
+ const w2 = W[i - 2];
1909
+ const s0 = rotr(w15, 7) ^ rotr(w15, 18) ^ w15 >>> 3;
1910
+ const s1 = rotr(w2, 17) ^ rotr(w2, 19) ^ w2 >>> 10;
1911
+ W[i] = W[i - 16] + s0 + W[i - 7] + s1 >>> 0;
1912
+ }
1913
+ let [a, b, c, d, e, f, g, h] = H;
1914
+ for (let i = 0; i < 64; i++) {
1915
+ const S1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
1916
+ const ch = e & f ^ ~e & g;
1917
+ const t1 = h + S1 + ch + K[i] + W[i] >>> 0;
1918
+ const S0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
1919
+ const maj = a & b ^ a & c ^ b & c;
1920
+ const t2 = S0 + maj >>> 0;
1921
+ h = g;
1922
+ g = f;
1923
+ f = e;
1924
+ e = d + t1 >>> 0;
1925
+ d = c;
1926
+ c = b;
1927
+ b = a;
1928
+ a = t1 + t2 >>> 0;
1929
+ }
1930
+ H[0] = H[0] + a >>> 0;
1931
+ H[1] = H[1] + b >>> 0;
1932
+ H[2] = H[2] + c >>> 0;
1933
+ H[3] = H[3] + d >>> 0;
1934
+ H[4] = H[4] + e >>> 0;
1935
+ H[5] = H[5] + f >>> 0;
1936
+ H[6] = H[6] + g >>> 0;
1937
+ H[7] = H[7] + h >>> 0;
1938
+ }
1939
+ const out = new Uint8Array(32);
1940
+ const outView = new DataView(out.buffer);
1941
+ for (let i = 0; i < 8; i++) outView.setUint32(i * 4, H[i]);
1942
+ return out;
1943
+ }
1794
1944
  function generateState() {
1795
1945
  const bytes = new Uint8Array(16);
1796
1946
  crypto.getRandomValues(bytes);
1797
1947
  return base64url(bytes);
1798
1948
  }
1949
+ var TOKEN_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
1950
+ function generateRequestId() {
1951
+ let out = "";
1952
+ const bytes = new Uint8Array(64);
1953
+ while (out.length < 32) {
1954
+ crypto.getRandomValues(bytes);
1955
+ for (const byte of bytes) {
1956
+ if (byte >= 248 || out.length === 32) continue;
1957
+ out += TOKEN_ALPHABET[byte % 62];
1958
+ }
1959
+ }
1960
+ return out;
1961
+ }
1799
1962
 
1800
1963
  // src/useZorealLogin.ts
1801
1964
  function useZorealFlow(options) {
@@ -1828,76 +1991,119 @@ function useZorealFlow(options) {
1828
1991
  const useAppLink = display === "link";
1829
1992
  const intent = resolveIntent(opts.intent, opts.scope, opts.acr_values);
1830
1993
  try {
1831
- const started = await startPairing(issuer, {
1832
- client_id: clientId,
1833
- scope: opts.scope ?? "openid",
1834
- state,
1835
- nonce,
1836
- code_challenge: await challengeS256(verifier),
1837
- redirect_uri: flow === "auth-code" ? opts.redirect_uri : void 0,
1838
- acr_values: Array.isArray(opts.acr_values) ? opts.acr_values.join(" ") : opts.acr_values,
1839
- max_age: opts.max_age,
1840
- prompt: opts.prompt,
1841
- locale,
1842
- display
1843
- });
1844
1994
  let code;
1845
1995
  let selectBy = "device";
1846
- if ("code" in started) {
1847
- code = started.code;
1848
- selectBy = "session";
1849
- } else {
1850
- selectBy = useAppLink ? "app_link" : "qr";
1851
- const qrRefreshSeconds = qrRefreshSecondsOf(started);
1996
+ if (useAppLink) {
1997
+ const requestId = generateRequestId();
1998
+ const startUrl = sameDeviceStartUrl(issuer, {
1999
+ client_id: clientId,
2000
+ scope: opts.scope ?? "openid",
2001
+ state,
2002
+ nonce,
2003
+ code_challenge: challengeS256Sync(verifier),
2004
+ redirect_uri: flow === "auth-code" ? opts.redirect_uri : void 0,
2005
+ acr_values: Array.isArray(opts.acr_values) ? opts.acr_values.join(" ") : opts.acr_values,
2006
+ max_age: opts.max_age,
2007
+ prompt: opts.prompt,
2008
+ locale,
2009
+ request_id: requestId,
2010
+ origin: window.location.origin
2011
+ });
2012
+ selectBy = "app_link";
1852
2013
  const cancel = () => {
1853
2014
  controller.abort();
1854
2015
  setPairing(null);
1855
- publishRef.current?.(null);
1856
2016
  };
1857
- const surface = {
1858
- pairUrl: started.pair_url,
1859
- appLink: useAppLink,
1860
- intent,
1861
- cancel,
1862
- // The app link has no QR and therefore no cadence to report.
1863
- ...useAppLink ? null : { qrRefreshSeconds }
1864
- };
1865
- let qrUrl = `${issuer}/pair/${encodeURIComponent(started.request_id)}/qr.svg`;
2017
+ const surface = { pairUrl: startUrl, appLink: true, intent, cancel };
1866
2018
  const active = {
1867
- requestId: started.request_id,
1868
- pairUrl: surface.pairUrl,
1869
- qrUrl,
1870
- state: { status: "pending", expiresIn: started.expires_in, qrUrl, ...surface },
1871
- appLink: useAppLink,
2019
+ requestId,
2020
+ pairUrl: startUrl,
2021
+ qrUrl: "",
2022
+ state: { status: "pending", ...surface },
2023
+ appLink: true,
1872
2024
  cancel
1873
2025
  };
1874
2026
  setPairing(active);
1875
- if (!useAppLink) {
1876
- publishRef.current?.({ state: active.state, qrUrl, intent, cancel });
1877
- }
1878
2027
  opts.onPairingStateChange?.(active.state);
1879
- if (useAppLink) {
1880
- window.location.assign(started.pair_url);
1881
- }
2028
+ window.location.assign(startUrl);
1882
2029
  code = await pollUntilApproved(
1883
2030
  issuer,
1884
- started.request_id,
2031
+ requestId,
1885
2032
  (s) => {
1886
- if (s.qrUrl) qrUrl = s.qrUrl;
1887
- const enriched = { ...s, ...surface, qrUrl };
1888
- setPairing(
1889
- (p) => p && p.requestId === started.request_id ? { ...p, qrUrl, state: enriched } : p
1890
- );
1891
- if (!useAppLink) {
1892
- publishRef.current?.({ state: enriched, qrUrl, intent, cancel });
1893
- }
2033
+ const enriched = { ...s, ...surface };
2034
+ setPairing((p) => p && p.requestId === requestId ? { ...p, state: enriched } : p);
1894
2035
  opts.onPairingStateChange?.(enriched);
1895
2036
  },
1896
2037
  controller.signal,
1897
- // The app link has no QR to animate, and the provider answers 404
1898
- // for one, so the frames are asked for only on the QR surface.
1899
- { qrRefreshSeconds: useAppLink ? void 0 : qrRefreshSeconds }
2038
+ { tolerateUnknownUntil: Date.now() + 15e3 }
1900
2039
  );
2040
+ } else {
2041
+ const started = await startPairing(issuer, {
2042
+ client_id: clientId,
2043
+ scope: opts.scope ?? "openid",
2044
+ state,
2045
+ nonce,
2046
+ code_challenge: await challengeS256(verifier),
2047
+ redirect_uri: flow === "auth-code" ? opts.redirect_uri : void 0,
2048
+ acr_values: Array.isArray(opts.acr_values) ? opts.acr_values.join(" ") : opts.acr_values,
2049
+ max_age: opts.max_age,
2050
+ prompt: opts.prompt,
2051
+ locale,
2052
+ display: "qr"
2053
+ });
2054
+ if ("code" in started) {
2055
+ code = started.code;
2056
+ selectBy = "session";
2057
+ } else {
2058
+ selectBy = "qr";
2059
+ const qrRefreshSeconds = qrRefreshSecondsOf(started);
2060
+ const cancel = () => {
2061
+ controller.abort();
2062
+ setPairing(null);
2063
+ publishRef.current?.(null);
2064
+ };
2065
+ const surface = {
2066
+ pairUrl: started.pair_url,
2067
+ appLink: false,
2068
+ intent,
2069
+ cancel,
2070
+ qrRefreshSeconds
2071
+ };
2072
+ let qrUrl = `${issuer}/pair/${encodeURIComponent(started.request_id)}/qr.svg`;
2073
+ const active = {
2074
+ requestId: started.request_id,
2075
+ pairUrl: surface.pairUrl,
2076
+ qrUrl,
2077
+ state: { status: "pending", expiresIn: started.expires_in, qrUrl, ...surface },
2078
+ appLink: false,
2079
+ cancel
2080
+ };
2081
+ setPairing(active);
2082
+ if (!useAppLink) {
2083
+ publishRef.current?.({ state: active.state, qrUrl, intent, cancel });
2084
+ }
2085
+ opts.onPairingStateChange?.(active.state);
2086
+ if (useAppLink) {
2087
+ window.location.assign(started.pair_url);
2088
+ }
2089
+ code = await pollUntilApproved(
2090
+ issuer,
2091
+ started.request_id,
2092
+ (s) => {
2093
+ if (s.qrUrl) qrUrl = s.qrUrl;
2094
+ const enriched = { ...s, ...surface, qrUrl };
2095
+ setPairing(
2096
+ (p) => p && p.requestId === started.request_id ? { ...p, qrUrl, state: enriched } : p
2097
+ );
2098
+ if (!useAppLink) {
2099
+ publishRef.current?.({ state: enriched, qrUrl, intent, cancel });
2100
+ }
2101
+ opts.onPairingStateChange?.(enriched);
2102
+ },
2103
+ controller.signal,
2104
+ { qrRefreshSeconds }
2105
+ );
2106
+ }
1901
2107
  }
1902
2108
  setPairing(null);
1903
2109
  publishRef.current?.(null);
@@ -1962,8 +2168,11 @@ function useZorealLogin(options) {
1962
2168
  }
1963
2169
 
1964
2170
  // src/ring.tsx
1965
- import { useEffect as useEffect3 } from "react";
2171
+ import { useEffect as useEffect3, useLayoutEffect, useRef as useRef3 } from "react";
1966
2172
  import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
2173
+ var TAIL = 0.3;
2174
+ var STACK = Array.from({ length: 12 }, (_, i) => 12 - i);
2175
+ var HALO = [3, 2, 1];
1967
2176
  function ZorealBusyRing({
1968
2177
  busy,
1969
2178
  radius = 8,
@@ -1976,11 +2185,42 @@ function ZorealBusyRing({
1976
2185
  useEffect3(() => {
1977
2186
  ensureStyles();
1978
2187
  }, []);
2188
+ const wrapRef = useRef3(null);
2189
+ const measureRef = useRef3(null);
2190
+ useLayoutEffect(() => {
2191
+ const wrap = wrapRef.current;
2192
+ const rect = measureRef.current;
2193
+ if (!wrap || !rect) return;
2194
+ const measure = () => {
2195
+ const length = rect.getTotalLength();
2196
+ if (length > 0) wrap.style.setProperty("--zrl-ring-len", `${length}px`);
2197
+ };
2198
+ measure();
2199
+ if (typeof ResizeObserver === "undefined") return;
2200
+ const observer = new ResizeObserver(measure);
2201
+ observer.observe(wrap);
2202
+ return () => observer.disconnect();
2203
+ }, [radius]);
1979
2204
  const rx = radius + 2;
1980
- const layer = (name) => /* @__PURE__ */ jsx5("rect", { className: cx(name), rx, ry: rx, pathLength: 100 });
2205
+ const layer = (key, name, len, alpha, ref) => /* @__PURE__ */ jsx5(
2206
+ "rect",
2207
+ {
2208
+ ref,
2209
+ className: cx(name),
2210
+ rx,
2211
+ ry: rx,
2212
+ style: {
2213
+ strokeDasharray: `calc(var(--zrl-l) * ${len}) calc(var(--zrl-l) * ${1 - len})`,
2214
+ "--zrl-s": `calc(var(--zrl-l) * ${-(TAIL - len)})`,
2215
+ opacity: alpha
2216
+ }
2217
+ },
2218
+ key
2219
+ );
1981
2220
  return /* @__PURE__ */ jsxs5(
1982
2221
  "span",
1983
2222
  {
2223
+ ref: wrapRef,
1984
2224
  className: className ? `${cx("root")} ${cx("ring")} ${className}` : `${cx("root")} ${cx("ring")}`,
1985
2225
  "data-theme": theme,
1986
2226
  "data-busy": busy,
@@ -1988,10 +2228,10 @@ function ZorealBusyRing({
1988
2228
  children: [
1989
2229
  children,
1990
2230
  /* @__PURE__ */ jsxs5("svg", { className: cx("ring-svg"), "aria-hidden": "true", children: [
1991
- layer("ring-halo"),
1992
- layer("ring-tail"),
1993
- layer("ring-body"),
1994
- layer("ring-head")
2231
+ HALO.map(
2232
+ (n, i) => layer(`h${n}`, "ring-halo", TAIL * n / HALO.length, `calc(var(--zrl-glow-opacity) * 0.6 / ${n})`, i === 0 ? measureRef : void 0)
2233
+ ),
2234
+ STACK.map((n) => layer(`t${n}`, n <= 2 ? "ring-head" : "ring-tail", TAIL * n / STACK.length, String(1 / n)))
1995
2235
  ] })
1996
2236
  ]
1997
2237
  }
@@ -2094,7 +2334,7 @@ function ZorealLogin(props) {
2094
2334
  }
2095
2335
 
2096
2336
  // src/useZorealAutoLogin.ts
2097
- import { useEffect as useEffect4, useRef as useRef3 } from "react";
2337
+ import { useEffect as useEffect4, useRef as useRef4 } from "react";
2098
2338
  function useZorealAutoLogin(options) {
2099
2339
  const { login } = useZorealFlow({
2100
2340
  flow: "browser-direct",
@@ -2111,7 +2351,7 @@ function useZorealAutoLogin(options) {
2111
2351
  },
2112
2352
  onNonOAuthError: (e) => options.onError?.(e)
2113
2353
  });
2114
- const fired = useRef3(false);
2354
+ const fired = useRef4(false);
2115
2355
  useEffect4(() => {
2116
2356
  if (options.disabled || fired.current) return;
2117
2357
  fired.current = true;