File size: 3,075 Bytes
dc47d57
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134ecb7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
dc47d57
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
import { useI18n } from '../i18n'
import type { MessageKey } from '../i18n/en'
import { isWebAuthnSupported } from '../auth/passkey'
import { Callout } from './ui'

type T = (key: MessageKey, vars?: Record<string, string | number>) => string

/** True when the app is rendered inside another frame — passkeys need the top-level origin. */
export function inEmbeddedFrame(): boolean {
  try {
    return window.self !== window.top
  } catch {
    return true
  }
}

export function statusLabel(t: T, status: string): string {
  const key = `d2.status.${status}` as MessageKey
  const label = t(key)
  return label === key ? status : label
}

export function roleLabel(t: T, role: string): string {
  const key = `d2.role.${role}` as MessageKey
  const label = t(key)
  return label === key ? role : label
}

/** Shows unsupported-browser and embedded-frame warnings for passkey flows. */
export function PasskeyEnvWarnings() {
  const { t } = useI18n()
  return (
    <>
      {!isWebAuthnSupported() && <Callout tone="warn">{t('d2.unsupported')}</Callout>}
      {inEmbeddedFrame() && <Callout tone="warn">{t('d2.iframeWarn')}</Callout>}
    </>
  )
}

/**
 * When the app is embedded in another site's iframe (e.g. the huggingface.co Spaces page),
 * browsers may block the session cookie and passkeys. Offer a one-tap break-out to the app's own
 * first-party tab, where everything works. Renders nothing when not embedded.
 */
export function OpenInNewTabBanner() {
  const { t } = useI18n()
  if (!inEmbeddedFrame()) return null
  const href = typeof window !== 'undefined' ? window.location.href : '#'
  return (
    <div className="callout tone-warn" role="note">
      <strong>{t('d2.openNewTab.title')}</strong>
      <p className="hint">{t('d2.openNewTab.body')}</p>
      <a className="btn" href={href} target="_blank" rel="noopener noreferrer">
        {t('d2.openNewTab.button')} ↗
      </a>
    </div>
  )
}

/** Accessible destructive-action confirmation. Focus lands on Cancel; Esc cancels. */
export function ConfirmDialog({
  message, confirmLabel, danger = true, busy = false, onConfirm, onCancel,
}: {
  message: string
  confirmLabel: string
  danger?: boolean
  busy?: boolean
  onConfirm: () => void
  onCancel: () => void
}) {
  const { t } = useI18n()
  return (
    <div className="d2-modal-backdrop" onClick={onCancel}>
      <div
        className="d2-modal card"
        role="alertdialog"
        aria-modal="true"
        aria-label={message}
        onClick={(e) => e.stopPropagation()}
        onKeyDown={(e) => {
          if (e.key === 'Escape') onCancel()
        }}
      >
        <p>{message}</p>
        <div className="row">
          <button type="button" className="btn" autoFocus onClick={onCancel} disabled={busy}>
            {t('d2.cancel')}
          </button>
          <button
            type="button"
            className={danger ? 'btn danger' : 'btn'}
            onClick={onConfirm}
            disabled={busy}
          >
            {confirmLabel}
          </button>
        </div>
      </div>
    </div>
  )
}