@holdyapp/sdk 0.2.1 → 1.0.1

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.
Files changed (75) hide show
  1. package/README.md +20 -147
  2. package/dist/auto-capture.d.ts +12 -16
  3. package/dist/auto-capture.d.ts.map +1 -0
  4. package/dist/browser.d.ts +23 -0
  5. package/dist/browser.d.ts.map +1 -0
  6. package/dist/core.d.ts +42 -0
  7. package/dist/core.d.ts.map +1 -0
  8. package/dist/holdy.js +2 -0
  9. package/dist/holdy.js.map +7 -0
  10. package/dist/index.cjs +430 -0
  11. package/dist/index.cjs.map +7 -0
  12. package/dist/index.d.ts +5 -46
  13. package/dist/index.d.ts.map +1 -0
  14. package/dist/index.mjs +399 -0
  15. package/dist/index.mjs.map +7 -0
  16. package/dist/server.cjs +177 -0
  17. package/dist/server.cjs.map +7 -0
  18. package/dist/server.d.ts +15 -0
  19. package/dist/server.d.ts.map +1 -0
  20. package/dist/server.mjs +148 -0
  21. package/dist/server.mjs.map +7 -0
  22. package/dist/types.d.ts +41 -109
  23. package/dist/types.d.ts.map +1 -0
  24. package/dist/web-context.d.ts +51 -0
  25. package/dist/web-context.d.ts.map +1 -0
  26. package/dist/web.d.ts +41 -0
  27. package/dist/web.d.ts.map +1 -0
  28. package/dist/web.mjs +389 -0
  29. package/dist/web.mjs.map +7 -0
  30. package/package.json +24 -20
  31. package/src/auto-capture.ts +51 -78
  32. package/src/browser.ts +55 -0
  33. package/src/core.test.ts +147 -0
  34. package/src/core.ts +172 -0
  35. package/src/index.ts +4 -427
  36. package/src/server.ts +19 -0
  37. package/src/types.ts +38 -162
  38. package/src/web-context.test.ts +71 -0
  39. package/src/web-context.ts +227 -0
  40. package/src/web.ts +108 -0
  41. package/LICENSE +0 -21
  42. package/dist/anchor.d.ts +0 -8
  43. package/dist/holdy.cjs.js +0 -255
  44. package/dist/holdy.cjs.js.map +0 -7
  45. package/dist/holdy.esm.js +0 -255
  46. package/dist/holdy.esm.js.map +0 -7
  47. package/dist/holdy.iife.js +0 -255
  48. package/dist/holdy.iife.js.map +0 -7
  49. package/dist/outcome.d.ts +0 -19
  50. package/dist/queue.d.ts +0 -54
  51. package/dist/render/kinds/banner.d.ts +0 -3
  52. package/dist/render/kinds/checklist.d.ts +0 -19
  53. package/dist/render/kinds/cta.d.ts +0 -8
  54. package/dist/render/kinds/modal.d.ts +0 -16
  55. package/dist/render/kinds/shared.d.ts +0 -16
  56. package/dist/render/kinds/spotlight.d.ts +0 -17
  57. package/dist/render/kinds/tooltip.d.ts +0 -3
  58. package/dist/render/root.d.ts +0 -3
  59. package/dist/render/styles.d.ts +0 -5
  60. package/dist/store.d.ts +0 -36
  61. package/dist/transport.d.ts +0 -59
  62. package/src/anchor.ts +0 -72
  63. package/src/outcome.ts +0 -51
  64. package/src/queue.ts +0 -296
  65. package/src/render/kinds/banner.ts +0 -52
  66. package/src/render/kinds/checklist.ts +0 -175
  67. package/src/render/kinds/cta.ts +0 -53
  68. package/src/render/kinds/modal.ts +0 -129
  69. package/src/render/kinds/shared.ts +0 -67
  70. package/src/render/kinds/spotlight.ts +0 -176
  71. package/src/render/kinds/tooltip.ts +0 -82
  72. package/src/render/root.ts +0 -69
  73. package/src/render/styles.ts +0 -280
  74. package/src/store.ts +0 -106
  75. 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
- }
@@ -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
- }
@@ -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
- }