@coventia/softphone 0.2.0 → 0.3.0
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/{call-DUVnDxsz.d.cts → call-CaPDWVk3.d.cts} +50 -1
- package/dist/{call-DUVnDxsz.d.ts → call-CaPDWVk3.d.ts} +50 -1
- package/dist/{chunk-YWI6EQYS.js → chunk-EYEDPD7Y.js} +108 -15
- package/dist/chunk-EYEDPD7Y.js.map +1 -0
- package/dist/{chunk-LMJXX4YM.js → chunk-UHNLEBBO.js} +4 -2
- package/dist/chunk-UHNLEBBO.js.map +1 -0
- package/dist/element/index.d.ts +76 -11
- package/dist/element/index.js +1411 -478
- package/dist/element/index.js.map +1 -1
- package/dist/{engine-EPCKf-Bl.d.ts → engine-BgBsIcEa.d.ts} +1 -1
- package/dist/{engine-CDjkrXHY.d.cts → engine-CU2rEo3g.d.cts} +1 -1
- package/dist/index.cjs +318 -32
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.js +157 -19
- package/dist/index.js.map +1 -1
- package/dist/react/index.cjs +2 -0
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +2 -2
- package/dist/react/index.d.ts +2 -2
- package/dist/react/index.js +1 -1
- package/dist/{softphone-BtdgZWkm.d.ts → softphone-DEt0TtQw.d.ts} +26 -10
- package/dist/{softphone-DHG9MTNg.d.cts → softphone-dZMkW8V1.d.cts} +26 -10
- package/dist/softphone.css +192 -46
- package/dist/{sources-5L2R4HTV.js → sources-M3GYMS6V.js} +58 -3
- package/dist/sources-M3GYMS6V.js.map +1 -0
- package/dist/testing/index.d.cts +2 -2
- package/dist/testing/index.d.ts +2 -2
- package/dist/ui/index.cjs +1308 -366
- package/dist/ui/index.cjs.map +1 -1
- package/dist/ui/index.d.cts +40 -7
- package/dist/ui/index.d.ts +40 -7
- package/dist/ui/index.js +1308 -369
- package/dist/ui/index.js.map +1 -1
- package/dist/zadarma/index.cjs +106 -13
- package/dist/zadarma/index.cjs.map +1 -1
- package/dist/zadarma/index.d.cts +2 -2
- package/dist/zadarma/index.d.ts +2 -2
- package/dist/zadarma/index.js +1 -1
- package/dist/zadarma-engine-L5DEFXUS.js +5 -0
- package/dist/{zadarma-engine-4VOPIBXG.js.map → zadarma-engine-L5DEFXUS.js.map} +1 -1
- package/package.json +1 -1
- package/dist/chunk-LMJXX4YM.js.map +0 -1
- package/dist/chunk-YWI6EQYS.js.map +0 -1
- package/dist/sources-5L2R4HTV.js.map +0 -1
- package/dist/zadarma-engine-4VOPIBXG.js +0 -5
package/dist/react/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/react/context.tsx","../../src/react/hooks.ts"],"names":["createContext","useContext","useSyncExternalStore","useCallback","useRef"],"mappings":"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","file":"index.cjs","sourcesContent":["import { createContext, useContext, type ReactNode } from 'react';\r\nimport type { Softphone } from '../core/softphone.js';\r\n\r\n/**\r\n * The provider takes an INSTANCE the host constructed — not config props.\r\n *\r\n * This is LiveKit's hard-won lesson (SPEC §5.1): they are deprecating their config-props\r\n * `<LiveKitRoom>` in favour of a provider that accepts an instance. Holding the `Softphone` above the\r\n * React tree lets the app own the connection, survive route changes and unmounts without dropping a\r\n * call, and unit-test the engine with no React at all. Config-props would rebuild the engine on\r\n * every prop change — catastrophic for something holding a live `RTCPeerConnection`.\r\n */\r\nconst SoftphoneContext = createContext<Softphone | null>(null);\r\n\r\nexport interface SoftphoneProviderProps {\r\n readonly softphone: Softphone;\r\n readonly children: ReactNode;\r\n}\r\n\r\nexport function SoftphoneProvider({ softphone, children }: SoftphoneProviderProps) {\r\n return <SoftphoneContext.Provider value={softphone}>{children}</SoftphoneContext.Provider>;\r\n}\r\n\r\n/** The `Softphone` instance from context. Stable — does not re-render on state changes. */\r\nexport function useSoftphone(): Softphone {\r\n const softphone = useContext(SoftphoneContext);\r\n if (!softphone) {\r\n throw new Error('useSoftphone() must be used within a <SoftphoneProvider>.');\r\n }\r\n return softphone;\r\n}\r\n","// Built-in, no shim: the peer floor is React 18 (SPEC §2.7), which ships useSyncExternalStore.\r\nimport { useCallback, useRef, useSyncExternalStore } from 'react';\r\nimport { useSoftphone } from './context.js';\r\nimport { SoftphoneEvent, CallEvent } from '../core/events.js';\r\nimport type { Call, CallState } from '../core/call.js';\r\nimport type { PresenceEntry, RemoteParty, SoftphoneState } from '../core/types.js';\r\nimport type { AudioDevices } from '../core/softphone.js';\r\nimport type { FatalSoftphoneError } from '../core/errors.js';\r\n\r\n/** Reactive softphone lifecycle state (`stopped` → `online` → …). */\r\n/**\r\n * The reason the phone cannot currently be used, or `null`.\r\n *\r\n * Reads `softphone.lastError`, which is STATE rather than only an event — the widget is usually\r\n * opened *after* something broke, and a listener mounted at that point would have missed the event\r\n * entirely and rendered a dialpad with no explanation.\r\n *\r\n * Re-reads on both channels because both can change it: `Error` sets it, and `StateChanged` clears it\r\n * on reaching `online`. Subscribing to only the first would leave a stale gate on screen after the\r\n * phone recovered.\r\n */\r\nexport function useLastError(): FatalSoftphoneError | null {\r\n const phone = useSoftphone();\r\n return useSyncExternalStore(\r\n useCallback(\r\n (cb: () => void) => {\r\n const offError = phone.on(SoftphoneEvent.Error, () => cb());\r\n const offState = phone.on(SoftphoneEvent.StateChanged, () => cb());\r\n return () => {\r\n offError();\r\n offState();\r\n };\r\n },\r\n [phone],\r\n ),\r\n useCallback(() => phone.lastError, [phone]),\r\n );\r\n}\r\n\r\nexport function useSoftphoneState(): SoftphoneState {\r\n const phone = useSoftphone();\r\n return useSyncExternalStore(\r\n useCallback((cb: () => void) => phone.on(SoftphoneEvent.StateChanged, () => cb()), [phone]),\r\n useCallback(() => phone.state, [phone]),\r\n );\r\n}\r\n\r\n/**\r\n * Whether THIS tab is the one allowed to be reachable (`multi-tab-leader-election.md`). `false` by\r\n * default and while waiting for another open tab to release it — show your own \"active in another\r\n * tab\" state for that duration; the `/ui` widget already does.\r\n */\r\nexport function useIsLeader(): boolean {\r\n const phone = useSoftphone();\r\n return useSyncExternalStore(\r\n useCallback((cb: () => void) => phone.on(SoftphoneEvent.LeaderChanged, () => cb()), [phone]),\r\n useCallback(() => phone.isLeader, [phone]),\r\n );\r\n}\r\n\r\n/**\r\n * The current call, or null. Re-renders when it changes — inbound, outbound, or ended.\r\n *\r\n * `phone.activeCall` returns a stable reference for a given call, so it is a safe snapshot.\r\n */\r\nexport function useActiveCall(): Call | null {\r\n const phone = useSoftphone();\r\n return useSyncExternalStore(\r\n useCallback((cb: () => void) => phone.on(SoftphoneEvent.ActiveCallChanged, () => cb()), [phone]),\r\n useCallback(() => phone.activeCall, [phone]),\r\n );\r\n}\r\n\r\n/**\r\n * The live agent-presence list (`phone.presence`, Phase B) — reactive on `SoftphoneEvent.\r\n * PresenceChanged`. `active` gates the SUBSCRIPTION only, not the underlying connection: the socket\r\n * itself stays governed entirely by `online && isLeader` inside the softphone (spec's own\r\n * always-connected decision), so passing `false` here does not close it — it only stops this\r\n * particular component from re-rendering on every update while it has no reason to care, e.g. the\r\n * transfer step being closed (spec §4.5: \"subscribe when the step opens... unsubscribe when it\r\n * closes\"). `phone.presence` is still read fresh on every call, so flipping `active` back to `true`\r\n * never shows a stale snapshot.\r\n */\r\nexport function usePresence(active: boolean): readonly PresenceEntry[] {\r\n const phone = useSoftphone();\r\n return useSyncExternalStore(\r\n useCallback(\r\n (cb: () => void) => {\r\n if (!active) return () => {};\r\n return phone.on(SoftphoneEvent.PresenceChanged, () => cb());\r\n },\r\n [phone, active],\r\n ),\r\n useCallback(() => phone.presence, [phone]),\r\n );\r\n}\r\n\r\n/** An immutable snapshot of a call's reactive fields. */\r\nexport interface CallView {\r\n readonly state: CallState;\r\n readonly isMuted: boolean;\r\n readonly isOnHold: boolean;\r\n readonly remote: RemoteParty;\r\n readonly remoteStream: MediaStream | null;\r\n readonly answeredAt: Date | null;\r\n}\r\n\r\nconst CALL_EVENTS: readonly CallEvent[] = [\r\n CallEvent.StateChanged,\r\n CallEvent.Muted,\r\n CallEvent.Held,\r\n CallEvent.Unheld,\r\n CallEvent.RemotePartyUpdated,\r\n CallEvent.RemoteStream,\r\n CallEvent.Ended,\r\n];\r\n\r\n/**\r\n * Reactive view of a specific call. Re-renders only when one of the call's own events fires — no\r\n * polling, no re-render storms.\r\n *\r\n * The snapshot is CACHED and invalidated only on an event, so `useSyncExternalStore` gets a stable\r\n * reference between changes (returning a fresh object every render would infinite-loop).\r\n */\r\nexport function useCall(call: Call | null): CallView | null {\r\n const cache = useRef<{ call: Call; view: CallView } | null>(null);\r\n\r\n const subscribe = useCallback(\r\n (cb: () => void) => {\r\n if (!call) return () => {};\r\n const unsubs = CALL_EVENTS.map((event) =>\r\n call.on(event, () => {\r\n cache.current = null; // invalidate; getSnapshot rebuilds on the next read\r\n cb();\r\n }),\r\n );\r\n return () => {\r\n for (const off of unsubs) off();\r\n };\r\n },\r\n [call],\r\n );\r\n\r\n const getSnapshot = useCallback((): CallView | null => {\r\n if (!call) return null;\r\n if (cache.current?.call === call) return cache.current.view;\r\n const view: CallView = {\r\n state: call.state,\r\n isMuted: call.isMuted,\r\n isOnHold: call.isOnHold,\r\n remote: call.remote,\r\n remoteStream: call.remoteStream,\r\n answeredAt: call.answeredAt,\r\n };\r\n cache.current = { call, view };\r\n return view;\r\n }, [call]);\r\n\r\n return useSyncExternalStore(subscribe, getSnapshot);\r\n}\r\n\r\n/**\r\n * The audio device surface (`phone.audio`) — reactive. Re-renders on `SoftphoneEvent.DeviceChange`: a\r\n * device plugged/unplugged, or a selection changed (SPEC §7 Phase 2).\r\n *\r\n * The snapshot is CACHED and invalidated only on that event, same reasoning as `useCall`'s own\r\n * `CallView`: `phone.audio` is ONE stable object whose fields mutate in place, so returning it\r\n * directly would never change by reference and `useSyncExternalStore` would never re-render. A fresh\r\n * object, rebuilt only when something actually changed, is what makes this reactive without polling.\r\n */\r\nexport function useDevices(): AudioDevices {\r\n const phone = useSoftphone();\r\n const cache = useRef<{ audio: AudioDevices; view: AudioDevices } | null>(null);\r\n\r\n const subscribe = useCallback(\r\n (cb: () => void) =>\r\n phone.on(SoftphoneEvent.DeviceChange, () => {\r\n cache.current = null; // invalidate; getSnapshot rebuilds on the next read\r\n cb();\r\n }),\r\n [phone],\r\n );\r\n\r\n const getSnapshot = useCallback((): AudioDevices => {\r\n const audio = phone.audio;\r\n if (cache.current?.audio === audio) return cache.current.view;\r\n const view: AudioDevices = {\r\n inputs: audio.inputs,\r\n outputs: audio.outputs,\r\n currentInputId: audio.currentInputId,\r\n currentOutputId: audio.currentOutputId,\r\n canSelectOutput: audio.canSelectOutput,\r\n setInput: (deviceId) => audio.setInput(deviceId),\r\n setOutput: (deviceId) => audio.setOutput(deviceId),\r\n setRingtoneOutput: (deviceId) => audio.setRingtoneOutput(deviceId),\r\n refresh: () => audio.refresh(),\r\n };\r\n cache.current = { audio, view };\r\n return view;\r\n }, [phone]);\r\n\r\n return useSyncExternalStore(subscribe, getSnapshot);\r\n}\r\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/react/context.tsx","../../src/react/hooks.ts"],"names":["createContext","useContext","useSyncExternalStore","useCallback","useRef"],"mappings":"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","file":"index.cjs","sourcesContent":["import { createContext, useContext, type ReactNode } from 'react';\r\nimport type { Softphone } from '../core/softphone.js';\r\n\r\n/**\r\n * The provider takes an INSTANCE the host constructed — not config props.\r\n *\r\n * This is LiveKit's hard-won lesson (SPEC §5.1): they are deprecating their config-props\r\n * `<LiveKitRoom>` in favour of a provider that accepts an instance. Holding the `Softphone` above the\r\n * React tree lets the app own the connection, survive route changes and unmounts without dropping a\r\n * call, and unit-test the engine with no React at all. Config-props would rebuild the engine on\r\n * every prop change — catastrophic for something holding a live `RTCPeerConnection`.\r\n */\r\nconst SoftphoneContext = createContext<Softphone | null>(null);\r\n\r\nexport interface SoftphoneProviderProps {\r\n readonly softphone: Softphone;\r\n readonly children: ReactNode;\r\n}\r\n\r\nexport function SoftphoneProvider({ softphone, children }: SoftphoneProviderProps) {\r\n return <SoftphoneContext.Provider value={softphone}>{children}</SoftphoneContext.Provider>;\r\n}\r\n\r\n/** The `Softphone` instance from context. Stable — does not re-render on state changes. */\r\nexport function useSoftphone(): Softphone {\r\n const softphone = useContext(SoftphoneContext);\r\n if (!softphone) {\r\n throw new Error('useSoftphone() must be used within a <SoftphoneProvider>.');\r\n }\r\n return softphone;\r\n}\r\n","// Built-in, no shim: the peer floor is React 18 (SPEC §2.7), which ships useSyncExternalStore.\r\nimport { useCallback, useRef, useSyncExternalStore } from 'react';\r\nimport { useSoftphone } from './context.js';\r\nimport { SoftphoneEvent, CallEvent } from '../core/events.js';\r\nimport type { Call, CallState } from '../core/call.js';\r\nimport type { PresenceEntry, RemoteParty, SoftphoneState } from '../core/types.js';\r\nimport type { AudioDevices } from '../core/softphone.js';\r\nimport type { FatalSoftphoneError } from '../core/errors.js';\r\n\r\n/** Reactive softphone lifecycle state (`stopped` → `online` → …). */\r\n/**\r\n * The reason the phone cannot currently be used, or `null`.\r\n *\r\n * Reads `softphone.lastError`, which is STATE rather than only an event — the widget is usually\r\n * opened *after* something broke, and a listener mounted at that point would have missed the event\r\n * entirely and rendered a dialpad with no explanation.\r\n *\r\n * Re-reads on both channels because both can change it: `Error` sets it, and `StateChanged` clears it\r\n * on reaching `online`. Subscribing to only the first would leave a stale gate on screen after the\r\n * phone recovered.\r\n */\r\nexport function useLastError(): FatalSoftphoneError | null {\r\n const phone = useSoftphone();\r\n return useSyncExternalStore(\r\n useCallback(\r\n (cb: () => void) => {\r\n const offError = phone.on(SoftphoneEvent.Error, () => cb());\r\n const offState = phone.on(SoftphoneEvent.StateChanged, () => cb());\r\n return () => {\r\n offError();\r\n offState();\r\n };\r\n },\r\n [phone],\r\n ),\r\n useCallback(() => phone.lastError, [phone]),\r\n );\r\n}\r\n\r\nexport function useSoftphoneState(): SoftphoneState {\r\n const phone = useSoftphone();\r\n return useSyncExternalStore(\r\n useCallback((cb: () => void) => phone.on(SoftphoneEvent.StateChanged, () => cb()), [phone]),\r\n useCallback(() => phone.state, [phone]),\r\n );\r\n}\r\n\r\n/**\r\n * Whether THIS tab is the one allowed to be reachable (`multi-tab-leader-election.md`). `false` by\r\n * default and while waiting for another open tab to release it — show your own \"active in another\r\n * tab\" state for that duration; the `/ui` widget already does.\r\n */\r\nexport function useIsLeader(): boolean {\r\n const phone = useSoftphone();\r\n return useSyncExternalStore(\r\n useCallback((cb: () => void) => phone.on(SoftphoneEvent.LeaderChanged, () => cb()), [phone]),\r\n useCallback(() => phone.isLeader, [phone]),\r\n );\r\n}\r\n\r\n/**\r\n * The current call, or null. Re-renders when it changes — inbound, outbound, or ended.\r\n *\r\n * `phone.activeCall` returns a stable reference for a given call, so it is a safe snapshot.\r\n */\r\nexport function useActiveCall(): Call | null {\r\n const phone = useSoftphone();\r\n return useSyncExternalStore(\r\n useCallback((cb: () => void) => phone.on(SoftphoneEvent.ActiveCallChanged, () => cb()), [phone]),\r\n useCallback(() => phone.activeCall, [phone]),\r\n );\r\n}\r\n\r\n/**\r\n * The live agent-presence list (`phone.presence`, Phase B) — reactive on `SoftphoneEvent.\r\n * PresenceChanged`. `active` gates the SUBSCRIPTION only, not the underlying connection: the socket\r\n * itself stays governed entirely by `online && isLeader` inside the softphone (spec's own\r\n * always-connected decision), so passing `false` here does not close it — it only stops this\r\n * particular component from re-rendering on every update while it has no reason to care, e.g. the\r\n * transfer step being closed (spec §4.5: \"subscribe when the step opens... unsubscribe when it\r\n * closes\"). `phone.presence` is still read fresh on every call, so flipping `active` back to `true`\r\n * never shows a stale snapshot.\r\n */\r\nexport function usePresence(active: boolean): readonly PresenceEntry[] {\r\n const phone = useSoftphone();\r\n return useSyncExternalStore(\r\n useCallback(\r\n (cb: () => void) => {\r\n if (!active) return () => {};\r\n return phone.on(SoftphoneEvent.PresenceChanged, () => cb());\r\n },\r\n [phone, active],\r\n ),\r\n useCallback(() => phone.presence, [phone]),\r\n );\r\n}\r\n\r\n/** An immutable snapshot of a call's reactive fields. */\r\nexport interface CallView {\r\n readonly state: CallState;\r\n readonly isMuted: boolean;\r\n readonly isOnHold: boolean;\r\n readonly remote: RemoteParty;\r\n readonly remoteStream: MediaStream | null;\r\n readonly answeredAt: Date | null;\r\n}\r\n\r\nconst CALL_EVENTS: readonly CallEvent[] = [\r\n CallEvent.StateChanged,\r\n CallEvent.Muted,\r\n CallEvent.Held,\r\n CallEvent.Unheld,\r\n CallEvent.RemotePartyUpdated,\r\n CallEvent.RemoteStream,\r\n CallEvent.Ended,\r\n];\r\n\r\n/**\r\n * Reactive view of a specific call. Re-renders only when one of the call's own events fires — no\r\n * polling, no re-render storms.\r\n *\r\n * The snapshot is CACHED and invalidated only on an event, so `useSyncExternalStore` gets a stable\r\n * reference between changes (returning a fresh object every render would infinite-loop).\r\n */\r\nexport function useCall(call: Call | null): CallView | null {\r\n const cache = useRef<{ call: Call; view: CallView } | null>(null);\r\n\r\n const subscribe = useCallback(\r\n (cb: () => void) => {\r\n if (!call) return () => {};\r\n const unsubs = CALL_EVENTS.map((event) =>\r\n call.on(event, () => {\r\n cache.current = null; // invalidate; getSnapshot rebuilds on the next read\r\n cb();\r\n }),\r\n );\r\n return () => {\r\n for (const off of unsubs) off();\r\n };\r\n },\r\n [call],\r\n );\r\n\r\n const getSnapshot = useCallback((): CallView | null => {\r\n if (!call) return null;\r\n if (cache.current?.call === call) return cache.current.view;\r\n const view: CallView = {\r\n state: call.state,\r\n isMuted: call.isMuted,\r\n isOnHold: call.isOnHold,\r\n remote: call.remote,\r\n remoteStream: call.remoteStream,\r\n answeredAt: call.answeredAt,\r\n };\r\n cache.current = { call, view };\r\n return view;\r\n }, [call]);\r\n\r\n return useSyncExternalStore(subscribe, getSnapshot);\r\n}\r\n\r\n/**\r\n * The audio device surface (`phone.audio`) — reactive. Re-renders on `SoftphoneEvent.DeviceChange`: a\r\n * device plugged/unplugged, or a selection changed (SPEC §7 Phase 2).\r\n *\r\n * The snapshot is CACHED and invalidated only on that event, same reasoning as `useCall`'s own\r\n * `CallView`: `phone.audio` is ONE stable object whose fields mutate in place, so returning it\r\n * directly would never change by reference and `useSyncExternalStore` would never re-render. A fresh\r\n * object, rebuilt only when something actually changed, is what makes this reactive without polling.\r\n */\r\nexport function useDevices(): AudioDevices {\r\n const phone = useSoftphone();\r\n const cache = useRef<{ audio: AudioDevices; view: AudioDevices } | null>(null);\r\n\r\n const subscribe = useCallback(\r\n (cb: () => void) =>\r\n phone.on(SoftphoneEvent.DeviceChange, () => {\r\n cache.current = null; // invalidate; getSnapshot rebuilds on the next read\r\n cb();\r\n }),\r\n [phone],\r\n );\r\n\r\n const getSnapshot = useCallback((): AudioDevices => {\r\n const audio = phone.audio;\r\n if (cache.current?.audio === audio) return cache.current.view;\r\n const view: AudioDevices = {\r\n inputs: audio.inputs,\r\n outputs: audio.outputs,\r\n currentInputId: audio.currentInputId,\r\n currentOutputId: audio.currentOutputId,\r\n currentRingtoneOutputId: audio.currentRingtoneOutputId,\r\n canSelectOutput: audio.canSelectOutput,\r\n canSelectRingtoneOutput: audio.canSelectRingtoneOutput,\r\n setInput: (deviceId) => audio.setInput(deviceId),\r\n setOutput: (deviceId) => audio.setOutput(deviceId),\r\n setRingtoneOutput: (deviceId) => audio.setRingtoneOutput(deviceId),\r\n refresh: () => audio.refresh(),\r\n };\r\n cache.current = { audio, view };\r\n return view;\r\n }, [phone]);\r\n\r\n return useSyncExternalStore(subscribe, getSnapshot);\r\n}\r\n"]}
|
package/dist/react/index.d.cts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { ReactNode } from 'react';
|
|
3
|
-
import { S as Softphone, A as AudioDevices } from '../softphone-
|
|
4
|
-
import { f as CallState, R as RemoteParty, a as Call, h as FatalSoftphoneError, P as PresenceEntry,
|
|
3
|
+
import { S as Softphone, A as AudioDevices } from '../softphone-dZMkW8V1.cjs';
|
|
4
|
+
import { f as CallState, R as RemoteParty, a as Call, h as FatalSoftphoneError, P as PresenceEntry, r as SoftphoneState } from '../call-CaPDWVk3.cjs';
|
|
5
5
|
|
|
6
6
|
interface SoftphoneProviderProps {
|
|
7
7
|
readonly softphone: Softphone;
|
package/dist/react/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { ReactNode } from 'react';
|
|
3
|
-
import { S as Softphone, A as AudioDevices } from '../softphone-
|
|
4
|
-
import { f as CallState, R as RemoteParty, a as Call, h as FatalSoftphoneError, P as PresenceEntry,
|
|
3
|
+
import { S as Softphone, A as AudioDevices } from '../softphone-DEt0TtQw.js';
|
|
4
|
+
import { f as CallState, R as RemoteParty, a as Call, h as FatalSoftphoneError, P as PresenceEntry, r as SoftphoneState } from '../call-CaPDWVk3.js';
|
|
5
5
|
|
|
6
6
|
interface SoftphoneProviderProps {
|
|
7
7
|
readonly softphone: Softphone;
|
package/dist/react/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { SoftphoneProvider, useActiveCall, useCall, useDevices, useIsLeader, useLastError, usePresence, useSoftphone, useSoftphoneState } from '../chunk-
|
|
1
|
+
export { SoftphoneProvider, useActiveCall, useCall, useDevices, useIsLeader, useLastError, usePresence, useSoftphone, useSoftphoneState } from '../chunk-UHNLEBBO.js';
|
|
2
2
|
import '../chunk-7R6LOOAI.js';
|
|
3
3
|
//# sourceMappingURL=index.js.map
|
|
4
4
|
//# sourceMappingURL=index.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { x as ContactsSource, y as HistorySource, g as Contact, A as AgentsDirectory, z as TransferOutcomesPage, B as PresenceSource, m as RingtoneSource, t as TypedEmitter, q as SoftphoneEventMap, r as SoftphoneState, h as FatalSoftphoneError, e as CallOptions, a as Call, P as PresenceEntry } from './call-CaPDWVk3.js';
|
|
2
2
|
|
|
3
|
-
/** Contacts, history, caller ID, the agents directory, transfer outcomes
|
|
4
|
-
* config. */
|
|
3
|
+
/** Contacts, history, caller ID, the agents directory, transfer outcomes, presence and ringtone
|
|
4
|
+
* settings, all from one config. */
|
|
5
5
|
interface BackendSources {
|
|
6
6
|
readonly contacts: ContactsSource;
|
|
7
7
|
readonly history: HistorySource;
|
|
@@ -14,6 +14,8 @@ interface BackendSources {
|
|
|
14
14
|
}) => Promise<TransferOutcomesPage>;
|
|
15
15
|
/** Phase B (presence-gated transfer picker) — opened by `presence-socket.ts`, never by UI directly. */
|
|
16
16
|
readonly presence: PresenceSource;
|
|
17
|
+
/** The caller's ringtone settings (custom-ringtone.md §4.4). Always Coventia's — no override. */
|
|
18
|
+
readonly ringtones: RingtoneSource;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
/**
|
|
@@ -25,12 +27,20 @@ interface AudioDevices {
|
|
|
25
27
|
readonly inputs: readonly MediaDeviceInfo[];
|
|
26
28
|
readonly outputs: readonly MediaDeviceInfo[];
|
|
27
29
|
/**
|
|
28
|
-
* The currently-selected input/output, or `null` — no explicit choice made, the
|
|
29
|
-
* default is in use. Never a stale ID: if the selected device is unplugged, its
|
|
30
|
-
*
|
|
30
|
+
* The currently-selected input/output/ringtone output, or `null` — no explicit choice made, the
|
|
31
|
+
* browser's own default is in use. Never a stale ID: if the selected device is unplugged, its
|
|
32
|
+
* selection reads `null` rather than pointing at a device that no longer exists.
|
|
33
|
+
*
|
|
34
|
+
* **Choices are remembered** — in `localStorage` (`coventia-softphone:devices`), so they survive a
|
|
35
|
+
* reload. What is remembered is the user's *preference*, kept apart from these current values: an
|
|
36
|
+
* unplugged headset reads `null` here, and reads as selected again the moment it is plugged back in,
|
|
37
|
+
* without the user choosing it twice. Device ids are per browser and per site, which is exactly the
|
|
38
|
+
* scope of that storage (custom-ringtone.md §4.5). Where storage is blocked (private mode, a
|
|
39
|
+
* sandboxed iframe) choices simply last for the page, as they used to.
|
|
31
40
|
*/
|
|
32
41
|
readonly currentInputId: string | null;
|
|
33
42
|
readonly currentOutputId: string | null;
|
|
43
|
+
readonly currentRingtoneOutputId: string | null;
|
|
34
44
|
/**
|
|
35
45
|
* Re-enumerate. `inputs`/`outputs` already stay current on their own — a device physically
|
|
36
46
|
* plugged or unplugged fires the browser's own `devicechange` and needs no help — but a
|
|
@@ -55,17 +65,23 @@ interface AudioDevices {
|
|
|
55
65
|
*/
|
|
56
66
|
setOutput(deviceId: string | null): Promise<void>;
|
|
57
67
|
/**
|
|
58
|
-
* The RINGTONE output, separate from the call speaker.
|
|
68
|
+
* The RINGTONE output, separate from the call speaker, or `null` for the browser's default device.
|
|
59
69
|
*
|
|
60
70
|
* Of every SDK surveyed only Twilio ships this, and users want it: ring on the laptop speakers so
|
|
61
|
-
* you hear it across the room, take the call in the headset.
|
|
71
|
+
* you hear it across the room, take the call in the headset. Only the inbound ring (and its preview
|
|
72
|
+
* in Ajustes) follows it; the call itself, the ringback and the short cues stay where they were.
|
|
62
73
|
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
74
|
+
* Like `setOutput`, this only records the choice and fires `SoftphoneEvent.DeviceChange` — the `/ui`
|
|
75
|
+
* layer is what routes the ring. A no-op for routing when `canSelectRingtoneOutput` is false.
|
|
65
76
|
*/
|
|
66
77
|
setRingtoneOutput(deviceId: string | null): Promise<void>;
|
|
67
78
|
/** False when the browser lacks `setSinkId` (Safari, most of Firefox) — `setOutput` is then a no-op. */
|
|
68
79
|
readonly canSelectOutput: boolean;
|
|
80
|
+
/**
|
|
81
|
+
* Whether the ring can be sent to a chosen device. False on Safari (no `setSinkId` at all) — the
|
|
82
|
+
* ring then plays on the default output and a UI should not offer the picker.
|
|
83
|
+
*/
|
|
84
|
+
readonly canSelectRingtoneOutput: boolean;
|
|
69
85
|
}
|
|
70
86
|
/**
|
|
71
87
|
* The engine. Long-lived; owns registration, credentials, devices, and the provider.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { x as ContactsSource, y as HistorySource, g as Contact, A as AgentsDirectory, z as TransferOutcomesPage, B as PresenceSource, m as RingtoneSource, t as TypedEmitter, q as SoftphoneEventMap, r as SoftphoneState, h as FatalSoftphoneError, e as CallOptions, a as Call, P as PresenceEntry } from './call-CaPDWVk3.cjs';
|
|
2
2
|
|
|
3
|
-
/** Contacts, history, caller ID, the agents directory, transfer outcomes
|
|
4
|
-
* config. */
|
|
3
|
+
/** Contacts, history, caller ID, the agents directory, transfer outcomes, presence and ringtone
|
|
4
|
+
* settings, all from one config. */
|
|
5
5
|
interface BackendSources {
|
|
6
6
|
readonly contacts: ContactsSource;
|
|
7
7
|
readonly history: HistorySource;
|
|
@@ -14,6 +14,8 @@ interface BackendSources {
|
|
|
14
14
|
}) => Promise<TransferOutcomesPage>;
|
|
15
15
|
/** Phase B (presence-gated transfer picker) — opened by `presence-socket.ts`, never by UI directly. */
|
|
16
16
|
readonly presence: PresenceSource;
|
|
17
|
+
/** The caller's ringtone settings (custom-ringtone.md §4.4). Always Coventia's — no override. */
|
|
18
|
+
readonly ringtones: RingtoneSource;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
/**
|
|
@@ -25,12 +27,20 @@ interface AudioDevices {
|
|
|
25
27
|
readonly inputs: readonly MediaDeviceInfo[];
|
|
26
28
|
readonly outputs: readonly MediaDeviceInfo[];
|
|
27
29
|
/**
|
|
28
|
-
* The currently-selected input/output, or `null` — no explicit choice made, the
|
|
29
|
-
* default is in use. Never a stale ID: if the selected device is unplugged, its
|
|
30
|
-
*
|
|
30
|
+
* The currently-selected input/output/ringtone output, or `null` — no explicit choice made, the
|
|
31
|
+
* browser's own default is in use. Never a stale ID: if the selected device is unplugged, its
|
|
32
|
+
* selection reads `null` rather than pointing at a device that no longer exists.
|
|
33
|
+
*
|
|
34
|
+
* **Choices are remembered** — in `localStorage` (`coventia-softphone:devices`), so they survive a
|
|
35
|
+
* reload. What is remembered is the user's *preference*, kept apart from these current values: an
|
|
36
|
+
* unplugged headset reads `null` here, and reads as selected again the moment it is plugged back in,
|
|
37
|
+
* without the user choosing it twice. Device ids are per browser and per site, which is exactly the
|
|
38
|
+
* scope of that storage (custom-ringtone.md §4.5). Where storage is blocked (private mode, a
|
|
39
|
+
* sandboxed iframe) choices simply last for the page, as they used to.
|
|
31
40
|
*/
|
|
32
41
|
readonly currentInputId: string | null;
|
|
33
42
|
readonly currentOutputId: string | null;
|
|
43
|
+
readonly currentRingtoneOutputId: string | null;
|
|
34
44
|
/**
|
|
35
45
|
* Re-enumerate. `inputs`/`outputs` already stay current on their own — a device physically
|
|
36
46
|
* plugged or unplugged fires the browser's own `devicechange` and needs no help — but a
|
|
@@ -55,17 +65,23 @@ interface AudioDevices {
|
|
|
55
65
|
*/
|
|
56
66
|
setOutput(deviceId: string | null): Promise<void>;
|
|
57
67
|
/**
|
|
58
|
-
* The RINGTONE output, separate from the call speaker.
|
|
68
|
+
* The RINGTONE output, separate from the call speaker, or `null` for the browser's default device.
|
|
59
69
|
*
|
|
60
70
|
* Of every SDK surveyed only Twilio ships this, and users want it: ring on the laptop speakers so
|
|
61
|
-
* you hear it across the room, take the call in the headset.
|
|
71
|
+
* you hear it across the room, take the call in the headset. Only the inbound ring (and its preview
|
|
72
|
+
* in Ajustes) follows it; the call itself, the ringback and the short cues stay where they were.
|
|
62
73
|
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
74
|
+
* Like `setOutput`, this only records the choice and fires `SoftphoneEvent.DeviceChange` — the `/ui`
|
|
75
|
+
* layer is what routes the ring. A no-op for routing when `canSelectRingtoneOutput` is false.
|
|
65
76
|
*/
|
|
66
77
|
setRingtoneOutput(deviceId: string | null): Promise<void>;
|
|
67
78
|
/** False when the browser lacks `setSinkId` (Safari, most of Firefox) — `setOutput` is then a no-op. */
|
|
68
79
|
readonly canSelectOutput: boolean;
|
|
80
|
+
/**
|
|
81
|
+
* Whether the ring can be sent to a chosen device. False on Safari (no `setSinkId` at all) — the
|
|
82
|
+
* ring then plays on the default output and a UI should not offer the picker.
|
|
83
|
+
*/
|
|
84
|
+
readonly canSelectRingtoneOutput: boolean;
|
|
69
85
|
}
|
|
70
86
|
/**
|
|
71
87
|
* The engine. Long-lived; owns registration, credentials, devices, and the provider.
|
package/dist/softphone.css
CHANGED
|
@@ -240,8 +240,6 @@
|
|
|
240
240
|
.cv-dial-prefix { padding: 0.7rem 0.6rem 0; }
|
|
241
241
|
.cv-dial-head { padding: 0.3rem 0.6rem 0.6rem; }
|
|
242
242
|
.cv-display { font-size: 1.5rem; }
|
|
243
|
-
.cv-controls-row { gap: 1.1rem; }
|
|
244
|
-
|
|
245
243
|
}
|
|
246
244
|
|
|
247
245
|
/* Short hosts. The flex chain already lets the keypad absorb what is left, but below this the keys
|
|
@@ -720,7 +718,7 @@
|
|
|
720
718
|
}
|
|
721
719
|
.cv-badge-hold { color: var(--cv-hold); font-weight: 600; }
|
|
722
720
|
|
|
723
|
-
/* ──
|
|
721
|
+
/* ── Device check (ringing / dialing / in-call) ────────────────────────────────
|
|
724
722
|
Before a call connects there is only ONE other control on screen (Colgar, or
|
|
725
723
|
Rechazar/Contestar) — putting Dispositivos in that same neighbourhood, even
|
|
726
724
|
spaced out, keeps it a mis-tap away from hanging up or answering. The entry
|
|
@@ -736,6 +734,11 @@
|
|
|
736
734
|
cramped and scrolly for just two rows. `.cv-call-devices-inline` REPLACES the
|
|
737
735
|
avatar/name/status block in `.cv-call` instead (see that JSX's own comment) —
|
|
738
736
|
an inline swap, not an overlay, so it gets the full box, not a leftover sliver.
|
|
737
|
+
|
|
738
|
+
Once connected it is the SAME chip and the SAME swap. Dispositivos used to be a fifth control in
|
|
739
|
+
the call row (with a floating popover), which is what forced that row down to 40px buttons and
|
|
740
|
+
9px labels in a 310px host. A set-once setting does not need a slot among the four things you do
|
|
741
|
+
mid-call (mute, hold, transfer, keypad), and the control row stays live under the swap.
|
|
739
742
|
───────────────────────────────────────────────────────────────────────────── */
|
|
740
743
|
.cv-call-devices-chip {
|
|
741
744
|
display: inline-flex;
|
|
@@ -808,16 +811,22 @@
|
|
|
808
811
|
display: flex;
|
|
809
812
|
flex-direction: column;
|
|
810
813
|
align-items: center;
|
|
811
|
-
gap: 0.
|
|
814
|
+
gap: 0.9rem;
|
|
812
815
|
padding: 0.35rem 1rem 1.1rem;
|
|
813
|
-
/* The containing block for `.cv-
|
|
816
|
+
/* The containing block for `.cv-keypad-popover` — see that rule's own comment for why it is
|
|
814
817
|
anchored here rather than to the trigger button. */
|
|
815
818
|
position: relative;
|
|
816
819
|
}
|
|
820
|
+
/* FOUR controls, sized so they never need to shrink to fit. This row held five (Dispositivos too) and
|
|
821
|
+
the only way to fit five in the CRM's 310px host was a 40px button with a 9px label and ~6px gaps —
|
|
822
|
+
below the 44/48px a thumb needs, and below legible for a word read mid-call. The default 48px size
|
|
823
|
+
below was never actually seen: it only applied above a 360px host, which no real embed is.
|
|
824
|
+
Dispositivos is the `.cv-call-chips` chip up by the call info instead (the same entry point the
|
|
825
|
+
ringing and dialing screens already use). 4 × 52 + 3 × 20 = 268px, inside 310 - 2 × 16 padding. */
|
|
817
826
|
.cv-controls-row {
|
|
818
827
|
display: flex;
|
|
819
828
|
justify-content: center;
|
|
820
|
-
gap: 1.
|
|
829
|
+
gap: 1.25rem;
|
|
821
830
|
}
|
|
822
831
|
.cv-controls-primary {
|
|
823
832
|
display: flex;
|
|
@@ -840,9 +849,12 @@
|
|
|
840
849
|
white-space: nowrap;
|
|
841
850
|
user-select: none;
|
|
842
851
|
}
|
|
852
|
+
/* The four secondaries' words get a notch more than Colgar / Rechazar / Contestar's 10px — they are
|
|
853
|
+
the ones read mid-call, and the five-up row used to shrink them to 9px (8px in a sidebar). */
|
|
854
|
+
.cv-controls-row .cv-ctrl-label { font-size: 0.6875rem; }
|
|
843
855
|
.cv-ctrl {
|
|
844
|
-
width:
|
|
845
|
-
height:
|
|
856
|
+
width: 52px;
|
|
857
|
+
height: 52px;
|
|
846
858
|
border-radius: 999px;
|
|
847
859
|
border: 1px solid var(--cv-border-strong);
|
|
848
860
|
background: var(--cv-bg);
|
|
@@ -855,6 +867,11 @@
|
|
|
855
867
|
}
|
|
856
868
|
.cv-ctrl:hover { background: var(--cv-surface); }
|
|
857
869
|
.cv-ctrl:active { transform: scale(0.94); }
|
|
870
|
+
.cv-ctrl:disabled {
|
|
871
|
+
opacity: 0.4;
|
|
872
|
+
cursor: not-allowed;
|
|
873
|
+
pointer-events: none;
|
|
874
|
+
}
|
|
858
875
|
|
|
859
876
|
/* The ON states say what they MEAN, in the widget's own tokens: muted is danger,
|
|
860
877
|
held is hold, transfer-open is accent. Colour is the state, not decoration. */
|
|
@@ -900,6 +917,28 @@
|
|
|
900
917
|
border-color: var(--cv-text);
|
|
901
918
|
}
|
|
902
919
|
|
|
920
|
+
/*
|
|
921
|
+
Sidebar-narrow (this file's own ≤300px tier, up in "Narrow hosts"). The four 52px controls with
|
|
922
|
+
20px gaps need 268px, which a 260px host cannot give — so this tier steps down to 44px with 12px
|
|
923
|
+
gaps (4 × 44 + 3 × 12 = 212px) and reclaims `.cv-controls`'s own horizontal padding, the first
|
|
924
|
+
lever that block's own comment names. 44px is still the Apple HIG minimum tap target; the five-up
|
|
925
|
+
row this replaced went to 36px here.
|
|
926
|
+
|
|
927
|
+
Placed AFTER `.cv-controls`/`.cv-controls-row` above rather than up with this file's other
|
|
928
|
+
`@container` blocks near the top: `gap` and `padding-left`/`-right` here have the same specificity as
|
|
929
|
+
those base rules' own shorthand, so — unlike a compound selector such as `.cv-controls-row .cv-ctrl`,
|
|
930
|
+
which wins on specificity regardless of position — source order is what decides, and a narrower-width
|
|
931
|
+
override declared BEFORE the base rule it means to narrow loses to it. Confirmed live: moved up into
|
|
932
|
+
the "Narrow hosts" section, the `.cv-ctrl` width and label font-size reductions took effect (compound
|
|
933
|
+
selectors) but `gap` and `.cv-controls`'s padding silently did not (plain selectors, later base rule won).
|
|
934
|
+
*/
|
|
935
|
+
@container cv (max-width: 300px) {
|
|
936
|
+
.cv-controls { padding-left: 0.5rem; padding-right: 0.5rem; }
|
|
937
|
+
.cv-controls-row { gap: 0.75rem; }
|
|
938
|
+
.cv-controls-row .cv-ctrl { width: 44px; height: 44px; }
|
|
939
|
+
.cv-controls-row .cv-ctrl-label { font-size: 0.625rem; }
|
|
940
|
+
}
|
|
941
|
+
|
|
903
942
|
/*
|
|
904
943
|
The attended-transfer target-entry panel (`docs/specs/transfer-availability.md` §5): an optional
|
|
905
944
|
outcome line, then a row of whatever's actionable (input+button today). Once a consult exists,
|
|
@@ -1146,49 +1185,58 @@
|
|
|
1146
1185
|
transform: scale(0.98);
|
|
1147
1186
|
}
|
|
1148
1187
|
|
|
1188
|
+
@keyframes cv-pop-in-up {
|
|
1189
|
+
from { opacity: 0; transform: translateY(4px) scale(0.97); }
|
|
1190
|
+
to { opacity: 1; transform: none; }
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1149
1193
|
/*
|
|
1150
|
-
The in-call
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
parent, never the narrow trigger itself — the same fix CountryPicker's own popover needed (see its
|
|
1159
|
-
comment).
|
|
1194
|
+
The in-call DTMF keypad — the one floating popover left on the call screen. (Dispositivos was the
|
|
1195
|
+
other; it is inline content now, `.cv-call-devices-inline` above, in every call state.)
|
|
1196
|
+
|
|
1197
|
+
Opens UPWARD (`bottom: 100%`): the trigger sits at the bottom of the call screen, and there is no
|
|
1198
|
+
room to open downward. Anchored to `.cv-controls`'s own horizontal padding — a container whose width
|
|
1199
|
+
is bounded by ITS parent, never the narrow trigger itself — the same fix CountryPicker's own popover
|
|
1200
|
+
needed (see its comment): anchoring to a narrow, off-centre trigger let the popover's edge reach
|
|
1201
|
+
`.cv-softphone`'s `overflow: hidden` and get clipped.
|
|
1160
1202
|
*/
|
|
1161
|
-
.cv-
|
|
1203
|
+
.cv-keypad-popover {
|
|
1162
1204
|
position: absolute;
|
|
1163
1205
|
z-index: 20;
|
|
1164
|
-
display:
|
|
1165
|
-
|
|
1166
|
-
gap:
|
|
1167
|
-
padding: 0.
|
|
1206
|
+
display: grid;
|
|
1207
|
+
grid-template-columns: repeat(3, 1fr);
|
|
1208
|
+
gap: 8px;
|
|
1209
|
+
padding: 0.85rem 1rem;
|
|
1168
1210
|
background: var(--cv-surface);
|
|
1169
1211
|
border: 1px solid var(--cv-border);
|
|
1170
1212
|
border-radius: var(--cv-radius);
|
|
1171
1213
|
box-shadow: var(--cv-shadow);
|
|
1172
1214
|
}
|
|
1173
|
-
.cv-
|
|
1215
|
+
.cv-keypad-popover--above {
|
|
1174
1216
|
bottom: 100%;
|
|
1175
1217
|
left: 1rem;
|
|
1176
1218
|
right: 1rem;
|
|
1177
1219
|
margin-bottom: 0.6rem;
|
|
1178
1220
|
transform-origin: bottom center;
|
|
1179
|
-
animation: cv-
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1221
|
+
animation: cv-pop-in-up 0.14s cubic-bezier(0.2, 0, 0.2, 1);
|
|
1222
|
+
}
|
|
1223
|
+
/* `.cv-key` already carries the full key look (border, hover, active-scale, digit/letter type) from
|
|
1224
|
+
the pre-call dialer — only the size needs setting here, since this grid has no flex ancestor to
|
|
1225
|
+
stretch the rows the way `.cv-keypad` gets to.
|
|
1226
|
+
|
|
1227
|
+
A fixed HEIGHT, not an `aspect-ratio`. This popover opens upward from `.cv-controls`, so it has
|
|
1228
|
+
(host height - controls block - margin) to work with, and a ratio ties its height to the host's
|
|
1229
|
+
WIDTH, which says nothing about how much height there is. Found live: the CRM's real body is 310×473
|
|
1230
|
+
(a 520px panel minus its 45px header), and a 1.3 ratio made this popover 285px — with the 184px
|
|
1231
|
+
controls block that is 479px, so its top ~7px were clipped by `.cv-softphone`'s `overflow: hidden`.
|
|
1232
|
+
48px keys give a 243px popover at ANY width, fit that host with room to spare, and stay at the
|
|
1233
|
+
44-48px minimum tap target (the old ratio gave 45px keys at a 260px host, 58px at 310). */
|
|
1234
|
+
.cv-keypad-popover .cv-key {
|
|
1235
|
+
height: 48px;
|
|
1184
1236
|
}
|
|
1185
1237
|
@media (prefers-reduced-motion: reduce) {
|
|
1186
|
-
.cv-
|
|
1238
|
+
.cv-keypad-popover--above { animation: none; }
|
|
1187
1239
|
}
|
|
1188
|
-
/* No per-row override needed here: `.cv-settings-row`/`.cv-settings-note` carry no border,
|
|
1189
|
-
background or padding of their own (see the Settings tab block) — this popover's own `gap` and
|
|
1190
|
-
`padding` are the only spacing in play, which is what keeps it lighter than the full Ajustes
|
|
1191
|
-
page's bordered section for what is meant to be a quick, transient panel. */
|
|
1192
1240
|
|
|
1193
1241
|
/* ── Lists (contacts / history) ──────────────────────────────────────────────── */
|
|
1194
1242
|
.cv-list-head { flex-shrink: 0; padding: 0.85rem 1rem 0.5rem; }
|
|
@@ -1904,6 +1952,116 @@
|
|
|
1904
1952
|
}
|
|
1905
1953
|
.cv-settings-select:focus { outline: none; border-color: var(--cv-accent); background: var(--cv-bg); }
|
|
1906
1954
|
.cv-settings-empty { font-size: 0.85rem; color: var(--cv-muted); }
|
|
1955
|
+
|
|
1956
|
+
/* `--cv-danger-strong` is the AS-TEXT danger token (see its definition): red-600 on the light `--cv-bg`,
|
|
1957
|
+
red-400 on the dark one. */
|
|
1958
|
+
.cv-settings-error { margin: 0.6rem 0.2rem 0; font-size: 0.78rem; line-height: 1.4; color: var(--cv-danger-strong); }
|
|
1959
|
+
|
|
1960
|
+
/*
|
|
1961
|
+
Ajustes as a list (custom-ringtone.md §4.6, revised 2026-10-14): each row names a setting, shows its CURRENT
|
|
1962
|
+
value under the name, and opens a page. Same icon circle and dividers as the device rows above (they live in
|
|
1963
|
+
the same `.cv-settings-section-body`, which supplies the padding and the dividers), same chevron as
|
|
1964
|
+
`.cv-detail-row--action` — one row vocabulary across the widget.
|
|
1965
|
+
*/
|
|
1966
|
+
.cv-settings-nav {
|
|
1967
|
+
display: flex;
|
|
1968
|
+
align-items: center;
|
|
1969
|
+
gap: 0.7rem;
|
|
1970
|
+
width: 100%;
|
|
1971
|
+
border: none;
|
|
1972
|
+
background: none;
|
|
1973
|
+
font: inherit;
|
|
1974
|
+
color: inherit;
|
|
1975
|
+
text-align: left;
|
|
1976
|
+
}
|
|
1977
|
+
button.cv-settings-nav { cursor: pointer; transition: background 0.12s ease; }
|
|
1978
|
+
button.cv-settings-nav:hover { background: var(--cv-surface); }
|
|
1979
|
+
button.cv-settings-nav:focus-visible { outline: 2px solid var(--cv-accent); outline-offset: -2px; }
|
|
1980
|
+
.cv-settings-nav-text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 0.1rem; }
|
|
1981
|
+
.cv-settings-nav-title { font-size: 0.88rem; font-weight: 600; color: var(--cv-text); }
|
|
1982
|
+
/* One line, cut with an ellipsis — the full device name is on its page. `--cv-muted` on `--cv-bg` is the
|
|
1983
|
+
pairing already verified for `.cv-settings-note`. The static row (a note, no page) may wrap instead. */
|
|
1984
|
+
.cv-settings-nav-value {
|
|
1985
|
+
font-size: 0.8rem;
|
|
1986
|
+
color: var(--cv-muted);
|
|
1987
|
+
overflow: hidden;
|
|
1988
|
+
text-overflow: ellipsis;
|
|
1989
|
+
white-space: nowrap;
|
|
1990
|
+
}
|
|
1991
|
+
.cv-settings-nav--static .cv-settings-nav-value { white-space: normal; line-height: 1.45; }
|
|
1992
|
+
.cv-settings-nav-chevron { flex-shrink: 0; color: var(--cv-muted); }
|
|
1993
|
+
|
|
1994
|
+
/* A settings page: the `.cv-detail` overlay (entrance, back button, reduced motion all inherited) with a
|
|
1995
|
+
scrolling body of its own. */
|
|
1996
|
+
.cv-settings-page-body { flex: 1; min-height: 0; overflow-y: auto; padding: 0.25rem 1rem 1rem; }
|
|
1997
|
+
.cv-settings-page-title {
|
|
1998
|
+
margin: 0.15rem 0.2rem 0.35rem;
|
|
1999
|
+
font-size: 1.05rem;
|
|
2000
|
+
font-weight: 700;
|
|
2001
|
+
letter-spacing: -0.01em;
|
|
2002
|
+
color: var(--cv-text);
|
|
2003
|
+
}
|
|
2004
|
+
/* Focus lands here on open (for keyboard and screen-reader users); a ring on a heading would only look
|
|
2005
|
+
like a stray box to everyone else. */
|
|
2006
|
+
.cv-settings-page-title:focus { outline: none; }
|
|
2007
|
+
.cv-settings-page-intro { margin: 0 0.2rem 0.85rem; font-size: 0.8rem; line-height: 1.45; color: var(--cv-muted); }
|
|
2008
|
+
.cv-settings-page-empty { margin: 0; padding: 0 0.2rem; font-size: 0.85rem; color: var(--cv-muted); }
|
|
2009
|
+
|
|
2010
|
+
/* The checkmark list: one card like `.cv-settings-section-body`, a row per choice. */
|
|
2011
|
+
.cv-choices {
|
|
2012
|
+
border: 1px solid var(--cv-border);
|
|
2013
|
+
border-radius: 14px;
|
|
2014
|
+
background: var(--cv-bg);
|
|
2015
|
+
overflow: hidden;
|
|
2016
|
+
}
|
|
2017
|
+
.cv-choice {
|
|
2018
|
+
position: relative;
|
|
2019
|
+
display: flex;
|
|
2020
|
+
align-items: center;
|
|
2021
|
+
gap: 0.6rem;
|
|
2022
|
+
min-height: 44px;
|
|
2023
|
+
padding: 0.6rem 0.85rem;
|
|
2024
|
+
cursor: pointer;
|
|
2025
|
+
transition: background 0.12s ease;
|
|
2026
|
+
}
|
|
2027
|
+
.cv-choice + .cv-choice { border-top: 1px solid var(--cv-border); }
|
|
2028
|
+
.cv-choice:hover { background: var(--cv-surface); }
|
|
2029
|
+
/* The real control: a native radio, kept in the accessibility tree and keyboard order, drawn by the row. */
|
|
2030
|
+
.cv-choice-input {
|
|
2031
|
+
position: absolute;
|
|
2032
|
+
opacity: 0;
|
|
2033
|
+
width: 1px;
|
|
2034
|
+
height: 1px;
|
|
2035
|
+
margin: 0;
|
|
2036
|
+
pointer-events: none;
|
|
2037
|
+
}
|
|
2038
|
+
.cv-choice:has(.cv-choice-input:focus-visible) { outline: 2px solid var(--cv-accent); outline-offset: -2px; }
|
|
2039
|
+
.cv-choice-text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 0.1rem; }
|
|
2040
|
+
.cv-choice-label { font-size: 0.88rem; color: var(--cv-text); overflow-wrap: anywhere; }
|
|
2041
|
+
.cv-choice-sub { font-size: 0.78rem; color: var(--cv-muted); }
|
|
2042
|
+
.cv-choice[data-checked] .cv-choice-label { font-weight: 600; }
|
|
2043
|
+
.cv-choice-check { flex-shrink: 0; color: var(--cv-accent-strong); visibility: hidden; }
|
|
2044
|
+
.cv-choice[data-checked] .cv-choice-check { visibility: visible; }
|
|
2045
|
+
.cv-choice-playing { display: inline-flex; flex-shrink: 0; color: var(--cv-accent-strong); }
|
|
2046
|
+
|
|
2047
|
+
.cv-settings-test,
|
|
2048
|
+
.cv-settings-retry {
|
|
2049
|
+
display: inline-flex;
|
|
2050
|
+
align-items: center;
|
|
2051
|
+
gap: 0.4rem;
|
|
2052
|
+
}
|
|
2053
|
+
.cv-settings-test { margin-top: 0.85rem; }
|
|
2054
|
+
.cv-settings-retry { margin-top: 0.6rem; }
|
|
2055
|
+
|
|
2056
|
+
/* Visually hidden, still read aloud. */
|
|
2057
|
+
.cv-sr-only {
|
|
2058
|
+
position: absolute;
|
|
2059
|
+
width: 1px;
|
|
2060
|
+
height: 1px;
|
|
2061
|
+
overflow: hidden;
|
|
2062
|
+
clip-path: inset(50%);
|
|
2063
|
+
white-space: nowrap;
|
|
2064
|
+
}
|
|
1907
2065
|
/* `--cv-muted` on `--cv-bg` — 4.83:1 light / 6.97:1 dark, computed fresh for THIS pairing (the note
|
|
1908
2066
|
used to sit on `--cv-surface` with its own card; now it sits directly on the section's `--cv-bg`,
|
|
1909
2067
|
a different background that earns its own check rather than inheriting the old one's number). */
|
|
@@ -1913,18 +2071,6 @@
|
|
|
1913
2071
|
font-size: 0.8rem;
|
|
1914
2072
|
line-height: 1.5;
|
|
1915
2073
|
}
|
|
1916
|
-
/* Closing note for the tab as a whole, not any one section — sits directly on `.cv-settings-body`,
|
|
1917
|
-
which shares the same `--cv-bg` as `.cv-settings-section-body` above, so the `--cv-muted`-on-
|
|
1918
|
-
`--cv-bg` pairing already verified for `.cv-settings-note` carries over unchanged. Centred and
|
|
1919
|
-
quieter than a row: this is a footnote about the tab, not a row's worth of content. */
|
|
1920
|
-
.cv-settings-more {
|
|
1921
|
-
margin: 0;
|
|
1922
|
-
padding: 0 0.5rem;
|
|
1923
|
-
text-align: center;
|
|
1924
|
-
color: var(--cv-muted);
|
|
1925
|
-
font-size: 0.78rem;
|
|
1926
|
-
line-height: 1.5;
|
|
1927
|
-
}
|
|
1928
2074
|
|
|
1929
2075
|
/* ── Centred states (mic gate) ───────────────────────────────────────────────── */
|
|
1930
2076
|
.cv-center {
|