{"_id":"@criclabs/universal-floating-contact-button","_rev":"5-61fa95e83d63f1cb6b7b8a8e1536c9df","name":"@criclabs/universal-floating-contact-button","dist-tags":{"latest":"1.0.3"},"versions":{"1.0.0":{"name":"@criclabs/universal-floating-contact-button","version":"1.0.0","keywords":["widget","embed","contact-widget","universal-floating-contact-button","lit","web-components","floating-button"],"license":"MIT","_id":"@criclabs/universal-floating-contact-button@1.0.0","maintainers":[{"name":"criclabs","email":"dev@criclabs.co"}],"dist":{"shasum":"ef58f9f7bee88458070fc48de286cbf0589c98a5","tarball":"https://registry.npmjs.org/@criclabs/universal-floating-contact-button/-/universal-floating-contact-button-1.0.0.tgz","fileCount":3,"integrity":"sha512-Ne3OSBb0NOh7s6hsUf8DnU6qPW/ILoqizI+Vyi+0C9Dn4RBlMrXJnQmJ9herGCOpT/xMC9STdUJUX9T+iWIc/A==","signatures":[{"sig":"MEUCIHgtsLCV0tJ0Ubofj3IYon76PHFzVz/eR8D2G2zg0hhdAiEAok4IQSy/PHJ6h1aFKZjrHrHrAeK7e00xz1/fWQMsvF8=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":64164},"main":"dist/widget.v1.js","type":"module","unpkg":"dist/widget.v1.js","browser":"dist/widget.v1.js","gitHead":"35a80b80b0978ef25e9d24afe955614c15872848","scripts":{"dev":"vite","build":"tsc && vite build","prepack":"npm run build","preview":"vite preview","typecheck":"tsc --noEmit"},"_npmUser":{"name":"criclabs","email":"dev@criclabs.co"},"jsdelivr":"dist/widget.v1.js","_npmVersion":"10.9.4","description":"Embeddable, framework-agnostic floating contact widget (Phone, LINE, Messenger, and more) built with Lit + Tailwind CSS, shipped as a single standalone script.","directories":{},"_nodeVersion":"22.21.1","dependencies":{"lit":"^3.2.1","lucide":"^0.468.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"vite":"^5.4.11","postcss":"^8.4.49","typescript":"^5.7.2","@types/node":"^22.10.2","tailwindcss":"^3.4.17","autoprefixer":"^10.4.20"},"_npmOperationalInternal":{"tmp":"tmp/universal-floating-contact-button_1.0.0_1786648837066_0.2563659936028251","host":"s3://npm-registry-packages-npm-production"}},"1.0.1":{"name":"@criclabs/universal-floating-contact-button","version":"1.0.1","keywords":["widget","embed","contact-widget","universal-floating-contact-button","lit","web-components","floating-button"],"license":"MIT","_id":"@criclabs/universal-floating-contact-button@1.0.1","maintainers":[{"name":"criclabs","email":"dev@criclabs.co"}],"dist":{"shasum":"ededbfac9ad4aa6653b930067bb6c16550fbcf49","tarball":"https://registry.npmjs.org/@criclabs/universal-floating-contact-button/-/universal-floating-contact-button-1.0.1.tgz","fileCount":3,"integrity":"sha512-b15TIBp01Vi5YeywB6+KVnsTsU8FZ/qwciYmgfQhJt6BI38ER3xDOge3Hc03nAXeLHWToju8aumXy9siEOdzpA==","signatures":[{"sig":"MEUCIFXj74zoJDenSq7tl9mNN9amTFldgc9zdE9qLabV3qX4AiEApCi4Rr67nefgpt0CEaUpGX7fH267xygw2O39QubAJ58=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":65477},"main":"dist/widget.v1.js","type":"module","unpkg":"dist/widget.v1.js","browser":"dist/widget.v1.js","gitHead":"f7042869057e37a276ff0a8d72275d295f96770d","scripts":{"dev":"vite","build":"tsc && vite build","prepack":"npm run build","preview":"vite preview","typecheck":"tsc --noEmit"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:ea268bb0-ebe5-4fd6-b864-2de023ffebfe"}},"jsdelivr":"dist/widget.v1.js","_npmVersion":"11.17.0","description":"Embeddable, framework-agnostic floating contact widget (Phone, LINE, Messenger, and more) built with Lit + Tailwind CSS, shipped as a single standalone script.","directories":{},"_nodeVersion":"24.19.0","dependencies":{"lit":"^3.2.1","lucide":"^0.468.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"vite":"^5.4.11","postcss":"^8.4.49","typescript":"^5.7.2","@types/node":"^22.10.2","tailwindcss":"^3.4.17","autoprefixer":"^10.4.20"},"_npmOperationalInternal":{"tmp":"tmp/universal-floating-contact-button_1.0.1_1786656746075_0.3965588431030498","host":"s3://npm-registry-packages-npm-production"}},"1.0.3":{"name":"@criclabs/universal-floating-contact-button","version":"1.0.3","description":"Embeddable, framework-agnostic floating contact widget (Phone, LINE, Messenger, and more) built with Lit + Tailwind CSS, shipped as a single standalone script.","type":"module","main":"dist/widget.v1.js","browser":"dist/widget.v1.js","jsdelivr":"dist/widget.v1.js","unpkg":"dist/widget.v1.js","scripts":{"dev":"vite","build":"tsc && vite build","prepack":"npm run build","preview":"vite preview","typecheck":"tsc --noEmit"},"keywords":["widget","embed","contact-widget","universal-floating-contact-button","lit","web-components","floating-button"],"license":"MIT","publishConfig":{"access":"public"},"dependencies":{"lit":"^3.2.1","lucide":"^0.468.0"},"devDependencies":{"@types/node":"^22.10.2","autoprefixer":"^10.4.20","postcss":"^8.4.49","tailwindcss":"^3.4.17","typescript":"^5.7.2","vite":"^5.4.11"},"gitHead":"c19e3dc7ee3e077765eb95534595e297b43fc9b3","_id":"@criclabs/universal-floating-contact-button@1.0.3","_nodeVersion":"24.19.0","_npmVersion":"11.17.0","dist":{"integrity":"sha512-dpAzqyYv1UehtBbSAk3h930w6m4EfUwbN6jLqW5iCcZwQDSpH/eLhcY+guYVXUdSnIA77Yg49p2NtVsdijcMgA==","shasum":"471a804e921afcaae73013c2e392600f69904ae4","tarball":"https://registry.npmjs.org/@criclabs/universal-floating-contact-button/-/universal-floating-contact-button-1.0.3.tgz","fileCount":45,"unpackedSize":198114,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIF/pxdDVafgOjj5OGwQ1+l2AlgeEWwNZV43MXyKAArXeAiEApVf4LgagwTtb4ekQS8Wh8/pIkIK01AVGcn/91wCua1w="}]},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:ea268bb0-ebe5-4fd6-b864-2de023ffebfe"}},"directories":{},"maintainers":[{"name":"dev-criclabs","email":"dev@criclabs.co"},{"name":"teamdisc","email":"team@criclabs.co"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/universal-floating-contact-button_1.0.3_1787540616013_0.9745261280980893"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-13T19:20:36.842Z","modified":"2026-08-24T03:03:36.334Z","1.0.0":"2026-08-13T19:20:37.228Z","1.0.1":"2026-08-13T21:32:26.224Z","1.0.3":"2026-08-24T03:03:36.151Z"},"license":"MIT","keywords":["widget","embed","contact-widget","universal-floating-contact-button","lit","web-components","floating-button"],"description":"Embeddable, framework-agnostic floating contact widget (Phone, LINE, Messenger, and more) built with Lit + Tailwind CSS, shipped as a single standalone script.","maintainers":[{"name":"dev-criclabs","email":"dev@criclabs.co"},{"name":"teamdisc","email":"team@criclabs.co"}],"readme":"# Universal Floating Contact Button\n\nA tiny, embeddable \"floating contact button\" — the classic bottom-right bubble\nthat expands into Phone / LINE / Messenger / WhatsApp / etc. buttons. Ships as\n**one standalone `<script>` file** (`dist/widget.v1.js`) that any website can\ndrop in, with zero build step or framework dependency required on the host\nsite (plain HTML, WordPress, Shopify, Webflow, LCS, whatever).\n\nBuilt with [Lit](https://lit.dev) (Web Components + Shadow DOM) and\n[Tailwind CSS](https://tailwindcss.com) compiled inline, bundled by\n[Vite](https://vitejs.dev) into a single IIFE.\n\n## Why Shadow DOM?\n\nThe widget renders inside a Shadow Root, so:\n\n- The host site's global CSS can never restyle/break the widget (no more\n  `button { border-radius: 0 !important }` fights).\n- The widget's own styles never leak out and affect the host page.\n\nSee `index.html` for a dev page that intentionally ships hostile global CSS\nto prove this isolation.\n\n## Project structure\n\n```\n├── src/\n│   ├── components/widget.ts   # <contact-widget-element> — the Lit component\n│   ├── assets/icons/           # Heroicons, Remix, and Tabler SVG variants\n│   ├── icon-variants.ts        # Maps widget/button types to variant SVG assets\n│   ├── icons.ts                # Inline fallback renderers and close icon\n│   ├── button-presets.ts       # Per-channel default colors/labels/icons\n│   ├── types.ts                # WidgetConfig / ButtonConfig public types\n│   ├── utils/link-resolver.ts  # Turns phone/LINE-ID/FB-URL shorthand into real links\n│   ├── index.ts                # SDK entry point — mounts the element + processes the command queue\n│   ├── styles.css               # Tailwind directives (compiled + inlined into the Shadow Root)\n│   └── vite-env.d.ts\n├── index.html                  # Dev-only test/demo page\n├── vite.config.ts              # Builds a single dist/widget.v1.js IIFE\n├── .github/workflows/\n│   └── publish-package.yml      # Publishes tagged releases to npm\n├── tailwind.config.js\n├── postcss.config.js\n└── tsconfig.json\n```\n\n## Getting started\n\n```bash\nnpm install\nnpm run dev       # http://localhost:5173 — live dev/test page\nnpm run build     # -> dist/widget.v1.js (single file, ready to deploy/CDN)\nnpm run typecheck # tsc --noEmit\n```\n\n## Distribution via jsDelivr\n\nThe built widget is published as part of the\n`@criclabs/universal-floating-contact-button` npm package.\njsDelivr mirrors public npm packages automatically, so no S3 bucket,\nCloudFront distribution, deploy credentials, or cache invalidation is needed.\nThe package includes `dist/widget.v1.js`, with its default CSS and icons\nalready bundled into that file.\n\nUse a major-version URL in production so compatible v1 updates are picked up\nwithout unexpectedly upgrading to a future breaking release:\n\n```text\nhttps://cdn.jsdelivr.net/npm/@criclabs/universal-floating-contact-button@1/dist/widget.v1.js\n```\n\nFor a release that must never change, pin the exact package version instead,\nfor example `@criclabs/universal-floating-contact-button@1.0.0`.\n\n### Release process\n\nReleases move from `develop` to `main` through a dedicated release PR. npm\nTrusted Publishing must be configured for this repository and\n`publish-package.yml`; no long-lived npm token is required.\n\n1. Update `develop`, then create a release branch named for the version being\n   prepared:\n\n   ```bash\n   git checkout develop\n   git pull\n   git checkout -b release/v1.0.1\n   ```\n\n2. Choose the appropriate [semantic version](https://semver.org/) increment\n   and update the package metadata without creating a Git tag:\n\n   ```bash\n   npm version patch --no-git-tag-version # or minor / major\n   git add package.json package-lock.json\n   git commit -m \"Release v1.0.1\"\n   git push -u origin release/v1.0.1\n   ```\n\n3. Open a PR from `release/v1.0.1` to `main` titled `Release v1.0.1`.\n   Complete the normal review and merge process.\n\n4. Merging a `release/v*` PR into `main` triggers `publish-package.yml`. The\n   workflow verifies that the branch name matches the version in\n   `package.json`, type-checks and builds the widget, tags the merge commit,\n   publishes it to npm, and fast-forwards `develop` to the released commit.\n   jsDelivr then mirrors the new package version automatically.\n\n   The repository must provide a `RELEASE_TOKEN` Actions secret for an admin\n   user. The token needs repository contents write access so the workflow can\n   create the tag and bypass `develop`'s pull-request requirement. npm itself\n   continues to use Trusted Publishing and does not require an npm token.\n\n5. Verify the release on npm and at the version-pinned CDN URL:\n\n   ```text\n   https://cdn.jsdelivr.net/npm/@criclabs/universal-floating-contact-button@1.0.1/dist/widget.v1.js\n   ```\n\nThe workflow still supports manually pushed `v*` tags as a recovery path. A\nmanual tag must match the version in `package.json`; it publishes the package\nbut does not update `develop`.\n\nPublished npm versions are immutable. If a release needs a correction, create\na new patch version rather than attempting to replace the existing version.\n\n## Embedding on any website\n\nPaste this snippet before `</body>` on any page:\n\n```html\n<script>\n  (function (w, d) {\n    if (w.ContactWidget) return;\n    var q = [];\n    function ContactWidget() {\n      q.push(arguments);\n    }\n    ContactWidget.q = q;\n    w.ContactWidget = ContactWidget;\n    var s = d.createElement('script');\n    s.async = true;\n    s.src = 'https://cdn.jsdelivr.net/npm/@criclabs/universal-floating-contact-button@1/dist/widget.v1.js';\n    d.body.appendChild(s);\n  })(window, document);\n\n  ContactWidget('init', {\n    color: '#EA1C24',\n    iconVariant: 'heroicon',\n    label: 'Contact us',\n    buttons: [\n      { type: 'phone', value: '0869828288' },\n      { type: 'line', value: '@LCSGROUP' },\n      { type: 'messenger', value: 'https://www.facebook.com/lcsgroupthailand' },\n    ],\n  });\n</script>\n```\n\nBecause calls are queued into `ContactWidget.q` until the real script\nfinishes downloading, **the `init` call can appear immediately after the\nloader** (as above) — there's no race condition, and it works the same way\nwhether the widget script has loaded yet or not. This is the same pattern\nused by Intercom, Google Analytics, Crisp, etc.\n\n### Real example — LCS Group Thailand\n\nThis is the exact config used in `index.html` to test the widget with real\nproduction contact info:\n\n```js\nContactWidget('init', {\n  color: '#EA1C24',\n  iconVariant: 'heroicon',\n  label: 'ติดต่อเรา',\n  buttons: [\n    { type: 'phone', value: '0869828288', label: 'โทรเลย' },\n    { type: 'line', value: '@LCSGROUP', label: 'แชททาง LINE' },\n    { type: 'messenger', value: 'https://www.facebook.com/lcsgroupthailand', label: 'Messenger' },\n  ],\n});\n```\n\nNote that the Messenger button's `value` is the **Facebook Page URL** — the\nwidget automatically rewrites `facebook.com/<page>` into the correct\n`https://m.me/<page>` deep link so the button opens a Messenger chat, not\nthe Facebook page itself.\n\n## Configuration reference\n\n### `WidgetConfig`\n\n| Property        | Type                          | Default          | Description                                                        |\n| ---------------- | ----------------------------- | ----------------- | -------------------------------------------------------------------- |\n| `color`          | `string` (CSS color)           | `#EA1C24`         | Main button color.                                                  |\n| `iconVariant`    | `'heroicon' \\| 'remix' \\| 'tabler'` | `'heroicon'` | Built-in icon set for the toggle and channel buttons.               |\n| `buttonSize`     | `number` (px)                  | `48`              | Diameter of the main toggle and inner channel buttons.               |\n| `position`       | `'bottom-right' \\| 'bottom-left'` | `'bottom-right'` | Which corner the widget docks to.                                    |\n| `offsetX`        | `number` (px)                  | `32`              | Horizontal distance from the viewport edge.                          |\n| `offsetY`        | `number` (px)                  | `32`              | Vertical distance from the viewport edge.                            |\n| `pulse`          | `boolean`                      | `false`           | Animated attention-grabbing ring around the closed button.           |\n| `pulseScale`     | `number`                       | `2.4`             | Maximum pulse size relative to the circular button.                   |\n| `pulseDuration`  | `number` (ms)                  | `1800`            | Duration of each pulse cycle.                                         |\n| `label`          | `string`                       | —                 | Tooltip pill shown next to the closed button (e.g. `\"Contact us\"`).  |\n| `showLabels`     | `boolean`                      | `false`           | Always show each button's label as a pill instead of only on hover.  |\n| `autoOpenDelay`  | `number` (ms)                  | —                 | Auto-expand the menu this many ms after the widget mounts.           |\n| `zIndex`         | `number`                       | `999999`          | Stacking order override.                                             |\n| `buttons`        | `ButtonConfig[]`               | **required**      | The channel buttons to render, top to bottom.                        |\n\n### Icon variants\n\nSet `iconVariant` at widget level to use a consistent icon family, then set\n`buttons[].iconVariant` when one button needs a different family. A per-button\n`icon` URL still takes priority over both variant settings. The close icon is\nshared across variants.\n\n| Icon | Heroicons (`heroicon`) | Remix (`remix`) | Tabler (`tabler`) |\n| --- | --- | --- | --- |\n| Widget toggle | ![Heroicons widget](./src/assets/icons/heroicon/heroicon_widget.svg) | ![Remix widget](./src/assets/icons/remix/remix_widget.svg) | ![Tabler widget](./src/assets/icons/tabler/tabler_widget.svg) |\n| Phone | ![Heroicons phone](./src/assets/icons/heroicon/heroicon_phone.svg) | ![Remix phone](./src/assets/icons/remix/remix_phone.svg) | ![Tabler phone](./src/assets/icons/tabler/tabler_phone.svg) |\n| LINE | ![Heroicons LINE](./src/assets/icons/heroicon/heroicon_line.svg) | ![Remix LINE](./src/assets/icons/remix/remix_line.svg) | ![Tabler LINE](./src/assets/icons/tabler/tabler_line.svg) |\n| Messenger | ![Heroicons Messenger](./src/assets/icons/heroicon/heroicon_messenger.svg) | ![Remix Messenger](./src/assets/icons/remix/remix_messenger.svg) | ![Tabler Messenger](./src/assets/icons/tabler/tabler_messenger.svg) |\n| Facebook | ![Heroicons Facebook](./src/assets/icons/heroicon/heroicon_facebook.svg) | ![Remix Facebook](./src/assets/icons/remix/remix_facebook.svg) | ![Tabler Facebook](./src/assets/icons/tabler/tabler_facebook.svg) |\n| WhatsApp | ![Heroicons WhatsApp](./src/assets/icons/heroicon/heroicon_whatsapp.svg) | ![Remix WhatsApp](./src/assets/icons/remix/remix_whatsapp.svg) | ![Tabler WhatsApp](./src/assets/icons/tabler/tabler_whatsapp.svg) |\n| Telegram | ![Heroicons Telegram](./src/assets/icons/heroicon/heroicon_telegram.svg) | ![Remix Telegram](./src/assets/icons/remix/remix_telegram.svg) | ![Tabler Telegram](./src/assets/icons/tabler/tabler_telegram.svg) |\n| Instagram | ![Heroicons Instagram](./src/assets/icons/heroicon/heroicon_instagram.svg) | ![Remix Instagram](./src/assets/icons/remix/remix_instagram.svg) | ![Tabler Instagram](./src/assets/icons/tabler/tabler_instagram.svg) |\n| TikTok | ![Heroicons TikTok](./src/assets/icons/heroicon/heroicon_tiktok.svg) | ![Remix TikTok](./src/assets/icons/remix/remix_tiktok.svg) | ![Tabler TikTok](./src/assets/icons/tabler/tabler_tiktok.svg) |\n| LinkedIn | ![Heroicons LinkedIn](./src/assets/icons/heroicon/heroicon_linkedin.svg) | ![Remix LinkedIn](./src/assets/icons/remix/remix_linkedin.svg) | ![Tabler LinkedIn](./src/assets/icons/tabler/tabler_linkedin.svg) |\n| WeChat | ![Heroicons WeChat](./src/assets/icons/heroicon/heroicon_wechat.svg) | ![Remix WeChat](./src/assets/icons/remix/remix_wechat.svg) | ![Tabler WeChat](./src/assets/icons/tabler/tabler_wechat.svg) |\n| Map | ![Heroicons map](./src/assets/icons/heroicon/heroicon_map.svg) | ![Remix map](./src/assets/icons/remix/remix_map.svg) | ![Tabler map](./src/assets/icons/tabler/tabler_map.svg) |\n| Form / custom | ![Heroicons form](./src/assets/icons/heroicon/heroicon_form.svg) | ![Remix form](./src/assets/icons/remix/remix_form.svg) | ![Tabler form](./src/assets/icons/tabler/tabler_form.svg) |\n| Email | ![Heroicons email](./src/assets/icons/heroicon/heroicon_mail.svg) | ![Remix email](./src/assets/icons/remix/remix_mail.svg) | ![Tabler email](./src/assets/icons/tabler/tabler_mail.svg) |\n\nMessenger, Telegram, and TikTok use their recognizable brand marks across all\nthree families because those logos do not have library-specific variants.\n\n### `ButtonConfig`\n\n| Property  | Type                                                                                                  | Description                                                                 |\n| --------- | ------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------- |\n| `type`    | `'phone' \\| 'line' \\| 'messenger' \\| 'facebook' \\| 'whatsapp' \\| 'telegram' \\| 'instagram' \\| 'tiktok' \\| 'linkedin' \\| 'wechat' \\| 'map' \\| 'form' \\| 'email' \\| 'custom'` | Which channel. Controls the default icon/color/link format. |\n| `value`   | `string`                                                                                                 | A shorthand (phone number, LINE ID, username) **or** a full URL — see below. |\n| `label`   | `string`                                                                                                 | Accessible label + tooltip text. Falls back to a sensible per-type default.  |\n| `color`   | `string`                                                                                                 | Override the built-in brand color/gradient for this button.                  |\n| `icon`    | `string` (URL)                                                                                           | Override the built-in icon with your own image (PNG/SVG/CDN link).           |\n| `iconVariant` | `'heroicon' \\| 'remix' \\| 'tabler'`                                                                  | Override the widget-level icon family for this button only.                  |\n| `target`  | `'_blank' \\| '_self'`                                                                                    | Defaults to `'_blank'`.                                                      |\n| `enabled` | `boolean`                                                                                                | Set `false` to temporarily hide a button without deleting its config.        |\n\n### Shorthand → link resolution\n\nYou can always paste an already-complete URL/URI, but each channel also\naccepts a shorthand so non-technical teams (content/marketing) can paste\nwhat they already have:\n\n| `type`      | Shorthand example                          | Resolves to                                    |\n| ----------- | -------------------------------------------- | ------------------------------------------------- |\n| `phone`     | `\"0869828288\"`                               | `tel:0869828288`                                  |\n| `email`     | `\"hello@example.com\"`                        | `mailto:hello@example.com`                        |\n| `line`      | `\"@LCSGROUP\"`                                 | `https://line.me/R/ti/p/%40LCSGROUP`               |\n| `messenger` | `\"https://www.facebook.com/lcsgroupthailand\"` | `https://m.me/lcsgroupthailand`                    |\n| `facebook`  | `\"lcsgroupthailand\"`                          | `https://www.facebook.com/lcsgroupthailand`        |\n| `whatsapp`  | `\"66869828288\"`                               | `https://wa.me/66869828288`                        |\n| `telegram`  | `\"lcsgroup\"`                                  | `https://t.me/lcsgroup`                            |\n| `instagram` | `\"lcsgroup\"`                                  | `https://www.instagram.com/lcsgroup`               |\n| `tiktok`    | `\"lcsgroup\"`                                  | `https://www.tiktok.com/@lcsgroup`                 |\n| `linkedin`  | `\"criclabs\"`                                  | `https://www.linkedin.com/in/criclabs`             |\n| `wechat`    | `\"@criclabs\"`                                 | `weixin://dl/chat?criclabs`                        |\n| `map`       | `\"Bangkok, Thailand\"`                         | Google Maps search URL                             |\n| `form`      | any full URL                                  | used as-is                                         |\n| `custom`    | any full URL                                  | used as-is (uses the selected form icon by default) |\n\n### Runtime commands\n\nOnce mounted, `window.ContactWidget(command, payload)` supports:\n\n- `ContactWidget('init', config)` — mount (or re-mount) the widget.\n- `ContactWidget('update', partialConfig)` — merge new config into the live widget (e.g. re-theme per page/section).\n- `ContactWidget('open')` / `ContactWidget('close')` — control the menu programmatically.\n- `ContactWidget('destroy')` — unmount the widget entirely.\n\nThe element also dispatches `cw-open`, `cw-close`, and `cw-button-click`\nDOM events (bubbling, composed) if you want to hook up analytics.\n\n## Design notes\n\n- **Single source of truth for links**: `src/utils/link-resolver.ts` centralizes every \"shorthand → real URI\" rule, so adding a new channel type only requires one resolver + one entry in `button-presets.ts`.\n- **Icon variants**: the widget bundles Heroicons, Remix, and Tabler SVG sets and selects them through `iconVariant`. Any button can still override its icon with a custom image URL.\n- **Animation**: menu items cascade in/out (`animate-cw-pop-in` / `animate-cw-pop-out`, staggered by proximity to the trigger button) and the trigger icon cross-fades between the chat bubble and the close `X`.\n- **Accessibility**: every link has an `aria-label`/`title`, the trigger button exposes `aria-expanded`, `Escape` closes the menu, and clicking outside the widget closes it too.\n","readmeFilename":"README.md"}