Skip to content

feat: Calendly#750

Merged
harlan-zw merged 7 commits intomainfrom
feat/calendly
May 8, 2026
Merged

feat: Calendly#750
harlan-zw merged 7 commits intomainfrom
feat/calendly

Conversation

@harlan-zw
Copy link
Copy Markdown
Collaborator

@harlan-zw harlan-zw commented May 7, 2026

🔗 Linked issue

Related to #177

❓ Type of change

  • 📖 Documentation
  • 🐞 Bug fix
  • 👌 Enhancement
  • ✨ New feature
  • 🧹 Chore
  • ⚠️ Breaking change

📚 Description

Adds Calendly to the registry as useScriptCalendly plus a headless <ScriptCalendlyInlineWidget> component for the most common embed shape. Bundled and proxied via assets.calendly.com with PRIVACY_IP_ONLY; the booking iframe still hits calendly.com directly since the vendor frame must load from origin.

🐞 Stylesheet leak fix (the bug analogous to #751's beacon-origin regression)

The composable used to inject <link rel=stylesheet href=https://assets.calendly.com/assets/external/widget.css> which leaked the visitor's IP to the vendor on every page render — and the url(/assets/external/close-icon.svg) reference inside that stylesheet leaked again on every popup close. The original e2e suite encoded this as expected behaviour (waitForSelector('link[href*="assets.calendly.com/assets/external/widget.css"]')), so the regression would have been invisible.

Fix: inline the 2.4 KB stylesheet via useHead({ style }), with the close-icon SVG embedded as a data URI. Verified live against https://calendly.com/<user>/30min: zero requests to assets.calendly.com in any of the three modes (inline / popup / badge).

🧪 Usage

Inline (component)

<script setup lang="ts">
const ready = ref(false)
</script>

<template>
  <ScriptCalendlyInlineWidget
    url="https://calendly.com/your-name/30min"
    @ready="ready = true"
  />
</template>

The component lazy-loads on visibility by default, exposes loading / awaitingLoad / error slots, and accepts prefill / utm / pageSettings / aboveTheFold / minHeight / trigger props. Mirrors <ScriptYouTubePlayer>.

Inline (composable)

<script setup lang="ts">
const { onLoaded } = useScriptCalendly()
onLoaded(({ Calendly }) => {
  Calendly.initInlineWidget({
    url: 'https://calendly.com/your-name/30min',
    parentElement: document.getElementById('calendly-inline')!,
  })
})
</script>

<template>
  <div id="calendly-inline" style="min-width: 320px; height: 700px" />
</template>

Popup

<script setup lang="ts">
const { proxy } = useScriptCalendly()
function open() {
  proxy.Calendly.initPopupWidget({
    url: 'https://calendly.com/your-name/30min',
    prefill: { name: 'Ada', email: 'ada@example.com' },
  })
}
</script>

Badge

<script setup lang="ts">
const { onLoaded } = useScriptCalendly()
onLoaded(({ Calendly }) => {
  Calendly.initBadgeWidget({
    url: 'https://calendly.com/your-name/30min',
    text: 'Schedule time with me',
    color: '#0069ff',
    textColor: '#ffffff',
  })
})
</script>

🤔 Why a component for inline only?

  • Inline needs a host element + lifecycle. That's exactly what Script*.vue components encode (cf. YouTube, Stripe, Vimeo). Without it consumers re-implement ref + onMounted + initInlineWidget({parentElement}) and get the SSR/trigger/cleanup boundaries wrong.
  • Popup and badge don't have a host element. Popup overlays body, badge floats fixed. Wrapping a single onMounted call would add no value, so they stay composable-only.
  • Pattern parity. Every other registry script with a renderable surface (YouTube, Vimeo, PayPal, Stripe) ships a component for that surface.

✅ What's covered

  • Live verification. Real Calendly account (https://calendly.com/<user>/30min); all three modes render in a real browser; zero requests to assets.calendly.com confirmed via performance.getEntriesByType('resource').
  • Bundled-asset privacy contract test. Node-fetches the real /_scripts/assets/<hash>.js (bypassing browser route stubs) and asserts the artefact exports initInlineWidget / initPopupWidget / initBadgeWidget and contains zero assets.calendly.com substrings — so any future regression that re-introduces the leak fails the suite.
  • Inline-stylesheet regression guard. E2e asserts no stylesheet link to assets.calendly.com is present and that the inline <style> carries both .calendly-spinner rules and a data:image/svg+xml close-icon URI.
  • iframe-mount contract. E2e clicks the trigger and asserts an iframe lands inside #calendly-host with the user's URL and embed_type=Inline.
  • Stub queue regression guard for feat: LinkedIn Insight Tag #741. Existing unit test asserts the queue captures the full args spread for every initialiser.
  • Proxy/first-party + types. proxy-configs unit test rows and types.test-d entry both updated. Full suite (51 files, 793 tests) green.

Adds the Calendly booking widget to the registry as `useScriptCalendly`,
covering inline, popup, and badge embeds with first-party proxy support
for `assets.calendly.com` and Partytown forwards for the widget initialisers.
@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented May 7, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
scripts-playground Ready Ready Preview, Comment May 8, 2026 5:46am

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 7, 2026

Open in StackBlitz

npm i https://pkg.pr.new/@nuxt/scripts@750

commit: f982359

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 7, 2026

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8a7cdc60-c204-43e9-97b8-ceed3b7e02d4

📥 Commits

Reviewing files that changed from the base of the PR and between 12b0222 and f982359.

📒 Files selected for processing (12)
  • FIRST_PARTY.md
  • docs/content/docs/1.guides/2.first-party.md
  • package.json
  • packages/script/src/registry-logos.ts
  • packages/script/src/registry-types.json
  • packages/script/src/registry.ts
  • packages/script/src/runtime/registry/schemas.ts
  • packages/script/src/runtime/types.ts
  • packages/script/src/script-meta.ts
  • playground/pages/index.vue
  • test/types/types.test-d.ts
  • test/unit/proxy-configs.test.ts

📝 Walkthrough

Walkthrough

This pull request introduces Calendly scheduling widget integration to Nuxt Scripts. The implementation provides a useScriptCalendly() composable that handles loading the Calendly external widget script with optional bundling and proxying, injects the widget stylesheet, and exposes a typed proxy wrapping window.Calendly methods. The integration includes widget variants for inline, popup, and badge embeds with support for prefill and UTM tracking options. A stub queue mechanism queues method calls until the SDK loads. The PR adds user documentation, playground demonstrations, comprehensive E2E test coverage for both bundled and CDN delivery modes, unit tests validating the stub queue and proxy configuration, and privacy tier declarations marking Calendly as PRIVACY_IP_ONLY with domain-level IP anonymization.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title 'feat: Calendly' directly and clearly summarizes the main change: adding Calendly support to the registry. It is concise and specific.
Description check ✅ Passed The PR description is comprehensive and directly related to the changeset. It explains the feature, the bug fix, usage patterns, and testing approach—all of which align with the file changes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/calendly

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
FIRST_PARTY.md (1)

116-148: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Clarify Calendly’s iframe exception in the table.

Current wording can imply all Calendly traffic is first-party proxied, but the booking iframe remains direct to calendly.com. Add a brief note to avoid privacy-scope confusion.

✏️ Suggested doc clarification
-| `calendly` | calendly | `PRIVACY_IP_ONLY` | Path A |
+| `calendly` | calendly | `PRIVACY_IP_ONLY` | Path A (script asset proxying; booking iframe remains direct to calendly.com) |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@FIRST_PARTY.md` around lines 116 - 148, The table entry for the `calendly`
config key (row showing `calendly` | `PRIVACY_IP_ONLY` | Path A) is ambiguous
about Calendly’s iframe being proxied; update that row to add a concise
clarifying note that while general Calendly integrations are treated as
PRIVACY_IP_ONLY via Path A, the booking iframe loads directly from calendly.com
and is not proxied (e.g., append “— booking iframe loads directly from
calendly.com, not proxied” or similar) so readers don’t assume full first‑party
proxy coverage.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/script/src/runtime/registry/calendly.ts`:
- Around line 34-40: The CalendlyInlineWidgetOptions interface currently types
parentElement as HTMLElement | string which allows invalid selector strings;
change parentElement to only HTMLElement in the CalendlyInlineWidgetOptions
declaration so callers must pass a DOM element, and update any call sites or
helper functions that constructed or accepted selector strings to instead
resolve the element (e.g., querySelector) before passing it to the
CalendlyInlineWidgetOptions constructor or to the functions that consume
parentElement; ensure references to parentElement in functions like the inline
widget renderer treat it as an HTMLElement (no string handling) and add runtime
checks where appropriate to throw a clear error if a non-HTMLElement is
supplied.

---

Outside diff comments:
In `@FIRST_PARTY.md`:
- Around line 116-148: The table entry for the `calendly` config key (row
showing `calendly` | `PRIVACY_IP_ONLY` | Path A) is ambiguous about Calendly’s
iframe being proxied; update that row to add a concise clarifying note that
while general Calendly integrations are treated as PRIVACY_IP_ONLY via Path A,
the booking iframe loads directly from calendly.com and is not proxied (e.g.,
append “— booking iframe loads directly from calendly.com, not proxied” or
similar) so readers don’t assume full first‑party proxy coverage.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3e0facc9-42af-4d83-bbe3-70f9832d2578

📥 Commits

Reviewing files that changed from the base of the PR and between 6ddc9f6 and 7454fcc.

📒 Files selected for processing (25)
  • FIRST_PARTY.md
  • docs/content/docs/1.guides/2.first-party.md
  • docs/content/scripts/calendly.md
  • packages/script/src/registry-logos.ts
  • packages/script/src/registry-types.json
  • packages/script/src/registry.ts
  • packages/script/src/runtime/registry/calendly.ts
  • packages/script/src/runtime/registry/schemas.ts
  • packages/script/src/runtime/types.ts
  • packages/script/src/script-meta.ts
  • playground/pages/index.vue
  • playground/pages/third-parties/calendly/default.vue
  • playground/pages/third-parties/calendly/nuxt-scripts.vue
  • test/e2e/_calendly-suite.ts
  • test/e2e/calendly-cdn.test.ts
  • test/e2e/calendly.test.ts
  • test/fixtures/calendly-cdn/nuxt.config.ts
  • test/fixtures/calendly-cdn/tsconfig.json
  • test/fixtures/calendly/app.vue
  • test/fixtures/calendly/nuxt.config.ts
  • test/fixtures/calendly/package.json
  • test/fixtures/calendly/pages/index.vue
  • test/fixtures/calendly/tsconfig.json
  • test/types/types.test-d.ts
  • test/unit/proxy-configs.test.ts

Comment thread packages/script/src/runtime/registry/calendly.ts
The calendly e2e tests failed in CI because:
- The fixtures lacked a `prepare:fixtures` entry, so `.nuxt/tsconfig.json`
  was missing when Vite parsed the page that imports `useScriptCalendly`.
- The cdn fixture had no per-call `bundle: false`, so the script was
  always served from /_scripts/assets/ (proxy) instead of the CDN.
- The stub queue e2e was racing the real script load (`onNuxtReady`)
  and is now a deterministic unit test.

Restructure pages to mirror the linkedin-insight fixtures: empty index,
composable usage on `/calendly`. cdn fixture now overrides the page +
registry config to disable bundling. Add unit test guarding the
multi-arg push regression (#741) on the stub queue.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
test/unit/calendly-stub-queue.test.ts (1)

53-62: ⚡ Quick win

Missing queue-length guard in the multi-arg test

The second test asserts the shape of stub.q[0] but never verifies that exactly one entry was enqueued. If a future change to the stub accidentally pushes the entry multiple times (e.g., once per arg), the assertion on q[0] alone would still pass.

🛡️ Proposed fix
  it('preserves multiple positional args (showPopupWidget(url, ...))', () => {
    const stub = createStub()
    stub.showPopupWidget('https://calendly.com/example/30min', { foo: 'bar' }, 42)
+   expect(stub.q).toHaveLength(1)
    expect(stub.q[0]).toEqual([
      'showPopupWidget',
      'https://calendly.com/example/30min',
      { foo: 'bar' },
      42,
    ])
  })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/unit/calendly-stub-queue.test.ts` around lines 53 - 62, The test for
createStub's showPopupWidget should assert the queue length before inspecting
stub.q[0]; add an explicit expectation that stub.q.length === 1 (or toEqual(1))
immediately after calling stub.showPopupWidget to ensure exactly one entry was
enqueued, then keep the existing equality check on stub.q[0]; reference
createStub, stub.showPopupWidget and stub.q when making this change.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/fixtures/calendly/pages/calendly.vue`:
- Around line 11-14: The call to proxy.Calendly.initInlineWidget uses a CSS
selector string for parentElement but Calendly expects an actual DOM node;
change the invocation so parentElement is a resolved HTMLElement (e.g., obtain
the element via document.getElementById('calendly-host') or
document.querySelector('#calendly-host') and pass that variable into
proxy.Calendly.initInlineWidget) to ensure the widget mounts inside the intended
container.

---

Nitpick comments:
In `@test/unit/calendly-stub-queue.test.ts`:
- Around line 53-62: The test for createStub's showPopupWidget should assert the
queue length before inspecting stub.q[0]; add an explicit expectation that
stub.q.length === 1 (or toEqual(1)) immediately after calling
stub.showPopupWidget to ensure exactly one entry was enqueued, then keep the
existing equality check on stub.q[0]; reference createStub, stub.showPopupWidget
and stub.q when making this change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 47210afd-2374-4e5b-b53e-cc841be0a607

📥 Commits

Reviewing files that changed from the base of the PR and between 7454fcc and c1a9b11.

📒 Files selected for processing (8)
  • package.json
  • test/e2e/_calendly-suite.ts
  • test/fixtures/calendly-cdn/nuxt.config.ts
  • test/fixtures/calendly-cdn/pages/calendly.vue
  • test/fixtures/calendly/nuxt.config.ts
  • test/fixtures/calendly/pages/calendly.vue
  • test/fixtures/calendly/pages/index.vue
  • test/unit/calendly-stub-queue.test.ts
✅ Files skipped from review due to trivial changes (3)
  • package.json
  • test/fixtures/calendly/nuxt.config.ts
  • test/fixtures/calendly-cdn/pages/calendly.vue
🚧 Files skipped from review as they are similar to previous changes (3)
  • test/fixtures/calendly/pages/index.vue
  • test/fixtures/calendly-cdn/nuxt.config.ts
  • test/e2e/_calendly-suite.ts

Comment thread test/fixtures/calendly/pages/calendly.vue
Calendly's `initInlineWidget` API requires a DOM element reference;
CSS selector strings are not supported. Tighten the type and update
fixtures to resolve the element via `querySelector` first.

Per CodeRabbit review on #750.
The composable injected `<link rel=stylesheet href=https://assets.calendly.com/assets/external/widget.css>`
which leaked the visitor IP to the vendor on every page render — and the
`url(/assets/external/close-icon.svg)` reference inside that stylesheet
leaked again on every popup-close. This bypassed the bundle/proxy
posture the registry advertises (`proxy.privacy: PRIVACY_IP_ONLY`).

Inlines the 2.4 KB stylesheet via `useHead({ style })`, with the
close-icon SVG embedded as a data URI. No more requests to
`assets.calendly.com` at any point in the widget lifecycle.

Adds `<ScriptCalendlyInlineWidget>` for the inline embed shape (popup
and badge stay composable-only, since they have no host element).
Mirrors `<ScriptYouTubePlayer>` — visibility trigger by default,
`above-the-fold` preconnect, slots for loading/awaiting/error.

Hardens the e2e suite to match the Ahrefs bar: asserts no
`assets.calendly.com` stylesheet link is present, that the inline
`<style>` carries the data-URI close icon, and that
`initInlineWidget` mounts a real iframe in the requested
parentElement. Adds a Node-fetch contract test that asserts the
bundled artefact still exports the widget API and contains no
`assets.calendly.com` references.

Docs aligned with the post-#751 layout (frontmatter component link,
`script-types` at end, code-group for proxy/onLoaded, dedicated
`<ScriptCalendlyInlineWidget>` section).
Comment thread test/e2e/_calendly-suite.ts Fixed
CodeQL flagged the substring `.includes('assets.calendly.com')` as
js/incomplete-url-substring-sanitization (false positive — this is a
test-side leak detector, not a server-side allowlist), but the lint
gates the PR. Parse with `new URL(l.href).hostname` to satisfy it.
# Conflicts:
#	FIRST_PARTY.md
#	docs/content/docs/1.guides/2.first-party.md
#	package.json
#	packages/script/src/registry-types.json
#	packages/script/src/runtime/registry/schemas.ts
#	packages/script/src/runtime/types.ts
#	playground/pages/index.vue
#	test/unit/proxy-configs.test.ts
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
packages/script/src/runtime/components/ScriptCalendlyInlineWidget.vue (1)

10-31: ⚡ Quick win

CalendlyPrefill, CalendlyUtm, and CalendlyPageSettings are redeclared locally instead of imported.

These three interfaces are also defined in calendly.ts (non-exported), forcing duplication. More importantly, the local CalendlyPageSettings here adds hideGdprBanner (line 28) which is absent from the version in calendly.ts, creating a silent type divergence: users of the raw useScriptCalendly composable can't type-safely pass hideGdprBanner to initInlineWidget options, even though Calendly's SDK accepts it.

Exporting the canonical types from calendly.ts (after adding hideGdprBanner there) and importing them here removes the duplication and the inconsistency in one step.

♻️ Proposed changes

In calendly.ts, export the shared interfaces and add the missing field:

-interface CalendlyPrefill {
+export interface CalendlyPrefill {
   name?: string
   email?: string
   firstName?: string
   lastName?: string
   customAnswers?: Record<string, string>
 }

-interface CalendlyUtm {
+export interface CalendlyUtm {
   utmCampaign?: string
   utmSource?: string
   utmMedium?: string
   utmContent?: string
   utmTerm?: string
 }

-interface CalendlyPageSettings {
+export interface CalendlyPageSettings {
   backgroundColor?: string
   hideEventTypeDetails?: boolean
   hideLandingPageDetails?: boolean
+  hideGdprBanner?: boolean
   primaryColor?: string
   textColor?: string
 }

In ScriptCalendlyInlineWidget.vue, replace the local re-declarations with imports:

-interface CalendlyPrefill {
-  name?: string
-  email?: string
-  firstName?: string
-  lastName?: string
-  customAnswers?: Record<string, string>
-}
-interface CalendlyUtm {
-  utmCampaign?: string
-  utmSource?: string
-  utmMedium?: string
-  utmContent?: string
-  utmTerm?: string
-}
-interface CalendlyPageSettings {
-  backgroundColor?: string
-  hideEventTypeDetails?: boolean
-  hideLandingPageDetails?: boolean
-  hideGdprBanner?: boolean
-  primaryColor?: string
-  textColor?: string
-}
+import type { CalendlyPrefill, CalendlyUtm, CalendlyPageSettings } from '../registry/calendly'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/script/src/runtime/components/ScriptCalendlyInlineWidget.vue` around
lines 10 - 31, The three local interfaces (CalendlyPrefill, CalendlyUtm,
CalendlyPageSettings) in ScriptCalendlyInlineWidget.vue duplicate types defined
in calendly.ts and introduce a missing field (hideGdprBanner) divergence; fix by
exporting the canonical interfaces from calendly.ts (add hideGdprBanner to
CalendlyPageSettings there), then remove the local redeclarations and import
CalendlyPrefill, CalendlyUtm, and CalendlyPageSettings into
ScriptCalendlyInlineWidget.vue so useScriptCalendly/initInlineWidget consumers
have a single, type-safe source of truth.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/script/src/runtime/components/ScriptCalendlyInlineWidget.vue`:
- Around line 92-95: The watch on status (watch(status, (s) => { if (s ===
'error') emit('error') })) can miss a pre-existing error because it registers
with the default immediate: false; update the component so that the current
status is checked at registration by either adding the watch option { immediate:
true } or explicitly checking if status === 'error' and calling emit('error')
during setup/mount; reference the existing watch(status, ...) and the
emit('error') call when making the change.

In `@packages/script/src/runtime/registry/calendly.ts`:
- Around line 26-32: The CalendlyPageSettings interface is missing the
hideGdprBanner property which causes a type mismatch with
ScriptCalendlyInlineWidget.vue and prevents callers of useScriptCalendly (and
types CalendlyInlineWidgetOptions / CalendlyPopupWidgetOptions that compose
CalendlyPageSettings) from passing this flag type-safely; update the
CalendlyPageSettings interface to include hideGdprBanner?: boolean so the
exported types align with the local copy in ScriptCalendlyInlineWidget.vue and
downstream option types accept the field.
- Around line 89-103: Remove the module-level boolean cssInjected and its
short-circuit in ensureCalendlyStylesheet so that useHead is always called
(unless import.meta.server) — rely on the CALENDLY_CSS_KEY de-duplication
instead; update ensureCalendlyStylesheet (referenced by useScriptCalendly) to
only skip on import.meta.server and then call useHead({ style: [{ key:
CALENDLY_CSS_KEY, innerHTML: CALENDLY_CSS }] }) so unhead lifecycle can
re-inject the stylesheet correctly after SPA navigation.

---

Nitpick comments:
In `@packages/script/src/runtime/components/ScriptCalendlyInlineWidget.vue`:
- Around line 10-31: The three local interfaces (CalendlyPrefill, CalendlyUtm,
CalendlyPageSettings) in ScriptCalendlyInlineWidget.vue duplicate types defined
in calendly.ts and introduce a missing field (hideGdprBanner) divergence; fix by
exporting the canonical interfaces from calendly.ts (add hideGdprBanner to
CalendlyPageSettings there), then remove the local redeclarations and import
CalendlyPrefill, CalendlyUtm, and CalendlyPageSettings into
ScriptCalendlyInlineWidget.vue so useScriptCalendly/initInlineWidget consumers
have a single, type-safe source of truth.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6e92d3c9-5b4c-4a1c-be3c-b56a9a0137bc

📥 Commits

Reviewing files that changed from the base of the PR and between 47fc93b and 12b0222.

📒 Files selected for processing (5)
  • docs/content/scripts/calendly.md
  • packages/script/src/runtime/components/ScriptCalendlyInlineWidget.vue
  • packages/script/src/runtime/registry/calendly.ts
  • playground/pages/third-parties/calendly/nuxt-scripts.vue
  • test/e2e/_calendly-suite.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • docs/content/scripts/calendly.md
  • test/e2e/_calendly-suite.ts

Comment on lines +92 to +95
watch(status, (s) => {
if (s === 'error')
emit('error')
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

watch on status registered with default immediate: false may miss a pre-existing error state.

If the script context entered 'error' before this component mounts (e.g., a shared useRegistryScript instance that already failed on a previous navigation), the error event is never emitted because watch doesn't fire for the value present at registration time.

🐛 Proposed fix
-  watch(status, (s) => {
+  watch(status, (s) => {
     if (s === 'error')
       emit('error')
-  })
+  }, { immediate: true })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
watch(status, (s) => {
if (s === 'error')
emit('error')
})
watch(status, (s) => {
if (s === 'error')
emit('error')
}, { immediate: true })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/script/src/runtime/components/ScriptCalendlyInlineWidget.vue` around
lines 92 - 95, The watch on status (watch(status, (s) => { if (s === 'error')
emit('error') })) can miss a pre-existing error because it registers with the
default immediate: false; update the component so that the current status is
checked at registration by either adding the watch option { immediate: true } or
explicitly checking if status === 'error' and calling emit('error') during
setup/mount; reference the existing watch(status, ...) and the emit('error')
call when making the change.

Comment on lines +26 to +32
interface CalendlyPageSettings {
backgroundColor?: string
hideEventTypeDetails?: boolean
hideLandingPageDetails?: boolean
primaryColor?: string
textColor?: string
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

CalendlyPageSettings is missing hideGdprBanner.

Calendly's SDK accepts hideGdprBanner as a page setting. It is absent here but present in ScriptCalendlyInlineWidget.vue's local copy of the same interface (causing the type divergence noted in the component review). Users of useScriptCalendly composing CalendlyInlineWidgetOptions or CalendlyPopupWidgetOptions directly cannot pass this field in a type-safe way.

🐛 Proposed fix
 interface CalendlyPageSettings {
   backgroundColor?: string
   hideEventTypeDetails?: boolean
   hideLandingPageDetails?: boolean
+  hideGdprBanner?: boolean
   primaryColor?: string
   textColor?: string
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
interface CalendlyPageSettings {
backgroundColor?: string
hideEventTypeDetails?: boolean
hideLandingPageDetails?: boolean
primaryColor?: string
textColor?: string
}
interface CalendlyPageSettings {
backgroundColor?: string
hideEventTypeDetails?: boolean
hideLandingPageDetails?: boolean
hideGdprBanner?: boolean
primaryColor?: string
textColor?: string
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/script/src/runtime/registry/calendly.ts` around lines 26 - 32, The
CalendlyPageSettings interface is missing the hideGdprBanner property which
causes a type mismatch with ScriptCalendlyInlineWidget.vue and prevents callers
of useScriptCalendly (and types CalendlyInlineWidgetOptions /
CalendlyPopupWidgetOptions that compose CalendlyPageSettings) from passing this
flag type-safely; update the CalendlyPageSettings interface to include
hideGdprBanner?: boolean so the exported types align with the local copy in
ScriptCalendlyInlineWidget.vue and downstream option types accept the field.

Comment on lines +89 to +103
let cssInjected = false

function ensureCalendlyStylesheet() {
if (import.meta.server || cssInjected)
return
cssInjected = true
useHead({
style: [
{
key: CALENDLY_CSS_KEY,
innerHTML: CALENDLY_CSS,
},
],
})
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

cssInjected module-level flag causes the Calendly stylesheet to disappear after SPA navigation.

When a component is unmounted, any head entries created by that component are automatically removed by unhead. Because ensureCalendlyStylesheet() is called inside useScriptCalendly() during component setup(), the useHead entry is component-scoped. On the first visit to a Calendly page the style is injected and cssInjected flips to true. When the user navigates away, the component unmounts and unhead removes the <style> tag. On the next visit, ensureCalendlyStylesheet() returns early because cssInjected is still true — the style is never re-injected, leaving the widget unstyled.

The fix is to drop the module-level flag entirely and let unhead's key handle deduplication. Multiple co-existing components on the same page calling useHead with the same key are safely collapsed to one <style> tag; when any one of them mounts or unmounts, the entry lifecycle follows naturally.

🐛 Proposed fix
-let cssInjected = false
-
 function ensureCalendlyStylesheet() {
-  if (import.meta.server || cssInjected)
+  if (import.meta.server)
     return
-  cssInjected = true
   useHead({
     style: [
       {
         key: CALENDLY_CSS_KEY,
         innerHTML: CALENDLY_CSS,
       },
     ],
   })
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/script/src/runtime/registry/calendly.ts` around lines 89 - 103,
Remove the module-level boolean cssInjected and its short-circuit in
ensureCalendlyStylesheet so that useHead is always called (unless
import.meta.server) — rely on the CALENDLY_CSS_KEY de-duplication instead;
update ensureCalendlyStylesheet (referenced by useScriptCalendly) to only skip
on import.meta.server and then call useHead({ style: [{ key: CALENDLY_CSS_KEY,
innerHTML: CALENDLY_CSS }] }) so unhead lifecycle can re-inject the stylesheet
correctly after SPA navigation.

@harlan-zw harlan-zw merged commit e852443 into main May 8, 2026
18 of 19 checks passed
@harlan-zw harlan-zw deleted the feat/calendly branch May 8, 2026 05:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants