@holdyapp/sdk 0.2.1 → 1.0.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/README.md +20 -147
- package/dist/auto-capture.d.ts +12 -16
- package/dist/auto-capture.d.ts.map +1 -0
- package/dist/browser.d.ts +23 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/core.d.ts +42 -0
- package/dist/core.d.ts.map +1 -0
- package/dist/holdy.js +2 -0
- package/dist/holdy.js.map +7 -0
- package/dist/index.cjs.js +422 -0
- package/dist/index.cjs.js.map +7 -0
- package/dist/index.d.ts +5 -46
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +399 -0
- package/dist/index.mjs.map +7 -0
- package/dist/server.cjs.js +171 -0
- package/dist/server.cjs.js.map +7 -0
- package/dist/server.d.ts +15 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.mjs +148 -0
- package/dist/server.mjs.map +7 -0
- package/dist/types.d.ts +41 -109
- package/dist/types.d.ts.map +1 -0
- package/dist/web-context.d.ts +51 -0
- package/dist/web-context.d.ts.map +1 -0
- package/dist/web.d.ts +41 -0
- package/dist/web.d.ts.map +1 -0
- package/dist/web.mjs +389 -0
- package/dist/web.mjs.map +7 -0
- package/package.json +24 -20
- package/src/auto-capture.ts +51 -78
- package/src/browser.ts +55 -0
- package/src/core.test.ts +147 -0
- package/src/core.ts +172 -0
- package/src/index.ts +4 -427
- package/src/server.ts +19 -0
- package/src/types.ts +38 -162
- package/src/web-context.test.ts +71 -0
- package/src/web-context.ts +227 -0
- package/src/web.ts +108 -0
- package/LICENSE +0 -21
- package/dist/anchor.d.ts +0 -8
- package/dist/holdy.cjs.js +0 -255
- package/dist/holdy.cjs.js.map +0 -7
- package/dist/holdy.esm.js +0 -255
- package/dist/holdy.esm.js.map +0 -7
- package/dist/holdy.iife.js +0 -255
- package/dist/holdy.iife.js.map +0 -7
- package/dist/outcome.d.ts +0 -19
- package/dist/queue.d.ts +0 -54
- package/dist/render/kinds/banner.d.ts +0 -3
- package/dist/render/kinds/checklist.d.ts +0 -19
- package/dist/render/kinds/cta.d.ts +0 -8
- package/dist/render/kinds/modal.d.ts +0 -16
- package/dist/render/kinds/shared.d.ts +0 -16
- package/dist/render/kinds/spotlight.d.ts +0 -17
- package/dist/render/kinds/tooltip.d.ts +0 -3
- package/dist/render/root.d.ts +0 -3
- package/dist/render/styles.d.ts +0 -5
- package/dist/store.d.ts +0 -36
- package/dist/transport.d.ts +0 -59
- package/src/anchor.ts +0 -72
- package/src/outcome.ts +0 -51
- package/src/queue.ts +0 -296
- package/src/render/kinds/banner.ts +0 -52
- package/src/render/kinds/checklist.ts +0 -175
- package/src/render/kinds/cta.ts +0 -53
- package/src/render/kinds/modal.ts +0 -129
- package/src/render/kinds/shared.ts +0 -67
- package/src/render/kinds/spotlight.ts +0 -176
- package/src/render/kinds/tooltip.ts +0 -82
- package/src/render/root.ts +0 -69
- package/src/render/styles.ts +0 -280
- package/src/store.ts +0 -106
- package/src/transport.ts +0 -182
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import type { InterventionPayload, OutcomeValue } from '../../types'
|
|
2
|
-
|
|
3
|
-
export interface RenderHandlers {
|
|
4
|
-
onExecuted(metadata?: Record<string, unknown>): void
|
|
5
|
-
onDismissed(metadata?: Record<string, unknown>): void
|
|
6
|
-
onFailed(metadata?: Record<string, unknown>): void
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export interface RenderControl {
|
|
10
|
-
el: HTMLElement
|
|
11
|
-
dismiss: (outcome?: OutcomeValue, metadata?: Record<string, unknown>) => void
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/** Cria botão fechar SDK-owned · dispatch dismissed. */
|
|
15
|
-
export function buildCloseButton(onDismiss: () => void): HTMLButtonElement {
|
|
16
|
-
const btn = document.createElement('button')
|
|
17
|
-
btn.type = 'button'
|
|
18
|
-
btn.className = 'holdy-close'
|
|
19
|
-
btn.setAttribute('aria-label', 'Fechar')
|
|
20
|
-
btn.textContent = '×'
|
|
21
|
-
btn.addEventListener('click', (e) => {
|
|
22
|
-
e.stopPropagation()
|
|
23
|
-
onDismiss()
|
|
24
|
-
})
|
|
25
|
-
return btn
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** Cria CTA button OU anchor SDK-owned · dispatch executed. */
|
|
29
|
-
export function buildCtaButton(
|
|
30
|
-
payload: InterventionPayload,
|
|
31
|
-
onExecuted: () => void,
|
|
32
|
-
): HTMLElement | null {
|
|
33
|
-
if (!payload.cta_text) return null
|
|
34
|
-
|
|
35
|
-
let el: HTMLElement
|
|
36
|
-
if (payload.cta_href) {
|
|
37
|
-
const a = document.createElement('a')
|
|
38
|
-
a.href = payload.cta_href
|
|
39
|
-
a.rel = 'noopener noreferrer'
|
|
40
|
-
a.target = '_self'
|
|
41
|
-
el = a
|
|
42
|
-
} else {
|
|
43
|
-
el = document.createElement('button')
|
|
44
|
-
;(el as HTMLButtonElement).type = 'button'
|
|
45
|
-
}
|
|
46
|
-
el.className = 'holdy-btn'
|
|
47
|
-
// textContent only · NEVER innerHTML.
|
|
48
|
-
el.textContent = payload.cta_text
|
|
49
|
-
el.addEventListener('click', () => onExecuted())
|
|
50
|
-
return el
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export function buildTextNode(text: string): HTMLParagraphElement {
|
|
54
|
-
const p = document.createElement('p')
|
|
55
|
-
p.className = 'holdy-card-text'
|
|
56
|
-
p.textContent = text
|
|
57
|
-
return p
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export function buildActionsRow(...children: Array<HTMLElement | null>): HTMLDivElement | null {
|
|
61
|
-
const filtered = children.filter((c): c is HTMLElement => c !== null)
|
|
62
|
-
if (filtered.length === 0) return null
|
|
63
|
-
const row = document.createElement('div')
|
|
64
|
-
row.className = 'holdy-card-actions'
|
|
65
|
-
filtered.forEach((c) => row.appendChild(c))
|
|
66
|
-
return row
|
|
67
|
-
}
|
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
import type { InterventionPayload } from '../../types'
|
|
2
|
-
import {
|
|
3
|
-
buildActionsRow,
|
|
4
|
-
buildCloseButton,
|
|
5
|
-
buildCtaButton,
|
|
6
|
-
buildTextNode,
|
|
7
|
-
type RenderControl,
|
|
8
|
-
type RenderHandlers,
|
|
9
|
-
} from './shared'
|
|
10
|
-
|
|
11
|
-
const SPOTLIGHT_PADDING = 6
|
|
12
|
-
const TOOLTIP_GAP_PX = 12
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Spotlight · backdrop escurecido com "buraco" recortado no anchor +
|
|
16
|
-
* tooltip apontando.
|
|
17
|
-
*
|
|
18
|
-
* UX:
|
|
19
|
-
* · backdrop tem clip-path criando ring around o elemento
|
|
20
|
-
* · tooltip aparece abaixo (ou acima se sem espaço)
|
|
21
|
-
* · clique no anchor original (atravessa o buraco via pointer-events:none
|
|
22
|
-
* na área do hole) executa
|
|
23
|
-
* · backdrop click fora do hole dismissa
|
|
24
|
-
* · ESC dismissa
|
|
25
|
-
*
|
|
26
|
-
* Uso: onboarding step "veja esse botão", first-time UI guidance.
|
|
27
|
-
*/
|
|
28
|
-
export function renderSpotlight(
|
|
29
|
-
payload: InterventionPayload,
|
|
30
|
-
anchor: Element | null,
|
|
31
|
-
handlers: RenderHandlers,
|
|
32
|
-
): RenderControl {
|
|
33
|
-
if (!anchor) {
|
|
34
|
-
handlers.onFailed({ reason: 'anchor_unresolved' })
|
|
35
|
-
const empty = document.createElement('div')
|
|
36
|
-
return { el: empty, dismiss: () => undefined }
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const container = document.createElement('div')
|
|
40
|
-
container.className = 'holdy-spotlight-container'
|
|
41
|
-
|
|
42
|
-
const backdrop = document.createElement('div')
|
|
43
|
-
backdrop.className = 'holdy-spotlight-backdrop'
|
|
44
|
-
container.appendChild(backdrop)
|
|
45
|
-
|
|
46
|
-
const ring = document.createElement('div')
|
|
47
|
-
ring.className = 'holdy-spotlight-ring'
|
|
48
|
-
container.appendChild(ring)
|
|
49
|
-
|
|
50
|
-
const card = document.createElement('div')
|
|
51
|
-
card.className = 'holdy-card holdy-spotlight-tooltip'
|
|
52
|
-
card.setAttribute('role', 'dialog')
|
|
53
|
-
|
|
54
|
-
if (payload.title) {
|
|
55
|
-
const h = document.createElement('h3')
|
|
56
|
-
h.className = 'holdy-card-title'
|
|
57
|
-
h.textContent = payload.title
|
|
58
|
-
card.appendChild(h)
|
|
59
|
-
}
|
|
60
|
-
const text = buildTextNode(payload.text)
|
|
61
|
-
card.appendChild(text)
|
|
62
|
-
|
|
63
|
-
const close = buildCloseButton(() => dismiss('dismissed'))
|
|
64
|
-
const cta = buildCtaButton(payload, () => {
|
|
65
|
-
handlers.onExecuted()
|
|
66
|
-
cleanup()
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
const actions = buildActionsRow(cta, close)
|
|
70
|
-
if (actions) card.appendChild(actions)
|
|
71
|
-
else card.appendChild(close)
|
|
72
|
-
|
|
73
|
-
container.appendChild(card)
|
|
74
|
-
|
|
75
|
-
// Anchor click intercept · marca executed também (sem precisar do CTA)
|
|
76
|
-
const onAnchorClick = (): void => {
|
|
77
|
-
handlers.onExecuted({ via: 'anchor_click' })
|
|
78
|
-
cleanup()
|
|
79
|
-
}
|
|
80
|
-
;(anchor as HTMLElement).addEventListener('click', onAnchorClick, { once: true, capture: true })
|
|
81
|
-
|
|
82
|
-
// Backdrop click dismissa
|
|
83
|
-
backdrop.addEventListener('click', () => dismiss('dismissed', { reason: 'backdrop_click' }))
|
|
84
|
-
|
|
85
|
-
const onKeydown = (e: KeyboardEvent): void => {
|
|
86
|
-
if (e.key === 'Escape') {
|
|
87
|
-
e.stopPropagation()
|
|
88
|
-
dismiss('dismissed', { reason: 'esc' })
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
document.addEventListener('keydown', onKeydown, true)
|
|
92
|
-
|
|
93
|
-
const position = (): void => {
|
|
94
|
-
const rect = anchor.getBoundingClientRect()
|
|
95
|
-
if (rect.width === 0 && rect.height === 0) {
|
|
96
|
-
// Anchor sumiu · dismiss silencioso (no_show)
|
|
97
|
-
dismiss('no_show', { reason: 'anchor_disappeared' })
|
|
98
|
-
return
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const top = Math.max(0, rect.top - SPOTLIGHT_PADDING)
|
|
102
|
-
const left = Math.max(0, rect.left - SPOTLIGHT_PADDING)
|
|
103
|
-
const width = rect.width + SPOTLIGHT_PADDING * 2
|
|
104
|
-
const height = rect.height + SPOTLIGHT_PADDING * 2
|
|
105
|
-
|
|
106
|
-
ring.style.top = `${top}px`
|
|
107
|
-
ring.style.left = `${left}px`
|
|
108
|
-
ring.style.width = `${width}px`
|
|
109
|
-
ring.style.height = `${height}px`
|
|
110
|
-
|
|
111
|
-
// Clip-path no backdrop pra criar buraco
|
|
112
|
-
backdrop.style.clipPath = `polygon(
|
|
113
|
-
0 0, 100% 0, 100% 100%, 0 100%, 0 0,
|
|
114
|
-
${left}px ${top}px,
|
|
115
|
-
${left}px ${top + height}px,
|
|
116
|
-
${left + width}px ${top + height}px,
|
|
117
|
-
${left + width}px ${top}px,
|
|
118
|
-
${left}px ${top}px
|
|
119
|
-
)`
|
|
120
|
-
|
|
121
|
-
// Tooltip abaixo ou acima do ring
|
|
122
|
-
const cardRect = card.getBoundingClientRect()
|
|
123
|
-
let cardTop = top + height + TOOLTIP_GAP_PX
|
|
124
|
-
let cardLeft = left + width / 2 - cardRect.width / 2
|
|
125
|
-
|
|
126
|
-
if (cardLeft < 8) cardLeft = 8
|
|
127
|
-
const maxLeft = window.innerWidth - cardRect.width - 8
|
|
128
|
-
if (cardLeft > maxLeft) cardLeft = maxLeft
|
|
129
|
-
if (cardTop + cardRect.height > window.innerHeight - 8) {
|
|
130
|
-
cardTop = top - cardRect.height - TOOLTIP_GAP_PX
|
|
131
|
-
}
|
|
132
|
-
card.style.top = `${Math.max(8, cardTop)}px`
|
|
133
|
-
card.style.left = `${cardLeft}px`
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Init off-screen
|
|
137
|
-
card.style.top = '-9999px'
|
|
138
|
-
card.style.left = '-9999px'
|
|
139
|
-
|
|
140
|
-
requestAnimationFrame(() => {
|
|
141
|
-
position()
|
|
142
|
-
window.addEventListener('scroll', position, true)
|
|
143
|
-
window.addEventListener('resize', position)
|
|
144
|
-
})
|
|
145
|
-
|
|
146
|
-
let dismissed = false
|
|
147
|
-
const cleanup = (): void => {
|
|
148
|
-
if (dismissed) return
|
|
149
|
-
dismissed = true
|
|
150
|
-
container.remove()
|
|
151
|
-
document.removeEventListener('keydown', onKeydown, true)
|
|
152
|
-
window.removeEventListener('scroll', position, true)
|
|
153
|
-
window.removeEventListener('resize', position)
|
|
154
|
-
;(anchor as HTMLElement).removeEventListener('click', onAnchorClick, true)
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
const dismiss = (
|
|
158
|
-
outcome: 'dismissed' | 'no_show' | 'failed' = 'dismissed',
|
|
159
|
-
metadata?: Record<string, unknown>,
|
|
160
|
-
): void => {
|
|
161
|
-
if (dismissed) return
|
|
162
|
-
if (outcome === 'dismissed') handlers.onDismissed(metadata)
|
|
163
|
-
if (outcome === 'no_show') {
|
|
164
|
-
// Não dispara onFailed · no_show é caso normal de anchor desaparecer
|
|
165
|
-
// mid-render. Caller (HoldySdk.renderDecision) já reporta no_show via
|
|
166
|
-
// outcomeTracker antes mesmo de chamar render. Aqui apenas cleanup.
|
|
167
|
-
}
|
|
168
|
-
if (outcome === 'failed') handlers.onFailed(metadata)
|
|
169
|
-
cleanup()
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
return {
|
|
173
|
-
el: container,
|
|
174
|
-
dismiss: (o) => dismiss(o === 'failed' || o === 'no_show' ? o : 'dismissed'),
|
|
175
|
-
}
|
|
176
|
-
}
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import type { InterventionPayload } from '../../types'
|
|
2
|
-
import {
|
|
3
|
-
buildActionsRow,
|
|
4
|
-
buildCloseButton,
|
|
5
|
-
buildCtaButton,
|
|
6
|
-
buildTextNode,
|
|
7
|
-
type RenderControl,
|
|
8
|
-
type RenderHandlers,
|
|
9
|
-
} from './shared'
|
|
10
|
-
|
|
11
|
-
const TOOLTIP_GAP_PX = 8
|
|
12
|
-
|
|
13
|
-
export function renderTooltip(
|
|
14
|
-
payload: InterventionPayload,
|
|
15
|
-
anchor: Element | null,
|
|
16
|
-
handlers: RenderHandlers,
|
|
17
|
-
): RenderControl {
|
|
18
|
-
const card = document.createElement('div')
|
|
19
|
-
card.className = 'holdy-card holdy-tooltip'
|
|
20
|
-
card.setAttribute('role', 'tooltip')
|
|
21
|
-
|
|
22
|
-
const text = buildTextNode(payload.text)
|
|
23
|
-
card.appendChild(text)
|
|
24
|
-
|
|
25
|
-
const close = buildCloseButton(() => dismiss('dismissed'))
|
|
26
|
-
const cta = buildCtaButton(payload, () => {
|
|
27
|
-
handlers.onExecuted()
|
|
28
|
-
cleanup()
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
const actions = buildActionsRow(cta, close)
|
|
32
|
-
if (actions) card.appendChild(actions)
|
|
33
|
-
else card.appendChild(close)
|
|
34
|
-
|
|
35
|
-
let dismissed = false
|
|
36
|
-
const cleanup = (): void => {
|
|
37
|
-
if (dismissed) return
|
|
38
|
-
dismissed = true
|
|
39
|
-
card.remove()
|
|
40
|
-
window.removeEventListener('scroll', position, true)
|
|
41
|
-
window.removeEventListener('resize', position)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const dismiss = (outcome: 'dismissed' | 'no_show' | 'failed' = 'dismissed'): void => {
|
|
45
|
-
if (dismissed) return
|
|
46
|
-
if (outcome === 'dismissed') handlers.onDismissed()
|
|
47
|
-
if (outcome === 'failed') handlers.onFailed()
|
|
48
|
-
cleanup()
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const position = (): void => {
|
|
52
|
-
if (!anchor) return
|
|
53
|
-
const rect = anchor.getBoundingClientRect()
|
|
54
|
-
if (rect.width === 0 && rect.height === 0) return
|
|
55
|
-
const cardRect = card.getBoundingClientRect()
|
|
56
|
-
let top = rect.bottom + TOOLTIP_GAP_PX
|
|
57
|
-
let left = rect.left + rect.width / 2 - cardRect.width / 2
|
|
58
|
-
// Keep in viewport
|
|
59
|
-
const maxLeft = window.innerWidth - cardRect.width - 8
|
|
60
|
-
if (left < 8) left = 8
|
|
61
|
-
if (left > maxLeft) left = maxLeft
|
|
62
|
-
if (top + cardRect.height > window.innerHeight - 8) {
|
|
63
|
-
// Flip above
|
|
64
|
-
top = rect.top - cardRect.height - TOOLTIP_GAP_PX
|
|
65
|
-
}
|
|
66
|
-
card.style.top = `${Math.max(8, top)}px`
|
|
67
|
-
card.style.left = `${left}px`
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Initial render off-screen pra medir antes de posicionar.
|
|
71
|
-
card.style.top = '-9999px'
|
|
72
|
-
card.style.left = '-9999px'
|
|
73
|
-
|
|
74
|
-
// Caller anexa ao shadow root, então retornamos card + ativador.
|
|
75
|
-
requestAnimationFrame(() => {
|
|
76
|
-
position()
|
|
77
|
-
window.addEventListener('scroll', position, true)
|
|
78
|
-
window.addEventListener('resize', position)
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
return { el: card, dismiss: (o) => dismiss(o === 'failed' || o === 'no_show' ? o : 'dismissed') }
|
|
82
|
-
}
|
package/src/render/root.ts
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import type { ThemeConfig } from '../types'
|
|
2
|
-
import { applyThemeVars, getStyleSheet, injectFallbackStyle } from './styles'
|
|
3
|
-
|
|
4
|
-
const HOST_TAG = 'holdy-root'
|
|
5
|
-
let hostEl: HTMLElement | null = null
|
|
6
|
-
let shadow: ShadowRoot | null = null
|
|
7
|
-
|
|
8
|
-
function defineCustomElement(): void {
|
|
9
|
-
if (typeof customElements === 'undefined') return
|
|
10
|
-
if (customElements.get(HOST_TAG)) return
|
|
11
|
-
// Minimal subclass · we keep behavior in plain functions.
|
|
12
|
-
class HoldyRoot extends HTMLElement {}
|
|
13
|
-
try {
|
|
14
|
-
customElements.define(HOST_TAG, HoldyRoot)
|
|
15
|
-
} catch {
|
|
16
|
-
// Already defined by another instance · safe to ignore.
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function ensureRoot(theme: ThemeConfig | undefined): ShadowRoot | null {
|
|
21
|
-
if (typeof document === 'undefined') return null
|
|
22
|
-
if (shadow) {
|
|
23
|
-
if (hostEl) applyThemeVars(hostEl, theme)
|
|
24
|
-
return shadow
|
|
25
|
-
}
|
|
26
|
-
defineCustomElement()
|
|
27
|
-
|
|
28
|
-
hostEl = document.createElement(HOST_TAG)
|
|
29
|
-
hostEl.setAttribute('aria-live', 'polite')
|
|
30
|
-
hostEl.setAttribute('data-holdy-root', '1')
|
|
31
|
-
hostEl.style.position = 'fixed'
|
|
32
|
-
hostEl.style.top = '0'
|
|
33
|
-
hostEl.style.left = '0'
|
|
34
|
-
hostEl.style.width = '0'
|
|
35
|
-
hostEl.style.height = '0'
|
|
36
|
-
hostEl.style.zIndex = '2147483647'
|
|
37
|
-
hostEl.style.pointerEvents = 'none'
|
|
38
|
-
|
|
39
|
-
const root = hostEl.attachShadow({ mode: 'closed' })
|
|
40
|
-
shadow = root
|
|
41
|
-
const sheet = getStyleSheet()
|
|
42
|
-
if (sheet && 'adoptedStyleSheets' in root) {
|
|
43
|
-
try {
|
|
44
|
-
;(root as ShadowRoot & { adoptedStyleSheets: CSSStyleSheet[] }).adoptedStyleSheets = [sheet]
|
|
45
|
-
} catch {
|
|
46
|
-
injectFallbackStyle(root)
|
|
47
|
-
}
|
|
48
|
-
} else {
|
|
49
|
-
injectFallbackStyle(root)
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
applyThemeVars(hostEl, theme)
|
|
53
|
-
|
|
54
|
-
if (document.body) {
|
|
55
|
-
document.body.appendChild(hostEl)
|
|
56
|
-
} else {
|
|
57
|
-
document.addEventListener('DOMContentLoaded', () => {
|
|
58
|
-
if (hostEl && !hostEl.isConnected) document.body.appendChild(hostEl)
|
|
59
|
-
})
|
|
60
|
-
}
|
|
61
|
-
return shadow
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export function appendToRoot(node: HTMLElement): void {
|
|
65
|
-
if (!shadow) return
|
|
66
|
-
// Re-enable pointer events on actual cards (root host is pointer-events:none).
|
|
67
|
-
node.style.pointerEvents = 'auto'
|
|
68
|
-
shadow.appendChild(node)
|
|
69
|
-
}
|
package/src/render/styles.ts
DELETED
|
@@ -1,280 +0,0 @@
|
|
|
1
|
-
import type { ThemeConfig } from '../types'
|
|
2
|
-
import { DEFAULT_THEME } from '../types'
|
|
3
|
-
|
|
4
|
-
const BASE_CSS = `
|
|
5
|
-
:host {
|
|
6
|
-
--holdy-primary: var(--holdy-cfg-primary, #ca2121);
|
|
7
|
-
--holdy-font: var(--holdy-cfg-font, system-ui, -apple-system, sans-serif);
|
|
8
|
-
--holdy-radius: var(--holdy-cfg-radius, 8px);
|
|
9
|
-
--holdy-offset: var(--holdy-cfg-offset, 8px);
|
|
10
|
-
--holdy-bg: #ffffff;
|
|
11
|
-
--holdy-fg: #1a1a1a;
|
|
12
|
-
--holdy-muted: #737373;
|
|
13
|
-
--holdy-border: rgba(0, 0, 0, 0.08);
|
|
14
|
-
--holdy-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
|
15
|
-
all: initial;
|
|
16
|
-
font-family: var(--holdy-font);
|
|
17
|
-
color: var(--holdy-fg);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
@media (prefers-color-scheme: dark) {
|
|
21
|
-
:host {
|
|
22
|
-
--holdy-bg: #111111;
|
|
23
|
-
--holdy-fg: #f5f5f5;
|
|
24
|
-
--holdy-muted: #a3a3a3;
|
|
25
|
-
--holdy-border: rgba(255, 255, 255, 0.12);
|
|
26
|
-
--holdy-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
.holdy-card {
|
|
31
|
-
background: var(--holdy-bg);
|
|
32
|
-
color: var(--holdy-fg);
|
|
33
|
-
border: 1px solid var(--holdy-border);
|
|
34
|
-
border-radius: var(--holdy-radius);
|
|
35
|
-
box-shadow: var(--holdy-shadow);
|
|
36
|
-
padding: 12px 14px;
|
|
37
|
-
font-size: 14px;
|
|
38
|
-
line-height: 1.45;
|
|
39
|
-
max-width: 320px;
|
|
40
|
-
pointer-events: auto;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
.holdy-card-text {
|
|
44
|
-
margin: 0;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
.holdy-card-actions {
|
|
48
|
-
display: flex;
|
|
49
|
-
gap: 8px;
|
|
50
|
-
margin-top: 10px;
|
|
51
|
-
align-items: center;
|
|
52
|
-
justify-content: flex-end;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
.holdy-btn {
|
|
56
|
-
font-family: var(--holdy-font);
|
|
57
|
-
font-size: 13px;
|
|
58
|
-
font-weight: 500;
|
|
59
|
-
padding: 6px 12px;
|
|
60
|
-
border-radius: 6px;
|
|
61
|
-
border: none;
|
|
62
|
-
cursor: pointer;
|
|
63
|
-
background: var(--holdy-primary);
|
|
64
|
-
color: #ffffff;
|
|
65
|
-
text-decoration: none;
|
|
66
|
-
display: inline-flex;
|
|
67
|
-
align-items: center;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
.holdy-btn:focus-visible {
|
|
71
|
-
outline: 2px solid var(--holdy-primary);
|
|
72
|
-
outline-offset: 2px;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
.holdy-btn-ghost {
|
|
76
|
-
background: transparent;
|
|
77
|
-
color: var(--holdy-muted);
|
|
78
|
-
border: 1px solid var(--holdy-border);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
.holdy-close {
|
|
82
|
-
background: transparent;
|
|
83
|
-
border: none;
|
|
84
|
-
color: var(--holdy-muted);
|
|
85
|
-
font-size: 16px;
|
|
86
|
-
line-height: 1;
|
|
87
|
-
cursor: pointer;
|
|
88
|
-
padding: 4px;
|
|
89
|
-
border-radius: 4px;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
.holdy-close:hover {
|
|
93
|
-
color: var(--holdy-fg);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
.holdy-tooltip {
|
|
97
|
-
position: fixed;
|
|
98
|
-
z-index: 2147483647;
|
|
99
|
-
max-width: 280px;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
.holdy-banner {
|
|
103
|
-
position: fixed;
|
|
104
|
-
top: 0;
|
|
105
|
-
left: 50%;
|
|
106
|
-
transform: translateX(-50%);
|
|
107
|
-
margin-top: var(--holdy-offset);
|
|
108
|
-
z-index: 2147483647;
|
|
109
|
-
max-width: min(560px, calc(100vw - 32px));
|
|
110
|
-
display: flex;
|
|
111
|
-
align-items: center;
|
|
112
|
-
gap: 12px;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
.holdy-banner .holdy-card-text {
|
|
116
|
-
flex: 1;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
.holdy-cta {
|
|
120
|
-
position: fixed;
|
|
121
|
-
right: var(--holdy-offset);
|
|
122
|
-
bottom: var(--holdy-offset);
|
|
123
|
-
z-index: 2147483647;
|
|
124
|
-
max-width: 320px;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
.holdy-card-title {
|
|
128
|
-
margin: 0 0 6px;
|
|
129
|
-
font-size: 15px;
|
|
130
|
-
font-weight: 600;
|
|
131
|
-
line-height: 1.3;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/* ── Modal (Wave 2) ─────────────────────────────────────────────── */
|
|
135
|
-
.holdy-modal-backdrop {
|
|
136
|
-
position: fixed;
|
|
137
|
-
inset: 0;
|
|
138
|
-
background: rgba(0, 0, 0, 0.5);
|
|
139
|
-
display: flex;
|
|
140
|
-
align-items: center;
|
|
141
|
-
justify-content: center;
|
|
142
|
-
z-index: 2147483647;
|
|
143
|
-
padding: 20px;
|
|
144
|
-
pointer-events: auto;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
.holdy-modal {
|
|
148
|
-
max-width: 480px;
|
|
149
|
-
width: 100%;
|
|
150
|
-
padding: 20px 22px;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
.holdy-modal:focus {
|
|
154
|
-
outline: none;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/* ── Checklist (Wave 2) ─────────────────────────────────────────── */
|
|
158
|
-
.holdy-checklist {
|
|
159
|
-
position: fixed;
|
|
160
|
-
right: var(--holdy-offset);
|
|
161
|
-
bottom: var(--holdy-offset);
|
|
162
|
-
z-index: 2147483647;
|
|
163
|
-
max-width: 320px;
|
|
164
|
-
padding: 14px 16px;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
.holdy-checklist-header {
|
|
168
|
-
display: flex;
|
|
169
|
-
align-items: flex-start;
|
|
170
|
-
justify-content: space-between;
|
|
171
|
-
gap: 8px;
|
|
172
|
-
margin-bottom: 4px;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
.holdy-checklist-title {
|
|
176
|
-
margin: 0;
|
|
177
|
-
font-size: 14px;
|
|
178
|
-
font-weight: 600;
|
|
179
|
-
line-height: 1.3;
|
|
180
|
-
flex: 1;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
.holdy-checklist-list {
|
|
184
|
-
list-style: none;
|
|
185
|
-
padding: 0;
|
|
186
|
-
margin: 8px 0 0;
|
|
187
|
-
display: flex;
|
|
188
|
-
flex-direction: column;
|
|
189
|
-
gap: 6px;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
.holdy-checklist-item {
|
|
193
|
-
display: flex;
|
|
194
|
-
align-items: center;
|
|
195
|
-
gap: 8px;
|
|
196
|
-
font-size: 13px;
|
|
197
|
-
line-height: 1.4;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
.holdy-checklist-item.is-done .holdy-checklist-label {
|
|
201
|
-
text-decoration: line-through;
|
|
202
|
-
color: var(--holdy-muted);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
.holdy-checklist-checkbox {
|
|
206
|
-
accent-color: var(--holdy-primary);
|
|
207
|
-
cursor: pointer;
|
|
208
|
-
flex-shrink: 0;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
.holdy-checklist-label {
|
|
212
|
-
color: var(--holdy-fg);
|
|
213
|
-
text-decoration: none;
|
|
214
|
-
cursor: pointer;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
.holdy-checklist-label:hover {
|
|
218
|
-
color: var(--holdy-primary);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/* ── Spotlight (Wave 2) ─────────────────────────────────────────── */
|
|
222
|
-
.holdy-spotlight-container {
|
|
223
|
-
position: fixed;
|
|
224
|
-
inset: 0;
|
|
225
|
-
z-index: 2147483647;
|
|
226
|
-
pointer-events: none;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
.holdy-spotlight-backdrop {
|
|
230
|
-
position: absolute;
|
|
231
|
-
inset: 0;
|
|
232
|
-
background: rgba(0, 0, 0, 0.55);
|
|
233
|
-
pointer-events: auto;
|
|
234
|
-
transition: clip-path 120ms ease-out;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
.holdy-spotlight-ring {
|
|
238
|
-
position: absolute;
|
|
239
|
-
border-radius: 8px;
|
|
240
|
-
box-shadow: 0 0 0 2px var(--holdy-primary), 0 0 24px rgba(202, 33, 33, 0.4);
|
|
241
|
-
pointer-events: none;
|
|
242
|
-
transition: top 120ms, left 120ms, width 120ms, height 120ms;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
.holdy-spotlight-tooltip {
|
|
246
|
-
position: absolute;
|
|
247
|
-
max-width: 320px;
|
|
248
|
-
pointer-events: auto;
|
|
249
|
-
}
|
|
250
|
-
`
|
|
251
|
-
|
|
252
|
-
let cachedSheet: CSSStyleSheet | null = null
|
|
253
|
-
|
|
254
|
-
export function getStyleSheet(): CSSStyleSheet | null {
|
|
255
|
-
if (typeof CSSStyleSheet === 'undefined') return null
|
|
256
|
-
if (cachedSheet) return cachedSheet
|
|
257
|
-
try {
|
|
258
|
-
const sheet = new CSSStyleSheet()
|
|
259
|
-
sheet.replaceSync(BASE_CSS)
|
|
260
|
-
cachedSheet = sheet
|
|
261
|
-
return sheet
|
|
262
|
-
} catch {
|
|
263
|
-
return null
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
export function applyThemeVars(host: HTMLElement, theme: ThemeConfig | undefined): void {
|
|
268
|
-
const t = theme ?? DEFAULT_THEME
|
|
269
|
-
host.style.setProperty('--holdy-cfg-primary', t.primary_color)
|
|
270
|
-
host.style.setProperty('--holdy-cfg-font', t.font_family)
|
|
271
|
-
host.style.setProperty('--holdy-cfg-radius', `${t.border_radius_px}px`)
|
|
272
|
-
host.style.setProperty('--holdy-cfg-offset', `${t.position_offset_px}px`)
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
/** Fallback pra browsers sem adoptedStyleSheets (Safari <16.4). */
|
|
276
|
-
export function injectFallbackStyle(root: ShadowRoot): void {
|
|
277
|
-
const el = document.createElement('style')
|
|
278
|
-
el.textContent = BASE_CSS
|
|
279
|
-
root.appendChild(el)
|
|
280
|
-
}
|