Hey there, It would be real fun allowing users to copy and paste GIFs (e.g. from Tenor.com or Giphy) directly from the AppLauncher. And maybe preview or auto-upload GIFs to ensure persistence
Idk why but, i felt like it... And acn be a help for some api and bento-grid thingies
heres the example html file:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>GIF LAUNCHER CONCEPT</title>
<link
href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&family=Syne:wght@500;600&display=swap"
rel="stylesheet" />
<style>
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--bg: #0e0e0f;
--surface: rgba(22, 22, 26, 0.97);
--border: rgba(255, 255, 255, 0.09);
--border-hover: rgba(52, 211, 195, 0.55);
--accent: #34d3c3;
--accent-dim: rgba(52, 211, 195, 0.12);
--text: #e8e8ea;
--text-muted: rgba(232, 232, 234, 0.35);
--radius: 14px;
--launcher-w: 700px;
}
body {
background: var(--bg);
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
font-family: "JetBrains Mono", monospace;
color: var(--text);
}
/* idk why tf i made these */
.launcher {
width: var(--launcher-w);
background: var(--surface);
border-radius: 22px;
border: 1.5px solid var(--border);
overflow: hidden;
box-shadow:
0 0 0 1px rgba(255, 255, 255, 0.03),
0 32px 80px rgba(0, 0, 0, 0.7);
}
/* search bar!! */
.search-area {
padding: 14px 14px 10px;
display: flex;
align-items: center;
gap: 10px;
border-bottom: 1px solid var(--border);
}
.search-icon {
flex-shrink: 0;
width: 18px;
height: 18px;
opacity: 0.35;
fill: var(--text);
}
.search-input {
flex: 1;
background: transparent;
border: none;
outline: none;
font-family: "JetBrains Mono", monospace;
font-size: 14px;
color: var(--text);
caret-color: var(--accent);
}
.search-input::placeholder {
color: var(--text-muted);
}
.tag-pill {
font-size: 10px;
color: var(--accent);
background: var(--accent-dim);
border: 1px solid rgba(52, 211, 195, 0.2);
border-radius: 6px;
padding: 3px 8px;
letter-spacing: 0.05em;
font-family: "JetBrains Mono", monospace;
white-space: nowrap;
opacity: 0;
transition: opacity 0.2s;
}
.tag-pill.visible {
opacity: 1;
}
/* bento grid [tis shyt looks hella cool] */
.bento-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-auto-rows: 130px;
gap: 7px;
padding: 10px;
max-height: 530px;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: rgba(255, 255, 255, 0.08) transparent;
}
.bento-grid::-webkit-scrollbar {
width: 4px;
}
.bento-grid::-webkit-scrollbar-track {
background: transparent;
}
.bento-grid::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.08);
border-radius: 4px;
}
/* gif cells */
.gif-cell {
border-radius: var(--radius);
overflow: hidden;
position: relative;
cursor: pointer;
border: 1.5px solid var(--border);
transition:
border-color 0.15s ease,
transform 0.12s ease;
background: rgba(255, 255, 255, 0.03);
outline: none;
}
.gif-cell:focus-visible {
border-color: var(--accent);
}
.gif-cell:hover {
border-color: var(--border-hover);
transform: scale(1.025);
z-index: 2;
}
.gif-cell img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
pointer-events: none;
}
.gif-cell.wide {
grid-column: span 2;
}
.gif-cell.tall {
grid-row: span 2;
}
.gif-cell.large {
grid-column: span 2;
grid-row: span 2;
}
/* copy flash overlay */
.gif-cell .copy-flash {
position: absolute;
inset: 0;
background: rgba(52, 211, 195, 0.22);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.08s;
pointer-events: none;
border-radius: calc(var(--radius) - 2px);
}
.gif-cell .copy-flash.show {
opacity: 1;
}
.copy-flash-label {
font-size: 11px;
font-family: "JetBrains Mono", monospace;
color: #fff;
background: rgba(0, 0, 0, 0.55);
padding: 4px 10px;
border-radius: 6px;
letter-spacing: 0.04em;
}
/* hover label */
.gif-cell .hover-label {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 20px 8px 7px;
background: linear-gradient(to top, rgba(0, 0, 0, 0.65), transparent);
font-size: 10px;
color: rgba(255, 255, 255, 0.6);
opacity: 0;
transition: opacity 0.15s;
pointer-events: none;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.gif-cell:hover .hover-label {
opacity: 1;
}
.skeleton {
animation: shimmer 1.5s ease-in-out infinite;
background: linear-gradient(90deg,
rgba(255, 255, 255, 0.03) 0%,
rgba(255, 255, 255, 0.07) 50%,
rgba(255, 255, 255, 0.03) 100%);
background-size: 200% 100%;
}
@keyframes shimmer {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
/* for empty / error states */
.state-box {
grid-column: 1 / -1;
grid-row: 1 / span 3;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10px;
padding: 48px 24px;
color: var(--text-muted);
}
.state-glyph {
font-size: 36px;
font-family: "Syne", sans-serif;
opacity: 0.25;
}
.state-label {
font-size: 12px;
letter-spacing: 0.06em;
}
/* status bar */
.status-bar {
padding: 8px 14px;
border-top: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: space-between;
font-size: 11px;
color: var(--text-muted);
}
.kbd {
display: inline-flex;
align-items: center;
gap: 4px;
}
.key {
font-size: 10px;
color: var(--accent);
background: var(--accent-dim);
border: 1px solid rgba(52, 211, 195, 0.2);
border-radius: 4px;
padding: 1px 6px;
font-family: "JetBrains Mono", monospace;
}
/* context menu */
.ctx-menu {
position: fixed;
background: rgba(16, 16, 18, 0.98);
border: 1.5px solid rgba(255, 255, 255, 0.12);
border-radius: 12px;
overflow: hidden;
z-index: 9999;
min-width: 175px;
display: none;
box-shadow: 0 16px 40px rgba(0, 0, 0, 0.7);
}
.ctx-menu.open {
display: block;
animation: popIn 0.1s ease;
}
@keyframes popIn {
from {
transform: scale(0.94);
opacity: 0;
}
to {
transform: scale(1);
opacity: 1;
}
}
.ctx-header {
padding: 8px 14px 6px;
font-size: 10px;
color: rgba(52, 211, 195, 0.6);
letter-spacing: 0.08em;
border-bottom: 1px solid rgba(255, 255, 255, 0.07);
}
.ctx-item {
padding: 10px 14px;
color: rgba(232, 232, 234, 0.85);
font-size: 12px;
font-family: "JetBrains Mono", monospace;
cursor: pointer;
display: flex;
align-items: center;
gap: 10px;
transition: background 0.08s;
}
.ctx-item:hover {
background: rgba(52, 211, 195, 0.1);
color: var(--text);
}
.ctx-item+.ctx-item {
border-top: 1px solid rgba(255, 255, 255, 0.04);
}
.ctx-dot {
width: 7px;
height: 7px;
border-radius: 50%;
flex-shrink: 0;
}
</style>
</head>
<body>
<div class="launcher" id="launcher">
<!-- SEARCH BARRRRR-->
<div class="search-area">
<svg class="search-icon" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path
d="M9 3.5a5.5 5.5 0 1 0 0 11 5.5 5.5 0 0 0 0-11zM2 9a7 7 0 1 1 12.452 4.391l3.328 3.329a.75.75 0 1 1-1.06 1.06l-3.329-3.328A7 7 0 0 1 2 9z" />
</svg>
<input class="search-input" id="searchInput" type="text" placeholder="Search GIFs..." autocomplete="off"
autofocus />
<span class="tag-pill" id="tagPill">tenor</span>
</div>
<!-- Gridz fritz-->
<div class="bento-grid" id="bentoGrid">
<div class="state-box">
<div class="state-glyph">GIF</div>
<div class="state-label">type to search tenor</div>
</div>
</div>
<!-- Status bar -->
<div class="status-bar">
<span id="statusMsg">powered by tenor</span>
<div class="kbd">
<span class="key">Enter</span> copy ·
<span class="key">RClick</span> format
</div>
</div>
</div>
<!-- Context menuu -->
<div class="ctx-menu" id="ctxMenu">
<div class="ctx-header">COPY AS</div>
<div class="ctx-item" data-fmt="jpeg">
<span class="ctx-dot" style="background: #f59e0b"></span> JPEG frame
</div>
<div class="ctx-item" data-fmt="png">
<span class="ctx-dot" style="background: #3b82f6"></span> PNG frame
</div>
<div class="ctx-item" data-fmt="webp">
<span class="ctx-dot" style="background: #a855f7"></span> WebP frame
</div>
<div class="ctx-item" data-fmt="url">
<span class="ctx-dot" style="background: #34d3c3"></span> GIF URL (text)
</div>
</div>
<script lang="ts">
const TENOR_KEY = "LIVDSRZULELA"; // found this usable public key
const TENOR_BASE = "https://api.tenor.com/v1/search";
const grid = document.getElementById("bentoGrid");
const input = document.getElementById("searchInput");
const ctxMenu = document.getElementById("ctxMenu");
const statusMsg = document.getElementById("statusMsg");
const tagPill = document.getElementById("tagPill");
let gifs = [];
let activeIdx = -1;
let debounce;
/* Bento layout pattern (cycles through 20 slots) */
const PATTERN = [
"large",
"",
"",
"tall",
"",
"wide",
"",
"",
"large",
"",
"",
"",
"wide",
"",
"",
"",
"tall",
"",
"",
"",
];
/* Search */
input.addEventListener("input", () => {
clearTimeout(debounce);
const q = input.value.trim();
tagPill.className = q ? "tag-pill visible" : "tag-pill";
if (!q) {
showEmpty();
return;
}
debounce = setTimeout(() => search(q), 380);
});
input.addEventListener("keydown", (e) => {
if (e.key === "Escape") {
input.value = "";
showEmpty();
tagPill.className = "tag-pill";
}
if (e.key === "Enter" && activeIdx >= 0) copyGif(activeIdx, "jpeg");
});
async function search(q) {
showSkeleton();
try {
const url = `${TENOR_BASE}?q=${encodeURIComponent(q)}&key=${TENOR_KEY}&limit=20&media_filter=minimal`;
const r = await fetch(url);
const d = await r.json();
if (!d.results?.length) {
showNoResults();
return;
}
gifs = d.results.map((g) => ({
gif: g.media[0]?.gif?.url || "",
tiny: g.media[0]?.tinygif?.url || g.media[0]?.gif?.url || "",
title: g.title || "",
}));
renderGrid();
statusMsg.textContent = `${gifs.length} results for "${q}"`;
} catch {
showError();
}
}
/* Render bento grid */
function renderGrid() {
grid.innerHTML = "";
gifs.forEach((g, i) => {
const cls = PATTERN[i % PATTERN.length] || "";
const cell = document.createElement("div");
cell.className = "gif-cell" + (cls ? " " + cls : "");
cell.tabIndex = 0;
const img = document.createElement("img");
img.src = g.tiny;
img.alt = g.title;
img.loading = "lazy";
const flash = document.createElement("div");
flash.className = "copy-flash";
flash.innerHTML = '<span class="copy-flash-label">Copied!</span>';
const label = document.createElement("div");
label.className = "hover-label";
label.textContent = g.title;
cell.append(img, flash, label);
cell.addEventListener("click", () => {
activeIdx = i;
copyGif(i, "jpeg");
});
cell.addEventListener("contextmenu", (e) => {
e.preventDefault();
activeIdx = i;
openCtx(e.clientX, e.clientY);
});
cell.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
activeIdx = i;
copyGif(i, "jpeg");
}
});
grid.appendChild(cell);
});
}
async function copyGif(idx, fmt) {
const g = gifs[idx];
if (!g) return;
if (fmt === "url") {
await navigator.clipboard.writeText(g.gif).catch(() => { });
flashCell(idx);
statusMsg.textContent = "Copied GIF URL";
return;
}
/* canvas frame grab */
const mimeMap = {
jpeg: "image/jpeg",
png: "image/png",
webp: "image/webp",
/* available formats here*/
};
const mime = mimeMap[fmt] || "image/jpeg";
try {
const img = new Image();
img.crossOrigin = "anonymous";
const proxyUrl = `https://images.weserv.nl/?url=${encodeURIComponent(g.tiny)}&output=${fmt}&maxage=1d`;
img.src = proxyUrl;
await new Promise((res, rej) => {
img.onload = res;
img.onerror = rej;
});
const canvas = document.createElement("canvas");
canvas.width = img.naturalWidth || 320;
canvas.height = img.naturalHeight || 240;
canvas.getContext("2d").drawImage(img, 0, 0);
const blob = await new Promise((res) =>
canvas.toBlob(res, mime, 0.92),
);
await navigator.clipboard.write([
new ClipboardItem({ [mime]: blob }),
]);
flashCell(idx);
statusMsg.textContent = `Copied as ${fmt.toUpperCase()}`;
} catch {
/* fallback: just copy da url */
try {
await navigator.clipboard.writeText(g.gif);
} catch { }
flashCell(idx);
statusMsg.textContent = `Copied URL (direct format blocked by browser)`;
}
}
function flashCell(idx) {
const cells = grid.querySelectorAll(".gif-cell");
const c = cells[idx];
if (!c) return;
const f = c.querySelector(".copy-flash");
f.classList.add("show");
setTimeout(() => f.classList.remove("show"), 700);
}
/* context menu */
function openCtx(x, y) {
ctxMenu.style.left = Math.min(x, window.innerWidth - 185) + "px";
ctxMenu.style.top = Math.min(y, window.innerHeight - 170) + "px";
ctxMenu.className = "ctx-menu open";
}
ctxMenu.querySelectorAll(".ctx-item").forEach((el) => {
el.addEventListener("click", () => {
ctxMenu.className = "ctx-menu";
copyGif(activeIdx, el.dataset.fmt);
});
});
document.addEventListener(
"click",
() => (ctxMenu.className = "ctx-menu"),
);
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") ctxMenu.className = "ctx-menu";
});
/* state helpers */
function showSkeleton() {
grid.innerHTML = "";
const sizes = ["large", "", "", "", "wide", "", "", "", "", "", "", ""];
sizes.forEach((s) => {
const d = document.createElement("div");
d.className = "gif-cell skeleton" + (s ? " " + s : "");
grid.appendChild(d);
});
statusMsg.textContent = "Searching…";
}
function showEmpty() {
gifs = [];
activeIdx = -1;
grid.innerHTML = `
<div class="state-box">
<div class="state-glyph">GIF</div>
<div class="state-label">type to search tenor</div>
</div>`;
statusMsg.textContent = "powered by tenor";
}
function showNoResults() {
grid.innerHTML = `
<div class="state-box">
<div class="state-glyph">∅</div>
<div class="state-label">no GIFs found</div>
</div>`;
statusMsg.textContent = "no results";
}
function showError() {
grid.innerHTML = `
<div class="state-box">
<div class="state-glyph">!</div>
<div class="state-label">could not reach tenor</div>
</div>`;
statusMsg.textContent = "network error";
}
</script>
</body>
</html>```
Feature Request:
Hey there, It would be real fun allowing users to copy and paste GIFs (e.g. from Tenor.com or Giphy) directly from the AppLauncher. And maybe preview or auto-upload GIFs to ensure persistence
or just show the gif in the app launcher as preview, on click
Idk why but, i felt like it... And acn be a help for some api and bento-grid thingies
heres the example html file: