import type { Meta, StoryObj } from '@storybook/html'; import { WhatsappEditor, type WhatsappEditorOptions } from '@42/core/whatsapp-editor'; import { Picker } from 'emoji-mart'; import '@42/styles/whatsapp-editor.css'; /** Default emoji set β€” swap this array (or plug a 3rd-party picker) to change it. */ const DEFAULT_EMOJIS = ['πŸ˜€', 'πŸ˜‚', '❀️', 'πŸ”₯', 'πŸ‘', 'πŸŽ‰', 'πŸš€', '✨', 'πŸ’―', 'πŸ™Œ', 'πŸ‘', '😎']; const HTML = `
`; /** Build a simple inline emoji grid from a customizable list. */ function gridPicker(emojis: string[]) { return (container: HTMLElement, insert: (emoji: string) => void): void => { container.classList.add('c42-whatsapp-editor-panel--grid'); container.innerHTML = emojis .map((e) => ``) .join(''); container.addEventListener('click', (ev) => { const btn = (ev.target as HTMLElement).closest('button'); if (btn) insert(btn.textContent!); }); }; } const meta: Meta = { title: 'Core/Inputs & Forms/WhatsappEditor', tags: ['autodocs'], argTypes: { maxLength: { control: { type: 'number', min: 10, max: 4096 } }, }, args: { maxLength: 4096 }, }; export default meta; type Story = StoryObj; /** * Toolbar + live preview, with the emoji picker anchored bottom-right (closed * until you tap πŸ™‚). Type `*bold*`, `_italic_`, `~strike~`, `` `code` ``. */ export const Default: Story = { render: (args) => { const wrapper = document.createElement('div'); wrapper.innerHTML = HTML; const root = wrapper.querySelector('[data-c42-whatsapp-editor]')!; new WhatsappEditor(root, { ...args, renderPicker: gridPicker(DEFAULT_EMOJIS) }); return wrapper; }, }; /** * ## Custom emoji set * * The emoji selection is fully user-controlled β€” pass any list (or a 3rd-party * picker like `emoji-mart`) through `renderPicker`: * * ```ts * const MY_EMOJIS = ['πŸ₯³', '🀝', 'πŸ“¦', '⏰', 'βœ…']; * new WhatsappEditor(root, { renderPicker: gridPicker(MY_EMOJIS) }); * ``` */ export const CustomEmojiSet: Story = { render: (args) => { const wrapper = document.createElement('div'); wrapper.innerHTML = HTML; const root = wrapper.querySelector('[data-c42-whatsapp-editor]')!; const business = ['πŸ₯³', '🀝', 'πŸ“¦', '⏰', 'βœ…', '❌', 'πŸ’³', 'πŸ“', 'πŸ“ž', '⭐']; new WhatsappEditor(root, { ...args, renderPicker: gridPicker(business) }); return wrapper; }, }; /** Seeded with formatted markup to show the preview immediately. */ export const Prefilled: Story = { render: (args) => { const wrapper = document.createElement('div'); wrapper.innerHTML = HTML; const root = wrapper.querySelector('[data-c42-whatsapp-editor]')!; new WhatsappEditor(root, { ...args, value: '*Order confirmed!* βœ…\n\n_Tracking_: ~12345~\n\n> Thanks for shopping\n- Fast\n- Secure', renderPicker: gridPicker(DEFAULT_EMOJIS), }); return wrapper; }, }; /** Without the emoji picker β€” toolbar + preview only. */ export const NoEmoji: Story = { render: (args) => { const wrapper = document.createElement('div'); wrapper.innerHTML = HTML; const root = wrapper.querySelector('[data-c42-whatsapp-editor]')!; new WhatsappEditor(root, { ...args, emojis: false }); return wrapper; }, }; /** * ## Floating selection menu (bubble toolbar) * * Add a `[data-c42-wa-floating]` element containing `[data-c42-wa-command]` * buttons anywhere inside the editor root. The controller shows it centered * above the selection whenever you select text, positions it with a mirror-div * caret measurement, and reflects each marker's active state via `aria-pressed` * / `data-active`. It hides on collapse, blur, Escape, scroll and resize. * * **Select a few words below** to reveal the bubble toolbar. * * ```html * * ``` */ export const FloatingMenu: Story = { render: (args) => { const wrapper = document.createElement('div'); wrapper.innerHTML = HTML; const root = wrapper.querySelector('[data-c42-whatsapp-editor]')!; new WhatsappEditor(root, { ...args, value: 'Select this text to reveal the floating toolbar above the selection.', renderPicker: gridPicker(DEFAULT_EMOJIS), }); return wrapper; }, }; /** * ## Built-in floating menu (`floating: true`) * * Don't want to author the bubble markup? Pass `floating: true` and the * controller injects a default, themed selection toolbar (bold / italic / * strikethrough / monospace) when you haven't provided your own * `[data-c42-wa-floating]`. Your markup always wins when present β€” the flag only * decides who supplies the UI; the controller owns the logic and events either way. * * ```ts * new WhatsappEditor(root, { floating: true }); * ``` * * **Select a few words below** β€” the bubble here is the injected default. */ export const BuiltInFloatingMenu: Story = { render: (args) => { const wrapper = document.createElement('div'); wrapper.innerHTML = HTML; const root = wrapper.querySelector('[data-c42-whatsapp-editor]')!; // Remove the authored bubble menu so the injected default is what you see. root.querySelector('[data-c42-wa-floating]')?.remove(); new WhatsappEditor(root, { ...args, floating: true, value: 'Select this text β€” this bubble menu was injected by floating: true.', renderPicker: gridPicker(DEFAULT_EMOJIS), }); return wrapper; }, }; /** * ## Custom-styled floating menu (your markup, your look) * * The bubble menu is just your `[data-c42-wa-floating]` element with * `[data-c42-wa-command]` buttons β€” style it however you like. The controller * only toggles `data-state` / `hidden`, positions it, and sets `aria-pressed` / * `data-active`, so all the formatting logic and events keep working no matter * how it looks. * * This example skins it as a dark WhatsApp-style pill and highlights the active * format in green β€” purely via CSS on a custom class. **Select text** to see it. * * ```css * .my-bubble.c42-whatsapp-editor-floating { * background: #111b21; * border: 0; * border-radius: 999px; * } * .my-bubble .c42-whatsapp-editor-command[aria-pressed='true'] { * background: #25d366; * color: #07130c; * } * ``` */ export const CustomFloatingMenu: Story = { render: (args) => { const wrapper = document.createElement('div'); wrapper.innerHTML = ` ${HTML}`; const root = wrapper.querySelector('[data-c42-whatsapp-editor]')!; // Tag the authored bubble menu with a class so the CSS above can restyle it. root.querySelector('[data-c42-wa-floating]')?.classList.add('my-bubble'); new WhatsappEditor(root, { ...args, value: 'Select me β€” the bubble menu uses a custom dark pill style.', renderPicker: gridPicker(DEFAULT_EMOJIS), }); return wrapper; }, }; /** * ## Device preview (opt-in) * * Wrap the `[data-c42-wa-preview]` element in `.c42-whatsapp-editor-device` to * render the live preview on a WhatsApp phone background (the image ships with * `@42/styles`). It is entirely optional β€” omit the wrapper to keep the plain * bubble. Positioning is tunable via CSS custom properties * (`--c42-wa-device-pad-top`, `--c42-wa-device-pad-x`, `--c42-wa-device-height`). * * ```html *
*
*
* ``` */ export const DevicePreview: Story = { render: (args) => { const wrapper = document.createElement('div'); wrapper.innerHTML = `
`; const root = wrapper.querySelector('[data-c42-whatsapp-editor]')!; new WhatsappEditor(root, { ...args, value: '*Order confirmed!* βœ…\n_Tracking_: 12345', emojis: false, }); return wrapper; }, }; /** * ## With emoji-mart (external plugin) * * `emoji-mart` is **not** bundled with `@42/core`. Install it separately and * mount it through `renderPicker` β€” the controller still owns open/close, * positioning and outside-click dismissal: * * ```bash * pnpm add emoji-mart @emoji-mart/data * ``` * * ```ts * import { Picker } from 'emoji-mart'; * new WhatsappEditor(root, { * renderPicker: (container, insert) => { * new Picker({ parent: container, onEmojiSelect: (e) => insert(e.native) }); * }, * }); * ``` */ export const WithEmojiMart: Story = { render: (args) => { const wrapper = document.createElement('div'); wrapper.innerHTML = `

Note: emoji-mart is not part of @42/core. Install separately: pnpm add emoji-mart @emoji-mart/data

${HTML}`; const root = wrapper.querySelector('[data-c42-whatsapp-editor]')!; new WhatsappEditor(root, { ...args, renderPicker: (container, insert) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any new (Picker as any)({ parent: container, onEmojiSelect: (emoji: { native: string }) => insert(emoji.native), theme: 'light', previewPosition: 'none', searchPosition: 'none', data: async () => { const res = await fetch('https://cdn.jsdelivr.net/npm/@emoji-mart/data'); return res.json(); }, }); }, }); return wrapper; }, };