ShareSheet
A bottom sheet with the ways to share a link: copy it, email, X, LinkedIn, WhatsApp and more, and the system's own.
Demo
Not shared yet.
Get it
It needs theme.qwe, as every component does; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.
$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/share_sheet.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/share_sheet.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/share_sheet.qwe
component ShareSheetDemoLive() { state sharing = false state how = null <div.demo-row> <Button variant="soft" on_click={() => { sharing = true }}>Invite (from state)</Button> <span.demo-note>{how == null ? "Not shared yet." : `Shared by ${how}.`}</span> <ShareSheet title="Invite to the project" url="https://quillolang.org/join/7f3a" text="Join my project on Quillo" targets={["copy", "email", "whatsapp", "telegram"]} open={sharing} on_share={(t) => { how = t }} on_close={() => { sharing = false }}/> </div>}view ShareSheetDemo() = <div.demo-row> <ShareSheet url="https://quillolang.org/components/" text="Quillo UI: components for Quillo" trigger="Share"/> <ShareSheetDemoLive/> </div>Source
ui/share_sheet.qwe
// ShareSheet: a panel rising from the bottom of the screen with the ways to share a// link: copy it (it says "Link copied"), email, X, LinkedIn, Facebook, WhatsApp,// Reddit, Telegram, and the system's own share sheet where the browser has one// ("More…"). The link is shown in full above them. It is the browser's own <dialog>,// so focus stays in it, and Escape or a click above it closes it.//// <ShareSheet url="https://example.com/posts/7" text="A post worth reading" trigger="Share"/>// <ShareSheet url={link} targets={["copy", "email", "whatsapp"]} open={sharing}// on_share={(how) => { track(how) }} on_close={() => { sharing = false }}/>//// url: the link (this page's address by default). text: what goes with it (the// subject of an email, the words of a post; the page's title by default).// targets: which, in order, of "copy", "email", "x", "linkedin", "facebook",// "whatsapp", "reddit", "telegram" and "more" (all of them by default; "more" shows// only where the browser has navigator.share). title: the sheet's heading ("Share").// trigger: the text of a button that opens it (styled as a Button when button_css is// on the page). Inside one of your components, open it from your own state instead:// open={...} and on_close(). on_share(target): called with the target used.import ui/theme.qwe use ui_clslet share_sheet_css = css`.ui-share-sheet-host { display: contents; }.ui-share-sheet { box-sizing: border-box; inset: auto 0 0 0; width: min(560px, 100vw); max-width: 100vw; max-height: calc(100dvh - 24px); margin: 0 auto; padding: 0; overflow: auto; border: 1px solid var(--ui-border); border-bottom: 0; border-radius: calc(var(--ui-radius) + 6px) calc(var(--ui-radius) + 6px) 0 0; background: var(--ui-surface); color: var(--ui-fg); box-shadow: var(--ui-shadow-lg); font: 14px/1.45 var(--ui-font);}.ui-share-sheet[open] { animation: ui-share-sheet-up .24s cubic-bezier(.2, .8, .2, 1); }.ui-share-sheet::backdrop { background: var(--ui-overlay); }@keyframes ui-share-sheet-up { from { transform: translateY(100%); } }.ui-share-sheet-box { padding: 10px 20px max(20px, env(safe-area-inset-bottom)); }.ui-share-sheet-grip { width: 40px; height: 5px; margin: 0 auto 12px; border-radius: 999px; background: var(--ui-strong); }.ui-share-sheet-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 12px; }.ui-share-sheet-head h2 { margin: 0; font-size: 17px; font-weight: 600; }.ui-share-sheet-x { display: grid; place-items: center; width: 30px; height: 30px; padding: 0; border: 0; border-radius: var(--ui-radius-sm); background: transparent; color: var(--ui-muted); cursor: pointer;}.ui-share-sheet-x:hover { background: var(--ui-subtle); color: var(--ui-fg); }.ui-share-sheet-x svg { width: 18px; height: 18px; }.ui-share-sheet-link { display: flex; gap: 8px; margin-bottom: 16px; }.ui-share-sheet-url { flex: 1; min-width: 0; height: 36px; padding: 0 10px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-subtle); color: var(--ui-fg); font: 13px/1 var(--ui-mono);}.ui-share-sheet-url:focus { outline: none; border-color: var(--ui-accent); box-shadow: var(--ui-focus); }.ui-share-sheet-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(84px, 1fr)); gap: 6px; margin: 0; padding: 0; list-style: none; }.ui-share-sheet-target { display: grid; justify-items: center; gap: 6px; width: 100%; padding: 10px 4px; border: 0; border-radius: var(--ui-radius-sm); background: transparent; color: var(--ui-fg); font: 12px/1.3 var(--ui-font); text-align: center; text-decoration: none; cursor: pointer;}.ui-share-sheet-target:hover { background: var(--ui-subtle); }.ui-share-sheet-icon { display: grid; place-items: center; width: 44px; height: 44px; border-radius: 50%; background: var(--ui-accent-soft); color: var(--ui-accent); font: 700 16px/1 var(--ui-font);}.ui-share-sheet-icon svg { width: 20px; height: 20px; }.ui-share-sheet-ok .ui-share-sheet-icon { background: var(--ui-success-soft); color: var(--ui-success); }.ui-share-sheet-x:focus-visible, .ui-share-sheet-target:focus-visible { outline: none; box-shadow: var(--ui-focus); }@media (prefers-reduced-motion: reduce) { .ui-share-sheet[open] { animation: none; } }`// Where each target goes with the link and its text; null for those that are not links.fn share_sheet_href(target: str, url: str, text: str) = match target { "email" => `mailto:?${url_encode({ subject: text, body: url })}` "x" => `https://x.com/intent/post?${url_encode({ text, url })}` "linkedin" => `https://www.linkedin.com/sharing/share-offsite/?${url_encode({ url })}` "facebook" => `https://www.facebook.com/sharer/sharer.php?${url_encode({ u: url })}` "whatsapp" => `https://wa.me/?${url_encode({ text: `${text} ${url}` })}` "reddit" => `https://www.reddit.com/submit?${url_encode({ url, title: text })}` "telegram" => `https://t.me/share/url?${url_encode({ url, text })}` _ => null }fn share_sheet_name(target: str) = match target { "copy" => "Copy link" "email" => "Email" "x" => "X" "linkedin" => "LinkedIn" "facebook" => "Facebook" "whatsapp" => "WhatsApp" "reddit" => "Reddit" "telegram" => "Telegram" "more" => "More…" _ => target }// The icon: a drawing for the generic ones, the name's first letters for the sites.fn share_sheet_icon(target: str) = match target { "copy" => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.5.5l3-3a5 5 0 0 0-7-7l-1.7 1.7M14 11a5 5 0 0 0-7.5-.5l-3 3a5 5 0 0 0 7 7l1.7-1.7"/></svg> "email" => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2ZM22 7l-10 7L2 7"/></svg> "more" => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8M16 6l-4-4-4 4M12 2v13"/></svg> "linkedin" => "in" "whatsapp" => "WA" "telegram" => "TG" _ => upper(slice(share_sheet_name(target), 0, 1)) }component ShareSheet(url: str? = null, text: str? = null, targets: list = ["copy", "email", "x", "linkedin", "facebook", "whatsapp", "reddit", "telegram", "more"], title: str = "Share", trigger: str? = null, open: bool = false, on_share = null, on_close = null, class: str? = null) { style global share_sheet_css state dlg = null state link = url ?? "" state words = text ?? "" state can_more = false state copied = "" // "", "ok" or "bad" if copied != "" { every(1800, () => { copied = "" }) } if open && dlg != null && !dlg.open { dlg.showModal() } if !open && dlg != null && dlg.open && trigger == null { dlg.close() } let shown = targets | filter((t) => t != "more" || can_more) let copy = () => { if js("isSecureContext") != true { copied = "bad" } else { js("navigator.clipboard.writeText", link).then((_ = null) => { copied = "ok" on_share && on_share("copy") }, (_ = null) => { copied = "bad" }) } } <div class={ui_cls(["ui-share-sheet-host", class])}> {trigger != null && <button.ui-btn.ui-btn-outline type="button" aria-haspopup="dialog" @click={() => dlg.showModal()}>{trigger}</button>} <dialog.ui-share-sheet aria-label={title} @mount={(el) => { dlg = el if url == null { link = js("location.href") } if text == null { words = js("document.title") ?? "" } can_more = js("navigator").share != null if open { el.showModal() } }} @close={() => { on_close && on_close() }} @click={(e) => { if e.target.tagName == "DIALOG" { dlg.close() } }}> <div.ui-share-sheet-box> <div.ui-share-sheet-grip aria-hidden="true"></div> <div.ui-share-sheet-head> <h2>{title}</h2> <button.ui-share-sheet-x type="button" aria-label="Close" @click={() => dlg.close()}> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12"/></svg> </button> </div> <div.ui-share-sheet-link> <input.ui-share-sheet-url type="text" readonly value={link} aria-label="Link" @focus={(e) => { e.target.select() }}/> </div> <ul.ui-share-sheet-grid> {for t in shown => <li key={t}>{t == "copy" || t == "more" ? <button type="button" class={ui_cls(["ui-share-sheet-target", t == "copy" && copied == "ok" && "ui-share-sheet-ok"])} data-target={t} @click={() => { if t == "copy" { copy() } else { js("navigator").share({ url: link, title: words, text: words }).then((_ = null) => { on_share && on_share("more") dlg.close() }, (_ = null) => {}) } }}> <span.ui-share-sheet-icon aria-hidden="true">{share_sheet_icon(t)}</span> <span>{t == "copy" && copied == "ok" ? "Link copied" : t == "copy" && copied == "bad" ? "Could not copy" : share_sheet_name(t)}</span> </button> : <a.ui-share-sheet-target href={share_sheet_href(t, link, words)} data-target={t} target={t == "email" ? null : "_blank"} rel={t == "email" ? null : "noopener noreferrer"} @click={() => { on_share && on_share(t) dlg.close() }}> <span.ui-share-sheet-icon aria-hidden="true">{share_sheet_icon(t)}</span> <span>{share_sheet_name(t)}</span> </a>}</li>} </ul> <p.ui-sr role="status">{copied == "ok" ? "Link copied" : copied == "bad" ? "Could not copy" : ""}</p> </div> </dialog> </div>}Related
FullscreenModalA dialog taking the whole window, with a bar holding close, its title and a main action, on the browser's <dialog>.5 views · 4 downloadsSheetA bottom sheet on the browser's <dialog>: half or full height by its handle or a drag, Escape to close.6 views · 5 downloadsAlertDialogA modal that only informs, with one OK button, on the browser's own <dialog> and no JavaScript.6 views · 7 downloadsChatWidgetA chat button in the corner that opens a small chat panel, with replies from your server and a count for unseen ones.4 views · 7 downloadsModalA dialog over the page, on the browser's own <dialog>: focus, Escape and screen readers handled.6 views · 8 downloadsPromptDialogA modal asking for one text value with OK and Cancel, on the browser's own <dialog>; Enter submits, Escape cancels.4 views · 5 downloads
Thread
Thread 0 posts
On ShareSheet: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.