great work :
Eleven ways to bring emoji into what you are writing. Each activation is shown in context, with a motion demo and three clear steps. Explore the three new gestures →
Three exploratory gestures. Watch the movement, open the emoji bud, then pick a reaction.
<div class="trigger-lab" data-kind="9" data-motion="loop" data-bud="true" data-action="Draw one small loop with the pointer"> <div class="trigger-lab__layout"><div class="trigger-lab__desktop"> <div class="trigger-lab__bar"><b>Messages</b><span>EmojiCopy <span class="jp-font"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f604.png" alt="Menu bar icon"></span></span></div> <div class="trigger-lab__canvas"><div class="trigger-lab__window"><div class="trigger-lab__title"><span class="trigger-lab__lights"><i></i><i></i><i></i></span> Team chat</div><div class="trigger-lab__conversation"><b>Casey</b><div class="trigger-lab__chat">The new design is ready!</div></div><div class="trigger-lab__composer">That looks great <span class="trigger-lab__result jp-font" data-result></span><span class="trigger-lab__caret"></span></div></div> <div class="trigger-lab__origin"><svg class="trigger-lab__trail" viewBox="0 0 100 100" aria-hidden="true"><path d="M72 76C15 96 8 15 51 14C89 13 99 55 77 69" fill="none" stroke="currentColor" stroke-width="3" stroke-dasharray="5 5"/><path d="M76 54L77 70L91 65" fill="none" stroke="currentColor" stroke-width="3"/></svg><svg class="trigger-lab__pointer" viewBox="0 0 26 32" aria-hidden="true"><path d="M3 2L23 20L14 21L10 30Z" fill="var(--metal-950)" stroke="var(--white)" stroke-width="2"/></svg></div> <button class="trigger-lab__seed jp-font" data-seed hidden aria-label="Open emoji favorites"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f604.png" alt="Emoji bud"></button> <div class="trigger-lab__palette" id="lab-palette-9" data-palette hidden aria-label="Emoji favorites"><div class="trigger-lab__palette-head"><b>EmojiCopy · Favorites</b><button class="trigger-lab__dismiss" data-dismiss aria-label="Close emoji favorites">Esc ×</button></div><div class="trigger-lab__choices"><button class="trigger-lab__choice jp-font" data-pick aria-label="Insert thumbs up"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f44d.png" alt="Thumbs up"></button><button class="trigger-lab__choice jp-font" data-pick aria-label="Insert party popper"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f389.png" alt="Party popper"></button><button class="trigger-lab__choice jp-font" data-pick aria-label="Insert heart"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/2764.png" alt="Heart"></button><button class="trigger-lab__choice jp-font" data-pick aria-label="Insert smile"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f604.png" alt="Smile"></button><button class="trigger-lab__choice jp-font" data-pick aria-label="Insert fire"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f525.png" alt="Fire"></button></div></div> </div></div><div class="trigger-lab__steps"><div class="trigger-lab__step" data-step="1"><strong>1</strong><div><b>Draw a small loop</b><span>No button held. A deliberate little circle.</span></div></div><div class="trigger-lab__step" data-step="2"><strong>2</strong><div><b>An emoji bud appears</b><span>Just above the pointer; click to unfold.</span></div></div><div class="trigger-lab__step" data-step="3"><strong>3</strong><div><b>Pick and keep typing</b><span>Insert into the active text field.</span></div></div></div></div> <div class="trigger-lab__controls"><button class="trigger-lab__play" data-play aria-controls="lab-palette-9" aria-expanded="false">Watch activation</button><button class="trigger-lab__reset" data-reset>Reset</button><span class="trigger-lab__status" role="status"></span></div> <div class="trigger-lab__tradeoff"><span><b>Why it could work</b><br>My first experiment: a small physical ritual that brings emoji to your hand.</span><span><b>What to test</b><br>Opt-in. Distinguish a deliberate loop from ordinary movement; never insert from the gesture alone.</span></div></div>
/* Trigger storyboards: desktop mockups, not native overlays. */
.trigger-lab { display: flex; flex-direction: column; gap: var(--sp-5); font-size: 1.5rem; color: var(--c-text-primary); }
.trigger-lab__layout { display: grid; grid-template-columns: minmax(0, 1fr) 230px; gap: var(--sp-6); align-items: center; }
.trigger-lab__desktop { position: relative; min-width: 0; border-radius: var(--r-lg); overflow: hidden; background: var(--metal-200); border: 1px solid var(--c-border-primary); }
.trigger-lab__bar { display: flex; align-items: center; justify-content: space-between; padding: var(--sp-3) var(--sp-4); background: var(--metal-100); color: var(--metal-700); font-size: 1.2rem; }
.trigger-lab__bar b { color: var(--metal-900); }
.trigger-lab__canvas { position: relative; height: 355px; padding: var(--sp-5); background: linear-gradient(140deg, var(--metal-100), var(--green-400)); }
.trigger-lab__window { position: absolute; inset: 22px 22px 30px; background: var(--white); border: 1px solid var(--metal-200); border-radius: var(--r-md); overflow: hidden; box-shadow: 0 12px 30px color-mix(in srgb, var(--metal-900) 12%, transparent); }
.trigger-lab__title { display: flex; align-items: center; gap: var(--sp-2); padding: var(--sp-3) var(--sp-4); border-bottom: 1px solid var(--metal-100); color: var(--metal-600); font-size: 1.3rem; }
.trigger-lab__lights { display: flex; gap: var(--sp-1); }
.trigger-lab__lights i { width: 8px; height: 8px; border-radius: var(--r-pill); background: var(--metal-300); }
.trigger-lab__conversation { display: flex; flex-direction: column; align-items: flex-start; gap: var(--sp-2); padding: var(--sp-5); color: var(--metal-600); }
.trigger-lab__conversation b { font-size: 1.2rem; }
.trigger-lab__chat { background: var(--metal-100); padding: var(--sp-3) var(--sp-4); border-radius: var(--r-md); color: var(--metal-900); font-size: 1.4rem; }
.trigger-lab__composer { position: absolute; bottom: 28px; left: 18px; right: 18px; border: 2px solid var(--green-700); border-radius: var(--r-md); padding: var(--sp-4); color: var(--metal-900); display: flex; align-items: center; gap: var(--sp-1); min-height: 26px; font-size: 1.6rem; }
.trigger-lab__caret { height: 22px; width: 2px; background: var(--green-800); }
.trigger-lab__result img { width: 25px; height: 25px; vertical-align: middle; }
.trigger-lab__origin { position: absolute; right: 21%; bottom: 105px; width: 160px; height: 120px; pointer-events: none; }
.trigger-lab__pointer { position: absolute; bottom: 0; right: 30px; width: 26px; height: 32px; filter: drop-shadow(0 2px 2px color-mix(in srgb, var(--metal-900) 25%, transparent)); }
.trigger-lab__trail { position: absolute; bottom: 0; right: 5px; width: 88px; height: 88px; color: var(--green-800); opacity: .6; }
.trigger-lab__keys { position: absolute; bottom: 62px; right: 6px; display: flex; gap: var(--sp-1); white-space: nowrap; }
.trigger-lab__keys kbd { display: grid; place-items: center; min-width: 35px; height: 33px; padding: 0 var(--sp-2); font: 600 1.6rem var(--ff-inter), sans-serif; color: var(--metal-900); background: var(--white); border: 1px solid var(--metal-300); border-radius: var(--r-sm); box-shadow: 0 3px 0 var(--metal-200); }
.trigger-lab__seed { position: absolute; right: 23%; bottom: 152px; width: 54px; height: 54px; display: grid; place-items: center; border: 1px solid var(--green-600); border-radius: var(--r-pill); background: var(--white); box-shadow: 0 0 0 6px color-mix(in srgb, var(--green-500) 22%, transparent); cursor: pointer; z-index: 3; }
.trigger-lab__seed img { width: 34px; height: 34px; }
.trigger-lab__palette { position: absolute; z-index: 4; right: 8%; bottom: 145px; width: min(285px, 83%); padding: var(--sp-3); box-sizing: border-box; border-radius: var(--r-lg); background: var(--metal-900); color: var(--white); box-shadow: 0 12px 26px color-mix(in srgb, var(--metal-900) 25%, transparent); }
.trigger-lab__palette::after { content: ''; position: absolute; bottom: -7px; right: 52px; width: 16px; height: 16px; transform: rotate(45deg); background: var(--metal-900); }
.trigger-lab__palette-head { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2); font-size: 1.2rem; padding-bottom: var(--sp-2); }
.trigger-lab__choices { display: flex; justify-content: space-between; gap: var(--sp-1); }
.trigger-lab__choice { display: grid; place-items: center; cursor: pointer; border: 0; border-radius: var(--r-sm); background: var(--metal-800); width: 44px; height: 44px; }
.trigger-lab__choice:hover { background: var(--green-700); }
.trigger-lab__choice img { width: 29px; height: 29px; }
.trigger-lab__dismiss { border: 0; background: transparent; color: var(--metal-300); cursor: pointer; font: inherit; padding: var(--sp-1); }
.trigger-lab__edge { position: absolute; right: 0; bottom: 136px; background: var(--metal-900); color: var(--white); border: 0; border-radius: var(--r-md) 0 0 var(--r-md); padding: var(--sp-3); cursor: pointer; }
.trigger-lab__edge img { width: 24px; height: 24px; }
.trigger-lab__steps { display: flex; flex-direction: column; gap: var(--sp-5); }
.trigger-lab__step { display: grid; grid-template-columns: 28px 1fr; gap: var(--sp-2); align-items: start; line-height: 1.5; }
.trigger-lab__step strong { display: grid; place-items: center; height: 26px; border-radius: var(--r-pill); background: var(--c-surface-tertiary); font-size: 1.2rem; }
.trigger-lab__step b { display: block; }
.trigger-lab__step span { font-size: 1.35rem; color: var(--c-text-secondary); }
.trigger-lab__step.is-current strong { background: var(--green-500); color: var(--green-950); }
.trigger-lab__controls { display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap; }
.trigger-lab__play, .trigger-lab__reset { border: 1px solid var(--c-border-primary); border-radius: var(--r-pill); padding: var(--sp-3) var(--sp-5); font: 600 1.4rem var(--ff-inter), sans-serif; cursor: pointer; }
.trigger-lab__play { color: var(--green-950); background: var(--green-500); }
.trigger-lab__reset { color: var(--c-text-secondary); background: var(--c-surface-primary); }
.trigger-lab__status { font-size: 1.3rem; color: var(--c-text-secondary); flex: 1; min-width: 180px; }
.trigger-lab__tradeoff { display: flex; gap: var(--sp-4); font-size: 1.35rem; line-height: 1.6; color: var(--c-text-secondary); border-top: 1px solid var(--c-border-primary); padding-top: var(--sp-4); }
.trigger-lab [hidden] { display: none !important; }
.trigger-lab button:focus-visible { outline: 3px solid var(--green-700); outline-offset: 3px; }
.trigger-lab[data-phase='gesture'] .trigger-lab__pointer { animation: trigger-orbit 1s ease-in-out; }
.trigger-lab[data-phase='gesture'] .trigger-lab__keys { animation: trigger-press .5s ease-in-out 2; }
.trigger-lab[data-phase='gesture'][data-motion='dwell'] .trigger-lab__pointer { animation: trigger-approach 1s ease-out; }
.trigger-lab[data-phase='gesture'][data-motion='carry'] .trigger-lab__pointer { animation: trigger-carry 1s ease-in-out; }
.trigger-lab[data-phase='done'] .trigger-lab__origin { opacity: .2; }
@keyframes trigger-orbit { 0%,100% { transform: translate(0,0); } 25% { transform: translate(-48px,0); } 50% { transform: translate(-48px,-48px); } 75% { transform: translate(0,-48px); } }
@keyframes trigger-press { 50% { transform: translateY(4px); } }
@keyframes trigger-approach { from { transform: translate(80px,-70px); } to { transform: translate(0,0); } }
@keyframes trigger-carry { from { transform: translate(-150px,-100px); } to { transform: translate(0,0); } }
@media (prefers-reduced-motion: reduce) { .trigger-lab *, .trigger-lab *::after { animation: none !important; } }
@media (max-width: 760px) { .trigger-lab__layout { grid-template-columns: 1fr; } .trigger-lab__steps { flex-direction: row; flex-wrap: wrap; } .trigger-lab__step { flex: 1; min-width: 145px; } .trigger-lab__tradeoff { flex-direction: column; } }
@media (max-width: 450px) { .trigger-lab__window { inset: 15px 8px; } .trigger-lab__composer { left: 10px; right: 10px; padding: var(--sp-3); font-size: 1.4rem; } .trigger-lab__origin { right: 10%; } }
.trigger-intro { font-size: 1.5rem; line-height: 1.6; color: var(--c-text-secondary); margin-bottom: var(--sp-5); }
.trigger-lab__bar img { width: 15px; height: 15px; vertical-align: middle; }
.trigger-lab__keys img { width: 34px; height: 34px; }
.trigger-lab[data-motion='keys'] .trigger-lab__pointer { display: none; }
.trigger-lab[data-kind='4'] .trigger-lab__origin { top: 4px; right: 0; bottom: auto; }
.trigger-lab[data-kind='4'] .trigger-lab__palette { width: min(340px, 90%); right: 5%; bottom: 100px; }
.trigger-lab[data-kind='4'] .trigger-lab__choices { display: grid; grid-template-columns: repeat(5, 1fr); gap: var(--sp-2); }
.trigger-lab[data-kind='8'] .trigger-lab__palette { right: 42px; bottom: 132px; }
.trigger-lab[data-kind='10'] .trigger-lab__origin { right: auto; left: 155px; bottom: 87px; }
.trigger-lab[data-kind='10'] .trigger-lab__seed { right: auto; left: 210px; bottom: 145px; }
.trigger-lab[data-kind='10'] .trigger-lab__palette { left: 72px; right: auto; }
.trigger-lab[data-kind='11'][data-phase='gesture'] .trigger-lab__keys { animation: trigger-carry 1s ease-in-out; }
.trigger-lab__selected { background: var(--green-400); }
.trigger-lab[data-kind='10'] .trigger-lab__origin { left: 88px; }
.trigger-lab[data-kind='10'] .trigger-lab__seed { left: 164px; }
.trigger-lab[data-kind='11'][data-phase='bud'] .trigger-lab__keys,
.trigger-lab[data-kind='11'][data-phase='open'] .trigger-lab__keys { visibility: hidden; }
.trigger-lab[data-kind='4'][data-phase='gesture'] .trigger-lab__pointer { animation: trigger-approach 1s ease-out; }
.trigger-lab[data-kind='4'][data-phase='gesture'] .trigger-lab__keys { outline: 2px solid var(--green-700); outline-offset: 5px; border-radius: var(--r-pill); }
// Isolated storyboards. Buttons simulate native activation; no global input is captured.
document.querySelectorAll('.trigger-lab').forEach(lab => {
const seed = lab.querySelector('[data-seed]')
const palette = lab.querySelector('[data-palette]')
const play = lab.querySelector('[data-play]')
const status = lab.querySelector('[role="status"]')
let timer
const phase = (name, step) => {
lab.dataset.phase = name
lab.querySelectorAll('[data-step]').forEach(item => item.classList.toggle('is-current', Number(item.dataset.step) === step))
}
const open = () => {
clearTimeout(timer)
seed.hidden = true
palette.hidden = false
play.setAttribute('aria-expanded', 'true')
phase('open', 2)
status.textContent = 'Click an emoji to insert it in this example. Escape cancels.'
palette.querySelector('[data-pick]').focus()
}
const reset = () => {
clearTimeout(timer)
seed.hidden = true
palette.hidden = true
play.setAttribute('aria-expanded', 'false')
const result = lab.querySelector('[data-result]')
result.textContent = result.dataset.initial || ''
phase('ready', 1)
status.textContent = 'Motion demo only · click “Watch activation” to begin.'
}
const cancel = () => {
clearTimeout(timer)
seed.hidden = true
palette.hidden = true
play.setAttribute('aria-expanded', 'false')
phase('ready', 1)
status.textContent = 'Dismissed. The message was not changed.'
play.focus()
}
play.addEventListener('click', () => {
reset()
phase('gesture', 1)
status.textContent = lab.dataset.action
timer = setTimeout(() => {
if (lab.dataset.bud === 'true') {
seed.hidden = false
phase('bud', 2)
status.textContent = 'The emoji bud is here. Click it to unfold favorites.'
seed.focus()
} else open()
}, 1050)
})
seed.addEventListener('click', open)
lab.querySelector('[data-reset]').addEventListener('click', reset)
lab.querySelector('[data-dismiss]').addEventListener('click', cancel)
lab.querySelector('[data-edge]')?.addEventListener('click', open)
lab.querySelectorAll('[data-pick]').forEach(button => button.addEventListener('click', () => {
lab.querySelector('[data-result]').replaceChildren(button.querySelector('img').cloneNode())
seed.hidden = true
palette.hidden = true
play.setAttribute('aria-expanded', 'false')
phase('done', 3)
status.textContent = 'Inserted here. In the proposed Mac flow, you keep typing in your app.'
play.focus()
}))
lab.addEventListener('keydown', event => {
if (event.key === 'Escape') { event.stopPropagation(); cancel() }
})
reset()
})
CONCEPT 9: Circle to bloom Draw one small loop with the pointer Opt-in. Distinguish a deliberate loop from ordinary movement; never insert from the gesture alone. This browser storyboard simulates the input using Watch activation; it does not detect a system-wide gesture or insert into another app. No claim of an unprecedented invention or native feasibility is made. Native sequence: capture verified editable target → recognize explicit activation → reveal bud or palette without modifying text → explicit emoji selection → revalidate target → insert, with one undo step. If no target is available, offer Copy explicitly. Cancel never changes text. Respect existing app controls, secure input, display bounds, and the user's opt-in setting. For buds: reveal beside the active pointer/caret, retain a generous travel corridor between pointer and bud, dismiss on continued typing, focus change, Escape, or movement away. Never insert on hover. Test the reveal delay and false positives with real users. Pocket mode is entered only via Carry in EmojiCopy. The carried emoji remains a proposal until a user confirms insertion. Clear after one successful insertion by default. Integration: mount this HTML once per demo, include the complete CSS and JS below. Source IDs must remain unique. Native implementation is a separate prototype. The browser mock uses PNGs; actual insertion must use the selected Unicode sequence.
<div class="trigger-lab" data-kind="10" data-motion="dwell" data-bud="true" data-action="Rest the pointer beside the text caret"> <div class="trigger-lab__layout"><div class="trigger-lab__desktop"> <div class="trigger-lab__bar"><b>Messages</b><span>EmojiCopy <span class="jp-font"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f604.png" alt="Menu bar icon"></span></span></div> <div class="trigger-lab__canvas"><div class="trigger-lab__window"><div class="trigger-lab__title"><span class="trigger-lab__lights"><i></i><i></i><i></i></span> Team chat</div><div class="trigger-lab__conversation"><b>Casey</b><div class="trigger-lab__chat">The new design is ready!</div></div><div class="trigger-lab__composer">That looks great <span class="trigger-lab__result jp-font" data-result></span><span class="trigger-lab__caret"></span></div></div> <div class="trigger-lab__origin"><svg class="trigger-lab__pointer" viewBox="0 0 26 32" aria-hidden="true"><path d="M3 2L23 20L14 21L10 30Z" fill="var(--metal-950)" stroke="var(--white)" stroke-width="2"/></svg></div> <button class="trigger-lab__seed jp-font" data-seed hidden aria-label="Open emoji favorites"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f604.png" alt="Emoji bud"></button> <div class="trigger-lab__palette" id="lab-palette-10" data-palette hidden aria-label="Emoji favorites"><div class="trigger-lab__palette-head"><b>EmojiCopy · Favorites</b><button class="trigger-lab__dismiss" data-dismiss aria-label="Close emoji favorites">Esc ×</button></div><div class="trigger-lab__choices"><button class="trigger-lab__choice jp-font" data-pick aria-label="Insert thumbs up"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f44d.png" alt="Thumbs up"></button><button class="trigger-lab__choice jp-font" data-pick aria-label="Insert party popper"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f389.png" alt="Party popper"></button><button class="trigger-lab__choice jp-font" data-pick aria-label="Insert heart"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/2764.png" alt="Heart"></button><button class="trigger-lab__choice jp-font" data-pick aria-label="Insert smile"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f604.png" alt="Smile"></button><button class="trigger-lab__choice jp-font" data-pick aria-label="Insert fire"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f525.png" alt="Fire"></button></div></div> </div></div><div class="trigger-lab__steps"><div class="trigger-lab__step" data-step="1"><strong>1</strong><div><b>Meet the caret</b><span>Move beside the blinking line and pause.</span></div></div><div class="trigger-lab__step" data-step="2"><strong>2</strong><div><b>A little smile appears</b><span>Above the caret, outside your words.</span></div></div><div class="trigger-lab__step" data-step="3"><strong>3</strong><div><b>Open it and pick</b><span>Click the smile, then an emoji.</span></div></div></div></div> <div class="trigger-lab__controls"><button class="trigger-lab__play" data-play aria-controls="lab-palette-10" aria-expanded="false">Watch activation</button><button class="trigger-lab__reset" data-reset>Reset</button><span class="trigger-lab__status" role="status"></span></div> <div class="trigger-lab__tradeoff"><span><b>Why it could work</b><br>The place you are already writing becomes the entry point.</span><span><b>What to test</b><br>Needs reliable caret geometry. No caret, no bud. Avoid buttons, selections, secure fields, and ongoing typing.</span></div></div>
/* Trigger storyboards: desktop mockups, not native overlays. */
.trigger-lab { display: flex; flex-direction: column; gap: var(--sp-5); font-size: 1.5rem; color: var(--c-text-primary); }
.trigger-lab__layout { display: grid; grid-template-columns: minmax(0, 1fr) 230px; gap: var(--sp-6); align-items: center; }
.trigger-lab__desktop { position: relative; min-width: 0; border-radius: var(--r-lg); overflow: hidden; background: var(--metal-200); border: 1px solid var(--c-border-primary); }
.trigger-lab__bar { display: flex; align-items: center; justify-content: space-between; padding: var(--sp-3) var(--sp-4); background: var(--metal-100); color: var(--metal-700); font-size: 1.2rem; }
.trigger-lab__bar b { color: var(--metal-900); }
.trigger-lab__canvas { position: relative; height: 355px; padding: var(--sp-5); background: linear-gradient(140deg, var(--metal-100), var(--green-400)); }
.trigger-lab__window { position: absolute; inset: 22px 22px 30px; background: var(--white); border: 1px solid var(--metal-200); border-radius: var(--r-md); overflow: hidden; box-shadow: 0 12px 30px color-mix(in srgb, var(--metal-900) 12%, transparent); }
.trigger-lab__title { display: flex; align-items: center; gap: var(--sp-2); padding: var(--sp-3) var(--sp-4); border-bottom: 1px solid var(--metal-100); color: var(--metal-600); font-size: 1.3rem; }
.trigger-lab__lights { display: flex; gap: var(--sp-1); }
.trigger-lab__lights i { width: 8px; height: 8px; border-radius: var(--r-pill); background: var(--metal-300); }
.trigger-lab__conversation { display: flex; flex-direction: column; align-items: flex-start; gap: var(--sp-2); padding: var(--sp-5); color: var(--metal-600); }
.trigger-lab__conversation b { font-size: 1.2rem; }
.trigger-lab__chat { background: var(--metal-100); padding: var(--sp-3) var(--sp-4); border-radius: var(--r-md); color: var(--metal-900); font-size: 1.4rem; }
.trigger-lab__composer { position: absolute; bottom: 28px; left: 18px; right: 18px; border: 2px solid var(--green-700); border-radius: var(--r-md); padding: var(--sp-4); color: var(--metal-900); display: flex; align-items: center; gap: var(--sp-1); min-height: 26px; font-size: 1.6rem; }
.trigger-lab__caret { height: 22px; width: 2px; background: var(--green-800); }
.trigger-lab__result img { width: 25px; height: 25px; vertical-align: middle; }
.trigger-lab__origin { position: absolute; right: 21%; bottom: 105px; width: 160px; height: 120px; pointer-events: none; }
.trigger-lab__pointer { position: absolute; bottom: 0; right: 30px; width: 26px; height: 32px; filter: drop-shadow(0 2px 2px color-mix(in srgb, var(--metal-900) 25%, transparent)); }
.trigger-lab__trail { position: absolute; bottom: 0; right: 5px; width: 88px; height: 88px; color: var(--green-800); opacity: .6; }
.trigger-lab__keys { position: absolute; bottom: 62px; right: 6px; display: flex; gap: var(--sp-1); white-space: nowrap; }
.trigger-lab__keys kbd { display: grid; place-items: center; min-width: 35px; height: 33px; padding: 0 var(--sp-2); font: 600 1.6rem var(--ff-inter), sans-serif; color: var(--metal-900); background: var(--white); border: 1px solid var(--metal-300); border-radius: var(--r-sm); box-shadow: 0 3px 0 var(--metal-200); }
.trigger-lab__seed { position: absolute; right: 23%; bottom: 152px; width: 54px; height: 54px; display: grid; place-items: center; border: 1px solid var(--green-600); border-radius: var(--r-pill); background: var(--white); box-shadow: 0 0 0 6px color-mix(in srgb, var(--green-500) 22%, transparent); cursor: pointer; z-index: 3; }
.trigger-lab__seed img { width: 34px; height: 34px; }
.trigger-lab__palette { position: absolute; z-index: 4; right: 8%; bottom: 145px; width: min(285px, 83%); padding: var(--sp-3); box-sizing: border-box; border-radius: var(--r-lg); background: var(--metal-900); color: var(--white); box-shadow: 0 12px 26px color-mix(in srgb, var(--metal-900) 25%, transparent); }
.trigger-lab__palette::after { content: ''; position: absolute; bottom: -7px; right: 52px; width: 16px; height: 16px; transform: rotate(45deg); background: var(--metal-900); }
.trigger-lab__palette-head { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2); font-size: 1.2rem; padding-bottom: var(--sp-2); }
.trigger-lab__choices { display: flex; justify-content: space-between; gap: var(--sp-1); }
.trigger-lab__choice { display: grid; place-items: center; cursor: pointer; border: 0; border-radius: var(--r-sm); background: var(--metal-800); width: 44px; height: 44px; }
.trigger-lab__choice:hover { background: var(--green-700); }
.trigger-lab__choice img { width: 29px; height: 29px; }
.trigger-lab__dismiss { border: 0; background: transparent; color: var(--metal-300); cursor: pointer; font: inherit; padding: var(--sp-1); }
.trigger-lab__edge { position: absolute; right: 0; bottom: 136px; background: var(--metal-900); color: var(--white); border: 0; border-radius: var(--r-md) 0 0 var(--r-md); padding: var(--sp-3); cursor: pointer; }
.trigger-lab__edge img { width: 24px; height: 24px; }
.trigger-lab__steps { display: flex; flex-direction: column; gap: var(--sp-5); }
.trigger-lab__step { display: grid; grid-template-columns: 28px 1fr; gap: var(--sp-2); align-items: start; line-height: 1.5; }
.trigger-lab__step strong { display: grid; place-items: center; height: 26px; border-radius: var(--r-pill); background: var(--c-surface-tertiary); font-size: 1.2rem; }
.trigger-lab__step b { display: block; }
.trigger-lab__step span { font-size: 1.35rem; color: var(--c-text-secondary); }
.trigger-lab__step.is-current strong { background: var(--green-500); color: var(--green-950); }
.trigger-lab__controls { display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap; }
.trigger-lab__play, .trigger-lab__reset { border: 1px solid var(--c-border-primary); border-radius: var(--r-pill); padding: var(--sp-3) var(--sp-5); font: 600 1.4rem var(--ff-inter), sans-serif; cursor: pointer; }
.trigger-lab__play { color: var(--green-950); background: var(--green-500); }
.trigger-lab__reset { color: var(--c-text-secondary); background: var(--c-surface-primary); }
.trigger-lab__status { font-size: 1.3rem; color: var(--c-text-secondary); flex: 1; min-width: 180px; }
.trigger-lab__tradeoff { display: flex; gap: var(--sp-4); font-size: 1.35rem; line-height: 1.6; color: var(--c-text-secondary); border-top: 1px solid var(--c-border-primary); padding-top: var(--sp-4); }
.trigger-lab [hidden] { display: none !important; }
.trigger-lab button:focus-visible { outline: 3px solid var(--green-700); outline-offset: 3px; }
.trigger-lab[data-phase='gesture'] .trigger-lab__pointer { animation: trigger-orbit 1s ease-in-out; }
.trigger-lab[data-phase='gesture'] .trigger-lab__keys { animation: trigger-press .5s ease-in-out 2; }
.trigger-lab[data-phase='gesture'][data-motion='dwell'] .trigger-lab__pointer { animation: trigger-approach 1s ease-out; }
.trigger-lab[data-phase='gesture'][data-motion='carry'] .trigger-lab__pointer { animation: trigger-carry 1s ease-in-out; }
.trigger-lab[data-phase='done'] .trigger-lab__origin { opacity: .2; }
@keyframes trigger-orbit { 0%,100% { transform: translate(0,0); } 25% { transform: translate(-48px,0); } 50% { transform: translate(-48px,-48px); } 75% { transform: translate(0,-48px); } }
@keyframes trigger-press { 50% { transform: translateY(4px); } }
@keyframes trigger-approach { from { transform: translate(80px,-70px); } to { transform: translate(0,0); } }
@keyframes trigger-carry { from { transform: translate(-150px,-100px); } to { transform: translate(0,0); } }
@media (prefers-reduced-motion: reduce) { .trigger-lab *, .trigger-lab *::after { animation: none !important; } }
@media (max-width: 760px) { .trigger-lab__layout { grid-template-columns: 1fr; } .trigger-lab__steps { flex-direction: row; flex-wrap: wrap; } .trigger-lab__step { flex: 1; min-width: 145px; } .trigger-lab__tradeoff { flex-direction: column; } }
@media (max-width: 450px) { .trigger-lab__window { inset: 15px 8px; } .trigger-lab__composer { left: 10px; right: 10px; padding: var(--sp-3); font-size: 1.4rem; } .trigger-lab__origin { right: 10%; } }
.trigger-intro { font-size: 1.5rem; line-height: 1.6; color: var(--c-text-secondary); margin-bottom: var(--sp-5); }
.trigger-lab__bar img { width: 15px; height: 15px; vertical-align: middle; }
.trigger-lab__keys img { width: 34px; height: 34px; }
.trigger-lab[data-motion='keys'] .trigger-lab__pointer { display: none; }
.trigger-lab[data-kind='4'] .trigger-lab__origin { top: 4px; right: 0; bottom: auto; }
.trigger-lab[data-kind='4'] .trigger-lab__palette { width: min(340px, 90%); right: 5%; bottom: 100px; }
.trigger-lab[data-kind='4'] .trigger-lab__choices { display: grid; grid-template-columns: repeat(5, 1fr); gap: var(--sp-2); }
.trigger-lab[data-kind='8'] .trigger-lab__palette { right: 42px; bottom: 132px; }
.trigger-lab[data-kind='10'] .trigger-lab__origin { right: auto; left: 155px; bottom: 87px; }
.trigger-lab[data-kind='10'] .trigger-lab__seed { right: auto; left: 210px; bottom: 145px; }
.trigger-lab[data-kind='10'] .trigger-lab__palette { left: 72px; right: auto; }
.trigger-lab[data-kind='11'][data-phase='gesture'] .trigger-lab__keys { animation: trigger-carry 1s ease-in-out; }
.trigger-lab__selected { background: var(--green-400); }
.trigger-lab[data-kind='10'] .trigger-lab__origin { left: 88px; }
.trigger-lab[data-kind='10'] .trigger-lab__seed { left: 164px; }
.trigger-lab[data-kind='11'][data-phase='bud'] .trigger-lab__keys,
.trigger-lab[data-kind='11'][data-phase='open'] .trigger-lab__keys { visibility: hidden; }
.trigger-lab[data-kind='4'][data-phase='gesture'] .trigger-lab__pointer { animation: trigger-approach 1s ease-out; }
.trigger-lab[data-kind='4'][data-phase='gesture'] .trigger-lab__keys { outline: 2px solid var(--green-700); outline-offset: 5px; border-radius: var(--r-pill); }
// Isolated storyboards. Buttons simulate native activation; no global input is captured.
document.querySelectorAll('.trigger-lab').forEach(lab => {
const seed = lab.querySelector('[data-seed]')
const palette = lab.querySelector('[data-palette]')
const play = lab.querySelector('[data-play]')
const status = lab.querySelector('[role="status"]')
let timer
const phase = (name, step) => {
lab.dataset.phase = name
lab.querySelectorAll('[data-step]').forEach(item => item.classList.toggle('is-current', Number(item.dataset.step) === step))
}
const open = () => {
clearTimeout(timer)
seed.hidden = true
palette.hidden = false
play.setAttribute('aria-expanded', 'true')
phase('open', 2)
status.textContent = 'Click an emoji to insert it in this example. Escape cancels.'
palette.querySelector('[data-pick]').focus()
}
const reset = () => {
clearTimeout(timer)
seed.hidden = true
palette.hidden = true
play.setAttribute('aria-expanded', 'false')
const result = lab.querySelector('[data-result]')
result.textContent = result.dataset.initial || ''
phase('ready', 1)
status.textContent = 'Motion demo only · click “Watch activation” to begin.'
}
const cancel = () => {
clearTimeout(timer)
seed.hidden = true
palette.hidden = true
play.setAttribute('aria-expanded', 'false')
phase('ready', 1)
status.textContent = 'Dismissed. The message was not changed.'
play.focus()
}
play.addEventListener('click', () => {
reset()
phase('gesture', 1)
status.textContent = lab.dataset.action
timer = setTimeout(() => {
if (lab.dataset.bud === 'true') {
seed.hidden = false
phase('bud', 2)
status.textContent = 'The emoji bud is here. Click it to unfold favorites.'
seed.focus()
} else open()
}, 1050)
})
seed.addEventListener('click', open)
lab.querySelector('[data-reset]').addEventListener('click', reset)
lab.querySelector('[data-dismiss]').addEventListener('click', cancel)
lab.querySelector('[data-edge]')?.addEventListener('click', open)
lab.querySelectorAll('[data-pick]').forEach(button => button.addEventListener('click', () => {
lab.querySelector('[data-result]').replaceChildren(button.querySelector('img').cloneNode())
seed.hidden = true
palette.hidden = true
play.setAttribute('aria-expanded', 'false')
phase('done', 3)
status.textContent = 'Inserted here. In the proposed Mac flow, you keep typing in your app.'
play.focus()
}))
lab.addEventListener('keydown', event => {
if (event.key === 'Escape') { event.stopPropagation(); cancel() }
})
reset()
})
CONCEPT 10: Meet the caret Rest the pointer beside the text caret Needs reliable caret geometry. No caret, no bud. Avoid buttons, selections, secure fields, and ongoing typing. This browser storyboard simulates the input using Watch activation; it does not detect a system-wide gesture or insert into another app. No claim of an unprecedented invention or native feasibility is made. Native sequence: capture verified editable target → recognize explicit activation → reveal bud or palette without modifying text → explicit emoji selection → revalidate target → insert, with one undo step. If no target is available, offer Copy explicitly. Cancel never changes text. Respect existing app controls, secure input, display bounds, and the user's opt-in setting. For buds: reveal beside the active pointer/caret, retain a generous travel corridor between pointer and bud, dismiss on continued typing, focus change, Escape, or movement away. Never insert on hover. Test the reveal delay and false positives with real users. Pocket mode is entered only via Carry in EmojiCopy. The carried emoji remains a proposal until a user confirms insertion. Clear after one successful insertion by default. Integration: mount this HTML once per demo, include the complete CSS and JS below. Source IDs must remain unique. Native implementation is a separate prototype. The browser mock uses PNGs; actual insertion must use the selected Unicode sequence.
<div class="trigger-lab" data-kind="11" data-motion="carry" data-bud="true" data-action="Carry a favorite from EmojiCopy to your next message"> <div class="trigger-lab__layout"><div class="trigger-lab__desktop"> <div class="trigger-lab__bar"><b>Messages</b><span>EmojiCopy <span class="jp-font"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f604.png" alt="Menu bar icon"></span></span></div> <div class="trigger-lab__canvas"><div class="trigger-lab__window"><div class="trigger-lab__title"><span class="trigger-lab__lights"><i></i><i></i><i></i></span> Team chat</div><div class="trigger-lab__conversation"><b>Casey</b><div class="trigger-lab__chat">The new design is ready!</div></div><div class="trigger-lab__composer">That looks great <span class="trigger-lab__result jp-font" data-result></span><span class="trigger-lab__caret"></span></div></div> <div class="trigger-lab__origin"><div class="trigger-lab__keys jp-font"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f44d.png" alt="Carried thumbs up"></div><svg class="trigger-lab__pointer" viewBox="0 0 26 32" aria-hidden="true"><path d="M3 2L23 20L14 21L10 30Z" fill="var(--metal-950)" stroke="var(--white)" stroke-width="2"/></svg></div> <button class="trigger-lab__seed jp-font" data-seed hidden aria-label="Open emoji favorites"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f44d.png" alt="Emoji bud"></button> <div class="trigger-lab__palette" id="lab-palette-11" data-palette hidden aria-label="Emoji favorites"><div class="trigger-lab__palette-head"><b>EmojiCopy · Favorites</b><button class="trigger-lab__dismiss" data-dismiss aria-label="Close emoji favorites">Esc ×</button></div><div class="trigger-lab__choices"><button class="trigger-lab__choice jp-font" data-pick aria-label="Insert thumbs up"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f44d.png" alt="Thumbs up"></button><button class="trigger-lab__choice jp-font" data-pick aria-label="Insert party popper"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f389.png" alt="Party popper"></button><button class="trigger-lab__choice jp-font" data-pick aria-label="Insert heart"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/2764.png" alt="Heart"></button><button class="trigger-lab__choice jp-font" data-pick aria-label="Insert smile"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f604.png" alt="Smile"></button><button class="trigger-lab__choice jp-font" data-pick aria-label="Insert fire"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f525.png" alt="Fire"></button></div></div> </div></div><div class="trigger-lab__steps"><div class="trigger-lab__step" data-step="1"><strong>1</strong><div><b>Pocket a favorite</b><span>Use “Carry” on an emoji in the picker.</span></div></div><div class="trigger-lab__step" data-step="2"><strong>2</strong><div><b>It travels with you</b><span>A small badge follows the pointer to a field.</span></div></div><div class="trigger-lab__step" data-step="3"><strong>3</strong><div><b>Click the badge and pick</b><span>Confirm the carried emoji or choose another favorite.</span></div></div></div></div> <div class="trigger-lab__controls"><button class="trigger-lab__play" data-play aria-controls="lab-palette-11" aria-expanded="false">Watch activation</button><button class="trigger-lab__reset" data-reset>Reset</button><span class="trigger-lab__status" role="status"></span></div> <div class="trigger-lab__tradeoff"><span><b>Why it could work</b><br>Useful when you repeat the same reaction across several apps.</span><span><b>What to test</b><br>Explicit temporary carry mode, not a permanent follower. Escape clears it. Validate a target before offering Insert.</span></div></div>
/* Trigger storyboards: desktop mockups, not native overlays. */
.trigger-lab { display: flex; flex-direction: column; gap: var(--sp-5); font-size: 1.5rem; color: var(--c-text-primary); }
.trigger-lab__layout { display: grid; grid-template-columns: minmax(0, 1fr) 230px; gap: var(--sp-6); align-items: center; }
.trigger-lab__desktop { position: relative; min-width: 0; border-radius: var(--r-lg); overflow: hidden; background: var(--metal-200); border: 1px solid var(--c-border-primary); }
.trigger-lab__bar { display: flex; align-items: center; justify-content: space-between; padding: var(--sp-3) var(--sp-4); background: var(--metal-100); color: var(--metal-700); font-size: 1.2rem; }
.trigger-lab__bar b { color: var(--metal-900); }
.trigger-lab__canvas { position: relative; height: 355px; padding: var(--sp-5); background: linear-gradient(140deg, var(--metal-100), var(--green-400)); }
.trigger-lab__window { position: absolute; inset: 22px 22px 30px; background: var(--white); border: 1px solid var(--metal-200); border-radius: var(--r-md); overflow: hidden; box-shadow: 0 12px 30px color-mix(in srgb, var(--metal-900) 12%, transparent); }
.trigger-lab__title { display: flex; align-items: center; gap: var(--sp-2); padding: var(--sp-3) var(--sp-4); border-bottom: 1px solid var(--metal-100); color: var(--metal-600); font-size: 1.3rem; }
.trigger-lab__lights { display: flex; gap: var(--sp-1); }
.trigger-lab__lights i { width: 8px; height: 8px; border-radius: var(--r-pill); background: var(--metal-300); }
.trigger-lab__conversation { display: flex; flex-direction: column; align-items: flex-start; gap: var(--sp-2); padding: var(--sp-5); color: var(--metal-600); }
.trigger-lab__conversation b { font-size: 1.2rem; }
.trigger-lab__chat { background: var(--metal-100); padding: var(--sp-3) var(--sp-4); border-radius: var(--r-md); color: var(--metal-900); font-size: 1.4rem; }
.trigger-lab__composer { position: absolute; bottom: 28px; left: 18px; right: 18px; border: 2px solid var(--green-700); border-radius: var(--r-md); padding: var(--sp-4); color: var(--metal-900); display: flex; align-items: center; gap: var(--sp-1); min-height: 26px; font-size: 1.6rem; }
.trigger-lab__caret { height: 22px; width: 2px; background: var(--green-800); }
.trigger-lab__result img { width: 25px; height: 25px; vertical-align: middle; }
.trigger-lab__origin { position: absolute; right: 21%; bottom: 105px; width: 160px; height: 120px; pointer-events: none; }
.trigger-lab__pointer { position: absolute; bottom: 0; right: 30px; width: 26px; height: 32px; filter: drop-shadow(0 2px 2px color-mix(in srgb, var(--metal-900) 25%, transparent)); }
.trigger-lab__trail { position: absolute; bottom: 0; right: 5px; width: 88px; height: 88px; color: var(--green-800); opacity: .6; }
.trigger-lab__keys { position: absolute; bottom: 62px; right: 6px; display: flex; gap: var(--sp-1); white-space: nowrap; }
.trigger-lab__keys kbd { display: grid; place-items: center; min-width: 35px; height: 33px; padding: 0 var(--sp-2); font: 600 1.6rem var(--ff-inter), sans-serif; color: var(--metal-900); background: var(--white); border: 1px solid var(--metal-300); border-radius: var(--r-sm); box-shadow: 0 3px 0 var(--metal-200); }
.trigger-lab__seed { position: absolute; right: 23%; bottom: 152px; width: 54px; height: 54px; display: grid; place-items: center; border: 1px solid var(--green-600); border-radius: var(--r-pill); background: var(--white); box-shadow: 0 0 0 6px color-mix(in srgb, var(--green-500) 22%, transparent); cursor: pointer; z-index: 3; }
.trigger-lab__seed img { width: 34px; height: 34px; }
.trigger-lab__palette { position: absolute; z-index: 4; right: 8%; bottom: 145px; width: min(285px, 83%); padding: var(--sp-3); box-sizing: border-box; border-radius: var(--r-lg); background: var(--metal-900); color: var(--white); box-shadow: 0 12px 26px color-mix(in srgb, var(--metal-900) 25%, transparent); }
.trigger-lab__palette::after { content: ''; position: absolute; bottom: -7px; right: 52px; width: 16px; height: 16px; transform: rotate(45deg); background: var(--metal-900); }
.trigger-lab__palette-head { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2); font-size: 1.2rem; padding-bottom: var(--sp-2); }
.trigger-lab__choices { display: flex; justify-content: space-between; gap: var(--sp-1); }
.trigger-lab__choice { display: grid; place-items: center; cursor: pointer; border: 0; border-radius: var(--r-sm); background: var(--metal-800); width: 44px; height: 44px; }
.trigger-lab__choice:hover { background: var(--green-700); }
.trigger-lab__choice img { width: 29px; height: 29px; }
.trigger-lab__dismiss { border: 0; background: transparent; color: var(--metal-300); cursor: pointer; font: inherit; padding: var(--sp-1); }
.trigger-lab__edge { position: absolute; right: 0; bottom: 136px; background: var(--metal-900); color: var(--white); border: 0; border-radius: var(--r-md) 0 0 var(--r-md); padding: var(--sp-3); cursor: pointer; }
.trigger-lab__edge img { width: 24px; height: 24px; }
.trigger-lab__steps { display: flex; flex-direction: column; gap: var(--sp-5); }
.trigger-lab__step { display: grid; grid-template-columns: 28px 1fr; gap: var(--sp-2); align-items: start; line-height: 1.5; }
.trigger-lab__step strong { display: grid; place-items: center; height: 26px; border-radius: var(--r-pill); background: var(--c-surface-tertiary); font-size: 1.2rem; }
.trigger-lab__step b { display: block; }
.trigger-lab__step span { font-size: 1.35rem; color: var(--c-text-secondary); }
.trigger-lab__step.is-current strong { background: var(--green-500); color: var(--green-950); }
.trigger-lab__controls { display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap; }
.trigger-lab__play, .trigger-lab__reset { border: 1px solid var(--c-border-primary); border-radius: var(--r-pill); padding: var(--sp-3) var(--sp-5); font: 600 1.4rem var(--ff-inter), sans-serif; cursor: pointer; }
.trigger-lab__play { color: var(--green-950); background: var(--green-500); }
.trigger-lab__reset { color: var(--c-text-secondary); background: var(--c-surface-primary); }
.trigger-lab__status { font-size: 1.3rem; color: var(--c-text-secondary); flex: 1; min-width: 180px; }
.trigger-lab__tradeoff { display: flex; gap: var(--sp-4); font-size: 1.35rem; line-height: 1.6; color: var(--c-text-secondary); border-top: 1px solid var(--c-border-primary); padding-top: var(--sp-4); }
.trigger-lab [hidden] { display: none !important; }
.trigger-lab button:focus-visible { outline: 3px solid var(--green-700); outline-offset: 3px; }
.trigger-lab[data-phase='gesture'] .trigger-lab__pointer { animation: trigger-orbit 1s ease-in-out; }
.trigger-lab[data-phase='gesture'] .trigger-lab__keys { animation: trigger-press .5s ease-in-out 2; }
.trigger-lab[data-phase='gesture'][data-motion='dwell'] .trigger-lab__pointer { animation: trigger-approach 1s ease-out; }
.trigger-lab[data-phase='gesture'][data-motion='carry'] .trigger-lab__pointer { animation: trigger-carry 1s ease-in-out; }
.trigger-lab[data-phase='done'] .trigger-lab__origin { opacity: .2; }
@keyframes trigger-orbit { 0%,100% { transform: translate(0,0); } 25% { transform: translate(-48px,0); } 50% { transform: translate(-48px,-48px); } 75% { transform: translate(0,-48px); } }
@keyframes trigger-press { 50% { transform: translateY(4px); } }
@keyframes trigger-approach { from { transform: translate(80px,-70px); } to { transform: translate(0,0); } }
@keyframes trigger-carry { from { transform: translate(-150px,-100px); } to { transform: translate(0,0); } }
@media (prefers-reduced-motion: reduce) { .trigger-lab *, .trigger-lab *::after { animation: none !important; } }
@media (max-width: 760px) { .trigger-lab__layout { grid-template-columns: 1fr; } .trigger-lab__steps { flex-direction: row; flex-wrap: wrap; } .trigger-lab__step { flex: 1; min-width: 145px; } .trigger-lab__tradeoff { flex-direction: column; } }
@media (max-width: 450px) { .trigger-lab__window { inset: 15px 8px; } .trigger-lab__composer { left: 10px; right: 10px; padding: var(--sp-3); font-size: 1.4rem; } .trigger-lab__origin { right: 10%; } }
.trigger-intro { font-size: 1.5rem; line-height: 1.6; color: var(--c-text-secondary); margin-bottom: var(--sp-5); }
.trigger-lab__bar img { width: 15px; height: 15px; vertical-align: middle; }
.trigger-lab__keys img { width: 34px; height: 34px; }
.trigger-lab[data-motion='keys'] .trigger-lab__pointer { display: none; }
.trigger-lab[data-kind='4'] .trigger-lab__origin { top: 4px; right: 0; bottom: auto; }
.trigger-lab[data-kind='4'] .trigger-lab__palette { width: min(340px, 90%); right: 5%; bottom: 100px; }
.trigger-lab[data-kind='4'] .trigger-lab__choices { display: grid; grid-template-columns: repeat(5, 1fr); gap: var(--sp-2); }
.trigger-lab[data-kind='8'] .trigger-lab__palette { right: 42px; bottom: 132px; }
.trigger-lab[data-kind='10'] .trigger-lab__origin { right: auto; left: 155px; bottom: 87px; }
.trigger-lab[data-kind='10'] .trigger-lab__seed { right: auto; left: 210px; bottom: 145px; }
.trigger-lab[data-kind='10'] .trigger-lab__palette { left: 72px; right: auto; }
.trigger-lab[data-kind='11'][data-phase='gesture'] .trigger-lab__keys { animation: trigger-carry 1s ease-in-out; }
.trigger-lab__selected { background: var(--green-400); }
.trigger-lab[data-kind='10'] .trigger-lab__origin { left: 88px; }
.trigger-lab[data-kind='10'] .trigger-lab__seed { left: 164px; }
.trigger-lab[data-kind='11'][data-phase='bud'] .trigger-lab__keys,
.trigger-lab[data-kind='11'][data-phase='open'] .trigger-lab__keys { visibility: hidden; }
.trigger-lab[data-kind='4'][data-phase='gesture'] .trigger-lab__pointer { animation: trigger-approach 1s ease-out; }
.trigger-lab[data-kind='4'][data-phase='gesture'] .trigger-lab__keys { outline: 2px solid var(--green-700); outline-offset: 5px; border-radius: var(--r-pill); }
// Isolated storyboards. Buttons simulate native activation; no global input is captured.
document.querySelectorAll('.trigger-lab').forEach(lab => {
const seed = lab.querySelector('[data-seed]')
const palette = lab.querySelector('[data-palette]')
const play = lab.querySelector('[data-play]')
const status = lab.querySelector('[role="status"]')
let timer
const phase = (name, step) => {
lab.dataset.phase = name
lab.querySelectorAll('[data-step]').forEach(item => item.classList.toggle('is-current', Number(item.dataset.step) === step))
}
const open = () => {
clearTimeout(timer)
seed.hidden = true
palette.hidden = false
play.setAttribute('aria-expanded', 'true')
phase('open', 2)
status.textContent = 'Click an emoji to insert it in this example. Escape cancels.'
palette.querySelector('[data-pick]').focus()
}
const reset = () => {
clearTimeout(timer)
seed.hidden = true
palette.hidden = true
play.setAttribute('aria-expanded', 'false')
const result = lab.querySelector('[data-result]')
result.textContent = result.dataset.initial || ''
phase('ready', 1)
status.textContent = 'Motion demo only · click “Watch activation” to begin.'
}
const cancel = () => {
clearTimeout(timer)
seed.hidden = true
palette.hidden = true
play.setAttribute('aria-expanded', 'false')
phase('ready', 1)
status.textContent = 'Dismissed. The message was not changed.'
play.focus()
}
play.addEventListener('click', () => {
reset()
phase('gesture', 1)
status.textContent = lab.dataset.action
timer = setTimeout(() => {
if (lab.dataset.bud === 'true') {
seed.hidden = false
phase('bud', 2)
status.textContent = 'The emoji bud is here. Click it to unfold favorites.'
seed.focus()
} else open()
}, 1050)
})
seed.addEventListener('click', open)
lab.querySelector('[data-reset]').addEventListener('click', reset)
lab.querySelector('[data-dismiss]').addEventListener('click', cancel)
lab.querySelector('[data-edge]')?.addEventListener('click', open)
lab.querySelectorAll('[data-pick]').forEach(button => button.addEventListener('click', () => {
lab.querySelector('[data-result]').replaceChildren(button.querySelector('img').cloneNode())
seed.hidden = true
palette.hidden = true
play.setAttribute('aria-expanded', 'false')
phase('done', 3)
status.textContent = 'Inserted here. In the proposed Mac flow, you keep typing in your app.'
play.focus()
}))
lab.addEventListener('keydown', event => {
if (event.key === 'Escape') { event.stopPropagation(); cancel() }
})
reset()
})
CONCEPT 11: Pocket emoji Carry a favorite from EmojiCopy to your next message Explicit temporary carry mode, not a permanent follower. Escape clears it. Validate a target before offering Insert. This browser storyboard simulates the input using Watch activation; it does not detect a system-wide gesture or insert into another app. No claim of an unprecedented invention or native feasibility is made. Native sequence: capture verified editable target → recognize explicit activation → reveal bud or palette without modifying text → explicit emoji selection → revalidate target → insert, with one undo step. If no target is available, offer Copy explicitly. Cancel never changes text. Respect existing app controls, secure input, display bounds, and the user's opt-in setting. For buds: reveal beside the active pointer/caret, retain a generous travel corridor between pointer and bud, dismiss on continued typing, focus change, Escape, or movement away. Never insert on hover. Test the reveal delay and false positives with real users. Pocket mode is entered only via Carry in EmojiCopy. The carried emoji remains a proposal until a user confirms insertion. Clear after one successful insertion by default. Integration: mount this HTML once per demo, include the complete CSS and JS below. Source IDs must remain unique. Native implementation is a separate prototype. The browser mock uses PNGs; actual insertion must use the selected Unicode sequence.
<div class="trigger-lab" data-kind="1" data-motion="keys" data-bud="false" data-action="Type :th after your message"> <div class="trigger-lab__layout"><div class="trigger-lab__desktop"> <div class="trigger-lab__bar"><b>Messages</b><span>EmojiCopy <span class="jp-font"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f604.png" alt="Menu bar icon"></span></span></div> <div class="trigger-lab__canvas"><div class="trigger-lab__window"><div class="trigger-lab__title"><span class="trigger-lab__lights"><i></i><i></i><i></i></span> Team chat</div><div class="trigger-lab__conversation"><b>Casey</b><div class="trigger-lab__chat">The new design is ready!</div></div><div class="trigger-lab__composer">That looks great <span class="trigger-lab__result jp-font" data-result data-initial=" :th"> :th</span><span class="trigger-lab__caret"></span></div></div> <div class="trigger-lab__origin"><div class="trigger-lab__keys"><kbd>:</kbd><kbd>t</kbd><kbd>h</kbd></div><svg class="trigger-lab__pointer" viewBox="0 0 26 32" aria-hidden="true"><path d="M3 2L23 20L14 21L10 30Z" fill="var(--metal-950)" stroke="var(--white)" stroke-width="2"/></svg></div> <button class="trigger-lab__seed jp-font" data-seed hidden aria-label="Open emoji favorites"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f44d.png" alt="Emoji bud"></button> <div class="trigger-lab__palette" id="lab-palette-1" data-palette hidden aria-label="Emoji favorites"><div class="trigger-lab__palette-head"><b>EmojiCopy · Favorites</b><button class="trigger-lab__dismiss" data-dismiss aria-label="Close emoji favorites">Esc ×</button></div><div class="trigger-lab__choices"><button class="trigger-lab__choice jp-font" data-pick aria-label="Insert thumbs up"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f44d.png" alt="Thumbs up"></button><button class="trigger-lab__choice jp-font" data-pick aria-label="Insert party popper"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f389.png" alt="Party popper"></button><button class="trigger-lab__choice jp-font" data-pick aria-label="Insert heart"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/2764.png" alt="Heart"></button><button class="trigger-lab__choice jp-font" data-pick aria-label="Insert smile"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f604.png" alt="Smile"></button><button class="trigger-lab__choice jp-font" data-pick aria-label="Insert fire"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f525.png" alt="Fire"></button></div></div> </div></div><div class="trigger-lab__steps"><div class="trigger-lab__step" data-step="1"><strong>1</strong><div><b>Type :th</b><span>Keep typing in the same field.</span></div></div><div class="trigger-lab__step" data-step="2"><strong>2</strong><div><b>Matches appear</b><span>A small row sits above the text.</span></div></div><div class="trigger-lab__step" data-step="3"><strong>3</strong><div><b>Choose thumbs up</b><span>The shortcode becomes an emoji.</span></div></div></div></div> <div class="trigger-lab__controls"><button class="trigger-lab__play" data-play aria-controls="lab-palette-1" aria-expanded="false">Watch activation</button><button class="trigger-lab__reset" data-reset>Reset</button><span class="trigger-lab__status" role="status"></span></div> <div class="trigger-lab__tradeoff"><span><b>Why it could work</b><br>Familiar to people who use shortcodes.</span><span><b>What to test</b><br>Native insertion must replace only the verified shortcode.</span></div></div>
/* Trigger storyboards: desktop mockups, not native overlays. */
.trigger-lab { display: flex; flex-direction: column; gap: var(--sp-5); font-size: 1.5rem; color: var(--c-text-primary); }
.trigger-lab__layout { display: grid; grid-template-columns: minmax(0, 1fr) 230px; gap: var(--sp-6); align-items: center; }
.trigger-lab__desktop { position: relative; min-width: 0; border-radius: var(--r-lg); overflow: hidden; background: var(--metal-200); border: 1px solid var(--c-border-primary); }
.trigger-lab__bar { display: flex; align-items: center; justify-content: space-between; padding: var(--sp-3) var(--sp-4); background: var(--metal-100); color: var(--metal-700); font-size: 1.2rem; }
.trigger-lab__bar b { color: var(--metal-900); }
.trigger-lab__canvas { position: relative; height: 355px; padding: var(--sp-5); background: linear-gradient(140deg, var(--metal-100), var(--green-400)); }
.trigger-lab__window { position: absolute; inset: 22px 22px 30px; background: var(--white); border: 1px solid var(--metal-200); border-radius: var(--r-md); overflow: hidden; box-shadow: 0 12px 30px color-mix(in srgb, var(--metal-900) 12%, transparent); }
.trigger-lab__title { display: flex; align-items: center; gap: var(--sp-2); padding: var(--sp-3) var(--sp-4); border-bottom: 1px solid var(--metal-100); color: var(--metal-600); font-size: 1.3rem; }
.trigger-lab__lights { display: flex; gap: var(--sp-1); }
.trigger-lab__lights i { width: 8px; height: 8px; border-radius: var(--r-pill); background: var(--metal-300); }
.trigger-lab__conversation { display: flex; flex-direction: column; align-items: flex-start; gap: var(--sp-2); padding: var(--sp-5); color: var(--metal-600); }
.trigger-lab__conversation b { font-size: 1.2rem; }
.trigger-lab__chat { background: var(--metal-100); padding: var(--sp-3) var(--sp-4); border-radius: var(--r-md); color: var(--metal-900); font-size: 1.4rem; }
.trigger-lab__composer { position: absolute; bottom: 28px; left: 18px; right: 18px; border: 2px solid var(--green-700); border-radius: var(--r-md); padding: var(--sp-4); color: var(--metal-900); display: flex; align-items: center; gap: var(--sp-1); min-height: 26px; font-size: 1.6rem; }
.trigger-lab__caret { height: 22px; width: 2px; background: var(--green-800); }
.trigger-lab__result img { width: 25px; height: 25px; vertical-align: middle; }
.trigger-lab__origin { position: absolute; right: 21%; bottom: 105px; width: 160px; height: 120px; pointer-events: none; }
.trigger-lab__pointer { position: absolute; bottom: 0; right: 30px; width: 26px; height: 32px; filter: drop-shadow(0 2px 2px color-mix(in srgb, var(--metal-900) 25%, transparent)); }
.trigger-lab__trail { position: absolute; bottom: 0; right: 5px; width: 88px; height: 88px; color: var(--green-800); opacity: .6; }
.trigger-lab__keys { position: absolute; bottom: 62px; right: 6px; display: flex; gap: var(--sp-1); white-space: nowrap; }
.trigger-lab__keys kbd { display: grid; place-items: center; min-width: 35px; height: 33px; padding: 0 var(--sp-2); font: 600 1.6rem var(--ff-inter), sans-serif; color: var(--metal-900); background: var(--white); border: 1px solid var(--metal-300); border-radius: var(--r-sm); box-shadow: 0 3px 0 var(--metal-200); }
.trigger-lab__seed { position: absolute; right: 23%; bottom: 152px; width: 54px; height: 54px; display: grid; place-items: center; border: 1px solid var(--green-600); border-radius: var(--r-pill); background: var(--white); box-shadow: 0 0 0 6px color-mix(in srgb, var(--green-500) 22%, transparent); cursor: pointer; z-index: 3; }
.trigger-lab__seed img { width: 34px; height: 34px; }
.trigger-lab__palette { position: absolute; z-index: 4; right: 8%; bottom: 145px; width: min(285px, 83%); padding: var(--sp-3); box-sizing: border-box; border-radius: var(--r-lg); background: var(--metal-900); color: var(--white); box-shadow: 0 12px 26px color-mix(in srgb, var(--metal-900) 25%, transparent); }
.trigger-lab__palette::after { content: ''; position: absolute; bottom: -7px; right: 52px; width: 16px; height: 16px; transform: rotate(45deg); background: var(--metal-900); }
.trigger-lab__palette-head { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2); font-size: 1.2rem; padding-bottom: var(--sp-2); }
.trigger-lab__choices { display: flex; justify-content: space-between; gap: var(--sp-1); }
.trigger-lab__choice { display: grid; place-items: center; cursor: pointer; border: 0; border-radius: var(--r-sm); background: var(--metal-800); width: 44px; height: 44px; }
.trigger-lab__choice:hover { background: var(--green-700); }
.trigger-lab__choice img { width: 29px; height: 29px; }
.trigger-lab__dismiss { border: 0; background: transparent; color: var(--metal-300); cursor: pointer; font: inherit; padding: var(--sp-1); }
.trigger-lab__edge { position: absolute; right: 0; bottom: 136px; background: var(--metal-900); color: var(--white); border: 0; border-radius: var(--r-md) 0 0 var(--r-md); padding: var(--sp-3); cursor: pointer; }
.trigger-lab__edge img { width: 24px; height: 24px; }
.trigger-lab__steps { display: flex; flex-direction: column; gap: var(--sp-5); }
.trigger-lab__step { display: grid; grid-template-columns: 28px 1fr; gap: var(--sp-2); align-items: start; line-height: 1.5; }
.trigger-lab__step strong { display: grid; place-items: center; height: 26px; border-radius: var(--r-pill); background: var(--c-surface-tertiary); font-size: 1.2rem; }
.trigger-lab__step b { display: block; }
.trigger-lab__step span { font-size: 1.35rem; color: var(--c-text-secondary); }
.trigger-lab__step.is-current strong { background: var(--green-500); color: var(--green-950); }
.trigger-lab__controls { display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap; }
.trigger-lab__play, .trigger-lab__reset { border: 1px solid var(--c-border-primary); border-radius: var(--r-pill); padding: var(--sp-3) var(--sp-5); font: 600 1.4rem var(--ff-inter), sans-serif; cursor: pointer; }
.trigger-lab__play { color: var(--green-950); background: var(--green-500); }
.trigger-lab__reset { color: var(--c-text-secondary); background: var(--c-surface-primary); }
.trigger-lab__status { font-size: 1.3rem; color: var(--c-text-secondary); flex: 1; min-width: 180px; }
.trigger-lab__tradeoff { display: flex; gap: var(--sp-4); font-size: 1.35rem; line-height: 1.6; color: var(--c-text-secondary); border-top: 1px solid var(--c-border-primary); padding-top: var(--sp-4); }
.trigger-lab [hidden] { display: none !important; }
.trigger-lab button:focus-visible { outline: 3px solid var(--green-700); outline-offset: 3px; }
.trigger-lab[data-phase='gesture'] .trigger-lab__pointer { animation: trigger-orbit 1s ease-in-out; }
.trigger-lab[data-phase='gesture'] .trigger-lab__keys { animation: trigger-press .5s ease-in-out 2; }
.trigger-lab[data-phase='gesture'][data-motion='dwell'] .trigger-lab__pointer { animation: trigger-approach 1s ease-out; }
.trigger-lab[data-phase='gesture'][data-motion='carry'] .trigger-lab__pointer { animation: trigger-carry 1s ease-in-out; }
.trigger-lab[data-phase='done'] .trigger-lab__origin { opacity: .2; }
@keyframes trigger-orbit { 0%,100% { transform: translate(0,0); } 25% { transform: translate(-48px,0); } 50% { transform: translate(-48px,-48px); } 75% { transform: translate(0,-48px); } }
@keyframes trigger-press { 50% { transform: translateY(4px); } }
@keyframes trigger-approach { from { transform: translate(80px,-70px); } to { transform: translate(0,0); } }
@keyframes trigger-carry { from { transform: translate(-150px,-100px); } to { transform: translate(0,0); } }
@media (prefers-reduced-motion: reduce) { .trigger-lab *, .trigger-lab *::after { animation: none !important; } }
@media (max-width: 760px) { .trigger-lab__layout { grid-template-columns: 1fr; } .trigger-lab__steps { flex-direction: row; flex-wrap: wrap; } .trigger-lab__step { flex: 1; min-width: 145px; } .trigger-lab__tradeoff { flex-direction: column; } }
@media (max-width: 450px) { .trigger-lab__window { inset: 15px 8px; } .trigger-lab__composer { left: 10px; right: 10px; padding: var(--sp-3); font-size: 1.4rem; } .trigger-lab__origin { right: 10%; } }
.trigger-intro { font-size: 1.5rem; line-height: 1.6; color: var(--c-text-secondary); margin-bottom: var(--sp-5); }
.trigger-lab__bar img { width: 15px; height: 15px; vertical-align: middle; }
.trigger-lab__keys img { width: 34px; height: 34px; }
.trigger-lab[data-motion='keys'] .trigger-lab__pointer { display: none; }
.trigger-lab[data-kind='4'] .trigger-lab__origin { top: 4px; right: 0; bottom: auto; }
.trigger-lab[data-kind='4'] .trigger-lab__palette { width: min(340px, 90%); right: 5%; bottom: 100px; }
.trigger-lab[data-kind='4'] .trigger-lab__choices { display: grid; grid-template-columns: repeat(5, 1fr); gap: var(--sp-2); }
.trigger-lab[data-kind='8'] .trigger-lab__palette { right: 42px; bottom: 132px; }
.trigger-lab[data-kind='10'] .trigger-lab__origin { right: auto; left: 155px; bottom: 87px; }
.trigger-lab[data-kind='10'] .trigger-lab__seed { right: auto; left: 210px; bottom: 145px; }
.trigger-lab[data-kind='10'] .trigger-lab__palette { left: 72px; right: auto; }
.trigger-lab[data-kind='11'][data-phase='gesture'] .trigger-lab__keys { animation: trigger-carry 1s ease-in-out; }
.trigger-lab__selected { background: var(--green-400); }
.trigger-lab[data-kind='10'] .trigger-lab__origin { left: 88px; }
.trigger-lab[data-kind='10'] .trigger-lab__seed { left: 164px; }
.trigger-lab[data-kind='11'][data-phase='bud'] .trigger-lab__keys,
.trigger-lab[data-kind='11'][data-phase='open'] .trigger-lab__keys { visibility: hidden; }
.trigger-lab[data-kind='4'][data-phase='gesture'] .trigger-lab__pointer { animation: trigger-approach 1s ease-out; }
.trigger-lab[data-kind='4'][data-phase='gesture'] .trigger-lab__keys { outline: 2px solid var(--green-700); outline-offset: 5px; border-radius: var(--r-pill); }
// Isolated storyboards. Buttons simulate native activation; no global input is captured.
document.querySelectorAll('.trigger-lab').forEach(lab => {
const seed = lab.querySelector('[data-seed]')
const palette = lab.querySelector('[data-palette]')
const play = lab.querySelector('[data-play]')
const status = lab.querySelector('[role="status"]')
let timer
const phase = (name, step) => {
lab.dataset.phase = name
lab.querySelectorAll('[data-step]').forEach(item => item.classList.toggle('is-current', Number(item.dataset.step) === step))
}
const open = () => {
clearTimeout(timer)
seed.hidden = true
palette.hidden = false
play.setAttribute('aria-expanded', 'true')
phase('open', 2)
status.textContent = 'Click an emoji to insert it in this example. Escape cancels.'
palette.querySelector('[data-pick]').focus()
}
const reset = () => {
clearTimeout(timer)
seed.hidden = true
palette.hidden = true
play.setAttribute('aria-expanded', 'false')
const result = lab.querySelector('[data-result]')
result.textContent = result.dataset.initial || ''
phase('ready', 1)
status.textContent = 'Motion demo only · click “Watch activation” to begin.'
}
const cancel = () => {
clearTimeout(timer)
seed.hidden = true
palette.hidden = true
play.setAttribute('aria-expanded', 'false')
phase('ready', 1)
status.textContent = 'Dismissed. The message was not changed.'
play.focus()
}
play.addEventListener('click', () => {
reset()
phase('gesture', 1)
status.textContent = lab.dataset.action
timer = setTimeout(() => {
if (lab.dataset.bud === 'true') {
seed.hidden = false
phase('bud', 2)
status.textContent = 'The emoji bud is here. Click it to unfold favorites.'
seed.focus()
} else open()
}, 1050)
})
seed.addEventListener('click', open)
lab.querySelector('[data-reset]').addEventListener('click', reset)
lab.querySelector('[data-dismiss]').addEventListener('click', cancel)
lab.querySelector('[data-edge]')?.addEventListener('click', open)
lab.querySelectorAll('[data-pick]').forEach(button => button.addEventListener('click', () => {
lab.querySelector('[data-result]').replaceChildren(button.querySelector('img').cloneNode())
seed.hidden = true
palette.hidden = true
play.setAttribute('aria-expanded', 'false')
phase('done', 3)
status.textContent = 'Inserted here. In the proposed Mac flow, you keep typing in your app.'
play.focus()
}))
lab.addEventListener('keydown', event => {
if (event.key === 'Escape') { event.stopPropagation(); cancel() }
})
reset()
})
CONCEPT 1: Type a colon Type :th after your message Native insertion must replace only the verified shortcode. This browser storyboard simulates the input using Watch activation; it does not detect a system-wide gesture or insert into another app. No claim of an unprecedented invention or native feasibility is made. Native sequence: capture verified editable target → recognize explicit activation → reveal bud or palette without modifying text → explicit emoji selection → revalidate target → insert, with one undo step. If no target is available, offer Copy explicitly. Cancel never changes text. Respect existing app controls, secure input, display bounds, and the user's opt-in setting. For buds: reveal beside the active pointer/caret, retain a generous travel corridor between pointer and bud, dismiss on continued typing, focus change, Escape, or movement away. Never insert on hover. Test the reveal delay and false positives with real users. Pocket mode is entered only via Carry in EmojiCopy. The carried emoji remains a proposal until a user confirms insertion. Clear after one successful insertion by default. Integration: mount this HTML once per demo, include the complete CSS and JS below. Source IDs must remain unique. Native implementation is a separate prototype. The browser mock uses PNGs; actual insertion must use the selected Unicode sequence.
TriggerObserver + ColonRowPanel · new — the full spec is cards A to G below, this card is the summary.THE ACTION Type a colon, then at least two letters, in a single word.
WHERE Any text field in any app, without leaving it.
HANDS Both on the keyboard. Nothing to reach for.
LEARN CURVE Nothing new IF the user already types :tada: in Slack.
A habit to build if they do not.
FIRES ONLY WHEN the character before the colon is a space, a line start,
or an opening bracket, AND two letters are typed, AND at
least one emoji matches.
NEVER FIRES ON 3:30 · https:// · key: value · [label]: url · ::before
:) and :-D · anything inside Terminal, Xcode or a password field
COST The heaviest of the four. It needs a keystroke observer and
the text caret position, and the caret is unavailable in most
Chromium and Electron apps, so the row simply does not draw
there. See card G.
COMPETES WITH Slack and Discord, which already do this inside themselves.
We ship OFF in both.
<div class="trigger-lab" data-kind="2" data-motion="keys" data-bud="false" data-action="Select a word, then press your emoji shortcut"> <div class="trigger-lab__layout"><div class="trigger-lab__desktop"> <div class="trigger-lab__bar"><b>Messages</b><span>EmojiCopy <span class="jp-font"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f604.png" alt="Menu bar icon"></span></span></div> <div class="trigger-lab__canvas"><div class="trigger-lab__window"><div class="trigger-lab__title"><span class="trigger-lab__lights"><i></i><i></i><i></i></span> Team chat</div><div class="trigger-lab__conversation"><b>Casey</b><div class="trigger-lab__chat">The new design is ready!</div></div><div class="trigger-lab__composer">Want to grab <span class="trigger-lab__result jp-font" data-result data-initial="pizza">pizza</span><span class="trigger-lab__caret"></span></div></div> <div class="trigger-lab__origin"><div class="trigger-lab__keys"><kbd>pizza</kbd><kbd>⌘</kbd><kbd>⌥</kbd><kbd>Space</kbd></div><svg class="trigger-lab__pointer" viewBox="0 0 26 32" aria-hidden="true"><path d="M3 2L23 20L14 21L10 30Z" fill="var(--metal-950)" stroke="var(--white)" stroke-width="2"/></svg></div> <button class="trigger-lab__seed jp-font" data-seed hidden aria-label="Open emoji favorites"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f355.png" alt="Emoji bud"></button> <div class="trigger-lab__palette" id="lab-palette-2" data-palette hidden aria-label="Emoji favorites"><div class="trigger-lab__palette-head"><b>Results for pizza</b><button class="trigger-lab__dismiss" data-dismiss aria-label="Close emoji favorites">Esc ×</button></div><div class="trigger-lab__choices"><button class="trigger-lab__choice jp-font" data-pick aria-label="Insert pizza"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f355.png" alt="Pizza"></button><button class="trigger-lab__choice jp-font" data-pick aria-label="Insert cheese"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f9c0.png" alt="Cheese"></button><button class="trigger-lab__choice jp-font" data-pick aria-label="Insert tomato"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f345.png" alt="Tomato"></button><button class="trigger-lab__choice jp-font" data-pick aria-label="Insert plate"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f37d.png" alt="Plate"></button><button class="trigger-lab__choice jp-font" data-pick aria-label="Insert fork and knife"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f374.png" alt="Fork and knife"></button></div></div> </div></div><div class="trigger-lab__steps"><div class="trigger-lab__step" data-step="1"><strong>1</strong><div><b>Select “pizza”</b><span>Highlight the word in your message.</span></div></div><div class="trigger-lab__step" data-step="2"><strong>2</strong><div><b>Press your emoji key</b><span>Open results already searching pizza.</span></div></div><div class="trigger-lab__step" data-step="3"><strong>3</strong><div><b>Choose a result</b><span>Replace the selected word.</span></div></div></div></div> <div class="trigger-lab__controls"><button class="trigger-lab__play" data-play aria-controls="lab-palette-2" aria-expanded="false">Watch activation</button><button class="trigger-lab__reset" data-reset>Reset</button><span class="trigger-lab__status" role="status"></span></div> <div class="trigger-lab__tradeoff"><span><b>Why it could work</b><br>You already supplied the search term.</span><span><b>What to test</b><br>Only replace a selection that the host app can verify.</span></div></div>
/* Trigger storyboards: desktop mockups, not native overlays. */
.trigger-lab { display: flex; flex-direction: column; gap: var(--sp-5); font-size: 1.5rem; color: var(--c-text-primary); }
.trigger-lab__layout { display: grid; grid-template-columns: minmax(0, 1fr) 230px; gap: var(--sp-6); align-items: center; }
.trigger-lab__desktop { position: relative; min-width: 0; border-radius: var(--r-lg); overflow: hidden; background: var(--metal-200); border: 1px solid var(--c-border-primary); }
.trigger-lab__bar { display: flex; align-items: center; justify-content: space-between; padding: var(--sp-3) var(--sp-4); background: var(--metal-100); color: var(--metal-700); font-size: 1.2rem; }
.trigger-lab__bar b { color: var(--metal-900); }
.trigger-lab__canvas { position: relative; height: 355px; padding: var(--sp-5); background: linear-gradient(140deg, var(--metal-100), var(--green-400)); }
.trigger-lab__window { position: absolute; inset: 22px 22px 30px; background: var(--white); border: 1px solid var(--metal-200); border-radius: var(--r-md); overflow: hidden; box-shadow: 0 12px 30px color-mix(in srgb, var(--metal-900) 12%, transparent); }
.trigger-lab__title { display: flex; align-items: center; gap: var(--sp-2); padding: var(--sp-3) var(--sp-4); border-bottom: 1px solid var(--metal-100); color: var(--metal-600); font-size: 1.3rem; }
.trigger-lab__lights { display: flex; gap: var(--sp-1); }
.trigger-lab__lights i { width: 8px; height: 8px; border-radius: var(--r-pill); background: var(--metal-300); }
.trigger-lab__conversation { display: flex; flex-direction: column; align-items: flex-start; gap: var(--sp-2); padding: var(--sp-5); color: var(--metal-600); }
.trigger-lab__conversation b { font-size: 1.2rem; }
.trigger-lab__chat { background: var(--metal-100); padding: var(--sp-3) var(--sp-4); border-radius: var(--r-md); color: var(--metal-900); font-size: 1.4rem; }
.trigger-lab__composer { position: absolute; bottom: 28px; left: 18px; right: 18px; border: 2px solid var(--green-700); border-radius: var(--r-md); padding: var(--sp-4); color: var(--metal-900); display: flex; align-items: center; gap: var(--sp-1); min-height: 26px; font-size: 1.6rem; }
.trigger-lab__caret { height: 22px; width: 2px; background: var(--green-800); }
.trigger-lab__result img { width: 25px; height: 25px; vertical-align: middle; }
.trigger-lab__origin { position: absolute; right: 21%; bottom: 105px; width: 160px; height: 120px; pointer-events: none; }
.trigger-lab__pointer { position: absolute; bottom: 0; right: 30px; width: 26px; height: 32px; filter: drop-shadow(0 2px 2px color-mix(in srgb, var(--metal-900) 25%, transparent)); }
.trigger-lab__trail { position: absolute; bottom: 0; right: 5px; width: 88px; height: 88px; color: var(--green-800); opacity: .6; }
.trigger-lab__keys { position: absolute; bottom: 62px; right: 6px; display: flex; gap: var(--sp-1); white-space: nowrap; }
.trigger-lab__keys kbd { display: grid; place-items: center; min-width: 35px; height: 33px; padding: 0 var(--sp-2); font: 600 1.6rem var(--ff-inter), sans-serif; color: var(--metal-900); background: var(--white); border: 1px solid var(--metal-300); border-radius: var(--r-sm); box-shadow: 0 3px 0 var(--metal-200); }
.trigger-lab__seed { position: absolute; right: 23%; bottom: 152px; width: 54px; height: 54px; display: grid; place-items: center; border: 1px solid var(--green-600); border-radius: var(--r-pill); background: var(--white); box-shadow: 0 0 0 6px color-mix(in srgb, var(--green-500) 22%, transparent); cursor: pointer; z-index: 3; }
.trigger-lab__seed img { width: 34px; height: 34px; }
.trigger-lab__palette { position: absolute; z-index: 4; right: 8%; bottom: 145px; width: min(285px, 83%); padding: var(--sp-3); box-sizing: border-box; border-radius: var(--r-lg); background: var(--metal-900); color: var(--white); box-shadow: 0 12px 26px color-mix(in srgb, var(--metal-900) 25%, transparent); }
.trigger-lab__palette::after { content: ''; position: absolute; bottom: -7px; right: 52px; width: 16px; height: 16px; transform: rotate(45deg); background: var(--metal-900); }
.trigger-lab__palette-head { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2); font-size: 1.2rem; padding-bottom: var(--sp-2); }
.trigger-lab__choices { display: flex; justify-content: space-between; gap: var(--sp-1); }
.trigger-lab__choice { display: grid; place-items: center; cursor: pointer; border: 0; border-radius: var(--r-sm); background: var(--metal-800); width: 44px; height: 44px; }
.trigger-lab__choice:hover { background: var(--green-700); }
.trigger-lab__choice img { width: 29px; height: 29px; }
.trigger-lab__dismiss { border: 0; background: transparent; color: var(--metal-300); cursor: pointer; font: inherit; padding: var(--sp-1); }
.trigger-lab__edge { position: absolute; right: 0; bottom: 136px; background: var(--metal-900); color: var(--white); border: 0; border-radius: var(--r-md) 0 0 var(--r-md); padding: var(--sp-3); cursor: pointer; }
.trigger-lab__edge img { width: 24px; height: 24px; }
.trigger-lab__steps { display: flex; flex-direction: column; gap: var(--sp-5); }
.trigger-lab__step { display: grid; grid-template-columns: 28px 1fr; gap: var(--sp-2); align-items: start; line-height: 1.5; }
.trigger-lab__step strong { display: grid; place-items: center; height: 26px; border-radius: var(--r-pill); background: var(--c-surface-tertiary); font-size: 1.2rem; }
.trigger-lab__step b { display: block; }
.trigger-lab__step span { font-size: 1.35rem; color: var(--c-text-secondary); }
.trigger-lab__step.is-current strong { background: var(--green-500); color: var(--green-950); }
.trigger-lab__controls { display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap; }
.trigger-lab__play, .trigger-lab__reset { border: 1px solid var(--c-border-primary); border-radius: var(--r-pill); padding: var(--sp-3) var(--sp-5); font: 600 1.4rem var(--ff-inter), sans-serif; cursor: pointer; }
.trigger-lab__play { color: var(--green-950); background: var(--green-500); }
.trigger-lab__reset { color: var(--c-text-secondary); background: var(--c-surface-primary); }
.trigger-lab__status { font-size: 1.3rem; color: var(--c-text-secondary); flex: 1; min-width: 180px; }
.trigger-lab__tradeoff { display: flex; gap: var(--sp-4); font-size: 1.35rem; line-height: 1.6; color: var(--c-text-secondary); border-top: 1px solid var(--c-border-primary); padding-top: var(--sp-4); }
.trigger-lab [hidden] { display: none !important; }
.trigger-lab button:focus-visible { outline: 3px solid var(--green-700); outline-offset: 3px; }
.trigger-lab[data-phase='gesture'] .trigger-lab__pointer { animation: trigger-orbit 1s ease-in-out; }
.trigger-lab[data-phase='gesture'] .trigger-lab__keys { animation: trigger-press .5s ease-in-out 2; }
.trigger-lab[data-phase='gesture'][data-motion='dwell'] .trigger-lab__pointer { animation: trigger-approach 1s ease-out; }
.trigger-lab[data-phase='gesture'][data-motion='carry'] .trigger-lab__pointer { animation: trigger-carry 1s ease-in-out; }
.trigger-lab[data-phase='done'] .trigger-lab__origin { opacity: .2; }
@keyframes trigger-orbit { 0%,100% { transform: translate(0,0); } 25% { transform: translate(-48px,0); } 50% { transform: translate(-48px,-48px); } 75% { transform: translate(0,-48px); } }
@keyframes trigger-press { 50% { transform: translateY(4px); } }
@keyframes trigger-approach { from { transform: translate(80px,-70px); } to { transform: translate(0,0); } }
@keyframes trigger-carry { from { transform: translate(-150px,-100px); } to { transform: translate(0,0); } }
@media (prefers-reduced-motion: reduce) { .trigger-lab *, .trigger-lab *::after { animation: none !important; } }
@media (max-width: 760px) { .trigger-lab__layout { grid-template-columns: 1fr; } .trigger-lab__steps { flex-direction: row; flex-wrap: wrap; } .trigger-lab__step { flex: 1; min-width: 145px; } .trigger-lab__tradeoff { flex-direction: column; } }
@media (max-width: 450px) { .trigger-lab__window { inset: 15px 8px; } .trigger-lab__composer { left: 10px; right: 10px; padding: var(--sp-3); font-size: 1.4rem; } .trigger-lab__origin { right: 10%; } }
.trigger-intro { font-size: 1.5rem; line-height: 1.6; color: var(--c-text-secondary); margin-bottom: var(--sp-5); }
.trigger-lab__bar img { width: 15px; height: 15px; vertical-align: middle; }
.trigger-lab__keys img { width: 34px; height: 34px; }
.trigger-lab[data-motion='keys'] .trigger-lab__pointer { display: none; }
.trigger-lab[data-kind='4'] .trigger-lab__origin { top: 4px; right: 0; bottom: auto; }
.trigger-lab[data-kind='4'] .trigger-lab__palette { width: min(340px, 90%); right: 5%; bottom: 100px; }
.trigger-lab[data-kind='4'] .trigger-lab__choices { display: grid; grid-template-columns: repeat(5, 1fr); gap: var(--sp-2); }
.trigger-lab[data-kind='8'] .trigger-lab__palette { right: 42px; bottom: 132px; }
.trigger-lab[data-kind='10'] .trigger-lab__origin { right: auto; left: 155px; bottom: 87px; }
.trigger-lab[data-kind='10'] .trigger-lab__seed { right: auto; left: 210px; bottom: 145px; }
.trigger-lab[data-kind='10'] .trigger-lab__palette { left: 72px; right: auto; }
.trigger-lab[data-kind='11'][data-phase='gesture'] .trigger-lab__keys { animation: trigger-carry 1s ease-in-out; }
.trigger-lab__selected { background: var(--green-400); }
.trigger-lab[data-kind='10'] .trigger-lab__origin { left: 88px; }
.trigger-lab[data-kind='10'] .trigger-lab__seed { left: 164px; }
.trigger-lab[data-kind='11'][data-phase='bud'] .trigger-lab__keys,
.trigger-lab[data-kind='11'][data-phase='open'] .trigger-lab__keys { visibility: hidden; }
.trigger-lab[data-kind='4'][data-phase='gesture'] .trigger-lab__pointer { animation: trigger-approach 1s ease-out; }
.trigger-lab[data-kind='4'][data-phase='gesture'] .trigger-lab__keys { outline: 2px solid var(--green-700); outline-offset: 5px; border-radius: var(--r-pill); }
// Isolated storyboards. Buttons simulate native activation; no global input is captured.
document.querySelectorAll('.trigger-lab').forEach(lab => {
const seed = lab.querySelector('[data-seed]')
const palette = lab.querySelector('[data-palette]')
const play = lab.querySelector('[data-play]')
const status = lab.querySelector('[role="status"]')
let timer
const phase = (name, step) => {
lab.dataset.phase = name
lab.querySelectorAll('[data-step]').forEach(item => item.classList.toggle('is-current', Number(item.dataset.step) === step))
}
const open = () => {
clearTimeout(timer)
seed.hidden = true
palette.hidden = false
play.setAttribute('aria-expanded', 'true')
phase('open', 2)
status.textContent = 'Click an emoji to insert it in this example. Escape cancels.'
palette.querySelector('[data-pick]').focus()
}
const reset = () => {
clearTimeout(timer)
seed.hidden = true
palette.hidden = true
play.setAttribute('aria-expanded', 'false')
const result = lab.querySelector('[data-result]')
result.textContent = result.dataset.initial || ''
phase('ready', 1)
status.textContent = 'Motion demo only · click “Watch activation” to begin.'
}
const cancel = () => {
clearTimeout(timer)
seed.hidden = true
palette.hidden = true
play.setAttribute('aria-expanded', 'false')
phase('ready', 1)
status.textContent = 'Dismissed. The message was not changed.'
play.focus()
}
play.addEventListener('click', () => {
reset()
phase('gesture', 1)
status.textContent = lab.dataset.action
timer = setTimeout(() => {
if (lab.dataset.bud === 'true') {
seed.hidden = false
phase('bud', 2)
status.textContent = 'The emoji bud is here. Click it to unfold favorites.'
seed.focus()
} else open()
}, 1050)
})
seed.addEventListener('click', open)
lab.querySelector('[data-reset]').addEventListener('click', reset)
lab.querySelector('[data-dismiss]').addEventListener('click', cancel)
lab.querySelector('[data-edge]')?.addEventListener('click', open)
lab.querySelectorAll('[data-pick]').forEach(button => button.addEventListener('click', () => {
lab.querySelector('[data-result]').replaceChildren(button.querySelector('img').cloneNode())
seed.hidden = true
palette.hidden = true
play.setAttribute('aria-expanded', 'false')
phase('done', 3)
status.textContent = 'Inserted here. In the proposed Mac flow, you keep typing in your app.'
play.focus()
}))
lab.addEventListener('keydown', event => {
if (event.key === 'Escape') { event.stopPropagation(); cancel() }
})
reset()
})
CONCEPT 2: Highlight first Select a word, then press your emoji shortcut Only replace a selection that the host app can verify. This browser storyboard simulates the input using Watch activation; it does not detect a system-wide gesture or insert into another app. No claim of an unprecedented invention or native feasibility is made. Native sequence: capture verified editable target → recognize explicit activation → reveal bud or palette without modifying text → explicit emoji selection → revalidate target → insert, with one undo step. If no target is available, offer Copy explicitly. Cancel never changes text. Respect existing app controls, secure input, display bounds, and the user's opt-in setting. For buds: reveal beside the active pointer/caret, retain a generous travel corridor between pointer and bud, dismiss on continued typing, focus change, Escape, or movement away. Never insert on hover. Test the reveal delay and false positives with real users. Pocket mode is entered only via Carry in EmojiCopy. The carried emoji remains a proposal until a user confirms insertion. Clear after one successful insertion by default. Integration: mount this HTML once per demo, include the complete CSS and JS below. Source IDs must remain unique. Native implementation is a separate prototype. The browser mock uses PNGs; actual insertion must use the selected Unicode sequence.
open_mini_window_at_cursor__handler · src-tauri/src/window.rs — the existing open path. This adds one Accessibility read before it.SelectionSeed · new (Rust) — reads kAXSelectedTextAttribute and passes it to the webview as the initial query.THE ACTION Highlight a word, press the emoji key you already use.
WHERE Any app that publishes its selection to Accessibility.
HANDS Keyboard, or trackpad then keyboard.
LEARN CURVE NOTHING NEW. It is the same shortcut, behaving smarter. A user
who never reads onboarding still gets exactly today's behavior.
FIRES WHEN A selection exists at the moment the shortcut is pressed, and
it is 1 to 3 words, under 32 characters, letters only, no line
breaks.
FALLS BACK No selection, an unreadable app, or a paragraph selected?
The panel opens plain and empty, exactly as it does today.
Nothing is lost and nothing is announced.
NEVER READS Secure fields. Check kAXSubroleAttribute before the read.
COST Cheapest real intelligence on this page. One AX read on a path
that already exists. Cap it with a short messaging timeout so a
hung app cannot stall the open.
// Runs immediately before the existing open, on the same handler.
fn selected_word() -> Option<String> {
let system = AXUIElementCreateSystemWide();
AXUIElementSetMessagingTimeout(system, 0.15); // never stall the open
let focused = copy_attr(system, kAXFocusedUIElementAttribute)?;
// Never read a password field.
if copy_attr(focused, kAXSubroleAttribute) == Some(kAXSecureTextFieldSubrole) {
return None
}
let text: String = copy_attr(focused, kAXSelectedTextAttribute)?;
// Guards. A leftover selection from earlier is the only failure mode,
// and it costs the user exactly one keystroke to clear.
let words = text.split_whitespace().count();
if words == 0 || words > 3 { return None }
if text.chars().count() > 32 { return None }
if text.contains('\\n') { return None }
if !text.chars().all(|c| c.is_alphabetic() || c.is_whitespace()) { return None }
Some(text)
}
<div class="trigger-lab" data-kind="3" data-motion="keys" data-bud="false" data-action="Squeeze both Shift keys together"> <div class="trigger-lab__layout"><div class="trigger-lab__desktop"> <div class="trigger-lab__bar"><b>Messages</b><span>EmojiCopy <span class="jp-font"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f604.png" alt="Menu bar icon"></span></span></div> <div class="trigger-lab__canvas"><div class="trigger-lab__window"><div class="trigger-lab__title"><span class="trigger-lab__lights"><i></i><i></i><i></i></span> Team chat</div><div class="trigger-lab__conversation"><b>Casey</b><div class="trigger-lab__chat">The new design is ready!</div></div><div class="trigger-lab__composer">That looks great <span class="trigger-lab__result jp-font" data-result></span><span class="trigger-lab__caret"></span></div></div> <div class="trigger-lab__origin"><div class="trigger-lab__keys"><kbd>⇧</kbd><kbd>+</kbd><kbd>⇧</kbd></div><svg class="trigger-lab__pointer" viewBox="0 0 26 32" aria-hidden="true"><path d="M3 2L23 20L14 21L10 30Z" fill="var(--metal-950)" stroke="var(--white)" stroke-width="2"/></svg></div> <button class="trigger-lab__seed jp-font" data-seed hidden aria-label="Open emoji favorites"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f44d.png" alt="Emoji bud"></button> <div class="trigger-lab__palette" id="lab-palette-3" data-palette hidden aria-label="Emoji favorites"><div class="trigger-lab__palette-head"><b>EmojiCopy · Favorites</b><button class="trigger-lab__dismiss" data-dismiss aria-label="Close emoji favorites">Esc ×</button></div><div class="trigger-lab__choices"><button class="trigger-lab__choice jp-font" data-pick aria-label="Insert thumbs up"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f44d.png" alt="Thumbs up"></button><button class="trigger-lab__choice jp-font" data-pick aria-label="Insert party popper"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f389.png" alt="Party popper"></button><button class="trigger-lab__choice jp-font" data-pick aria-label="Insert heart"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/2764.png" alt="Heart"></button><button class="trigger-lab__choice jp-font" data-pick aria-label="Insert smile"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f604.png" alt="Smile"></button><button class="trigger-lab__choice jp-font" data-pick aria-label="Insert fire"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f525.png" alt="Fire"></button></div></div> </div></div><div class="trigger-lab__steps"><div class="trigger-lab__step" data-step="1"><strong>1</strong><div><b>Squeeze both Shifts</b><span>One key beneath each pinky.</span></div></div><div class="trigger-lab__step" data-step="2"><strong>2</strong><div><b>Picker appears</b><span>The keyboard stays within reach.</span></div></div><div class="trigger-lab__step" data-step="3"><strong>3</strong><div><b>Pick an emoji</b><span>Return to the same text field.</span></div></div></div></div> <div class="trigger-lab__controls"><button class="trigger-lab__play" data-play aria-controls="lab-palette-3" aria-expanded="false">Watch activation</button><button class="trigger-lab__reset" data-reset>Reset</button><span class="trigger-lab__status" role="status"></span></div> <div class="trigger-lab__tradeoff"><span><b>Why it could work</b><br>A physical gesture with no letter to remember.</span><span><b>What to test</b><br>Test false activations and modifier accessibility settings.</span></div></div>
/* Trigger storyboards: desktop mockups, not native overlays. */
.trigger-lab { display: flex; flex-direction: column; gap: var(--sp-5); font-size: 1.5rem; color: var(--c-text-primary); }
.trigger-lab__layout { display: grid; grid-template-columns: minmax(0, 1fr) 230px; gap: var(--sp-6); align-items: center; }
.trigger-lab__desktop { position: relative; min-width: 0; border-radius: var(--r-lg); overflow: hidden; background: var(--metal-200); border: 1px solid var(--c-border-primary); }
.trigger-lab__bar { display: flex; align-items: center; justify-content: space-between; padding: var(--sp-3) var(--sp-4); background: var(--metal-100); color: var(--metal-700); font-size: 1.2rem; }
.trigger-lab__bar b { color: var(--metal-900); }
.trigger-lab__canvas { position: relative; height: 355px; padding: var(--sp-5); background: linear-gradient(140deg, var(--metal-100), var(--green-400)); }
.trigger-lab__window { position: absolute; inset: 22px 22px 30px; background: var(--white); border: 1px solid var(--metal-200); border-radius: var(--r-md); overflow: hidden; box-shadow: 0 12px 30px color-mix(in srgb, var(--metal-900) 12%, transparent); }
.trigger-lab__title { display: flex; align-items: center; gap: var(--sp-2); padding: var(--sp-3) var(--sp-4); border-bottom: 1px solid var(--metal-100); color: var(--metal-600); font-size: 1.3rem; }
.trigger-lab__lights { display: flex; gap: var(--sp-1); }
.trigger-lab__lights i { width: 8px; height: 8px; border-radius: var(--r-pill); background: var(--metal-300); }
.trigger-lab__conversation { display: flex; flex-direction: column; align-items: flex-start; gap: var(--sp-2); padding: var(--sp-5); color: var(--metal-600); }
.trigger-lab__conversation b { font-size: 1.2rem; }
.trigger-lab__chat { background: var(--metal-100); padding: var(--sp-3) var(--sp-4); border-radius: var(--r-md); color: var(--metal-900); font-size: 1.4rem; }
.trigger-lab__composer { position: absolute; bottom: 28px; left: 18px; right: 18px; border: 2px solid var(--green-700); border-radius: var(--r-md); padding: var(--sp-4); color: var(--metal-900); display: flex; align-items: center; gap: var(--sp-1); min-height: 26px; font-size: 1.6rem; }
.trigger-lab__caret { height: 22px; width: 2px; background: var(--green-800); }
.trigger-lab__result img { width: 25px; height: 25px; vertical-align: middle; }
.trigger-lab__origin { position: absolute; right: 21%; bottom: 105px; width: 160px; height: 120px; pointer-events: none; }
.trigger-lab__pointer { position: absolute; bottom: 0; right: 30px; width: 26px; height: 32px; filter: drop-shadow(0 2px 2px color-mix(in srgb, var(--metal-900) 25%, transparent)); }
.trigger-lab__trail { position: absolute; bottom: 0; right: 5px; width: 88px; height: 88px; color: var(--green-800); opacity: .6; }
.trigger-lab__keys { position: absolute; bottom: 62px; right: 6px; display: flex; gap: var(--sp-1); white-space: nowrap; }
.trigger-lab__keys kbd { display: grid; place-items: center; min-width: 35px; height: 33px; padding: 0 var(--sp-2); font: 600 1.6rem var(--ff-inter), sans-serif; color: var(--metal-900); background: var(--white); border: 1px solid var(--metal-300); border-radius: var(--r-sm); box-shadow: 0 3px 0 var(--metal-200); }
.trigger-lab__seed { position: absolute; right: 23%; bottom: 152px; width: 54px; height: 54px; display: grid; place-items: center; border: 1px solid var(--green-600); border-radius: var(--r-pill); background: var(--white); box-shadow: 0 0 0 6px color-mix(in srgb, var(--green-500) 22%, transparent); cursor: pointer; z-index: 3; }
.trigger-lab__seed img { width: 34px; height: 34px; }
.trigger-lab__palette { position: absolute; z-index: 4; right: 8%; bottom: 145px; width: min(285px, 83%); padding: var(--sp-3); box-sizing: border-box; border-radius: var(--r-lg); background: var(--metal-900); color: var(--white); box-shadow: 0 12px 26px color-mix(in srgb, var(--metal-900) 25%, transparent); }
.trigger-lab__palette::after { content: ''; position: absolute; bottom: -7px; right: 52px; width: 16px; height: 16px; transform: rotate(45deg); background: var(--metal-900); }
.trigger-lab__palette-head { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2); font-size: 1.2rem; padding-bottom: var(--sp-2); }
.trigger-lab__choices { display: flex; justify-content: space-between; gap: var(--sp-1); }
.trigger-lab__choice { display: grid; place-items: center; cursor: pointer; border: 0; border-radius: var(--r-sm); background: var(--metal-800); width: 44px; height: 44px; }
.trigger-lab__choice:hover { background: var(--green-700); }
.trigger-lab__choice img { width: 29px; height: 29px; }
.trigger-lab__dismiss { border: 0; background: transparent; color: var(--metal-300); cursor: pointer; font: inherit; padding: var(--sp-1); }
.trigger-lab__edge { position: absolute; right: 0; bottom: 136px; background: var(--metal-900); color: var(--white); border: 0; border-radius: var(--r-md) 0 0 var(--r-md); padding: var(--sp-3); cursor: pointer; }
.trigger-lab__edge img { width: 24px; height: 24px; }
.trigger-lab__steps { display: flex; flex-direction: column; gap: var(--sp-5); }
.trigger-lab__step { display: grid; grid-template-columns: 28px 1fr; gap: var(--sp-2); align-items: start; line-height: 1.5; }
.trigger-lab__step strong { display: grid; place-items: center; height: 26px; border-radius: var(--r-pill); background: var(--c-surface-tertiary); font-size: 1.2rem; }
.trigger-lab__step b { display: block; }
.trigger-lab__step span { font-size: 1.35rem; color: var(--c-text-secondary); }
.trigger-lab__step.is-current strong { background: var(--green-500); color: var(--green-950); }
.trigger-lab__controls { display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap; }
.trigger-lab__play, .trigger-lab__reset { border: 1px solid var(--c-border-primary); border-radius: var(--r-pill); padding: var(--sp-3) var(--sp-5); font: 600 1.4rem var(--ff-inter), sans-serif; cursor: pointer; }
.trigger-lab__play { color: var(--green-950); background: var(--green-500); }
.trigger-lab__reset { color: var(--c-text-secondary); background: var(--c-surface-primary); }
.trigger-lab__status { font-size: 1.3rem; color: var(--c-text-secondary); flex: 1; min-width: 180px; }
.trigger-lab__tradeoff { display: flex; gap: var(--sp-4); font-size: 1.35rem; line-height: 1.6; color: var(--c-text-secondary); border-top: 1px solid var(--c-border-primary); padding-top: var(--sp-4); }
.trigger-lab [hidden] { display: none !important; }
.trigger-lab button:focus-visible { outline: 3px solid var(--green-700); outline-offset: 3px; }
.trigger-lab[data-phase='gesture'] .trigger-lab__pointer { animation: trigger-orbit 1s ease-in-out; }
.trigger-lab[data-phase='gesture'] .trigger-lab__keys { animation: trigger-press .5s ease-in-out 2; }
.trigger-lab[data-phase='gesture'][data-motion='dwell'] .trigger-lab__pointer { animation: trigger-approach 1s ease-out; }
.trigger-lab[data-phase='gesture'][data-motion='carry'] .trigger-lab__pointer { animation: trigger-carry 1s ease-in-out; }
.trigger-lab[data-phase='done'] .trigger-lab__origin { opacity: .2; }
@keyframes trigger-orbit { 0%,100% { transform: translate(0,0); } 25% { transform: translate(-48px,0); } 50% { transform: translate(-48px,-48px); } 75% { transform: translate(0,-48px); } }
@keyframes trigger-press { 50% { transform: translateY(4px); } }
@keyframes trigger-approach { from { transform: translate(80px,-70px); } to { transform: translate(0,0); } }
@keyframes trigger-carry { from { transform: translate(-150px,-100px); } to { transform: translate(0,0); } }
@media (prefers-reduced-motion: reduce) { .trigger-lab *, .trigger-lab *::after { animation: none !important; } }
@media (max-width: 760px) { .trigger-lab__layout { grid-template-columns: 1fr; } .trigger-lab__steps { flex-direction: row; flex-wrap: wrap; } .trigger-lab__step { flex: 1; min-width: 145px; } .trigger-lab__tradeoff { flex-direction: column; } }
@media (max-width: 450px) { .trigger-lab__window { inset: 15px 8px; } .trigger-lab__composer { left: 10px; right: 10px; padding: var(--sp-3); font-size: 1.4rem; } .trigger-lab__origin { right: 10%; } }
.trigger-intro { font-size: 1.5rem; line-height: 1.6; color: var(--c-text-secondary); margin-bottom: var(--sp-5); }
.trigger-lab__bar img { width: 15px; height: 15px; vertical-align: middle; }
.trigger-lab__keys img { width: 34px; height: 34px; }
.trigger-lab[data-motion='keys'] .trigger-lab__pointer { display: none; }
.trigger-lab[data-kind='4'] .trigger-lab__origin { top: 4px; right: 0; bottom: auto; }
.trigger-lab[data-kind='4'] .trigger-lab__palette { width: min(340px, 90%); right: 5%; bottom: 100px; }
.trigger-lab[data-kind='4'] .trigger-lab__choices { display: grid; grid-template-columns: repeat(5, 1fr); gap: var(--sp-2); }
.trigger-lab[data-kind='8'] .trigger-lab__palette { right: 42px; bottom: 132px; }
.trigger-lab[data-kind='10'] .trigger-lab__origin { right: auto; left: 155px; bottom: 87px; }
.trigger-lab[data-kind='10'] .trigger-lab__seed { right: auto; left: 210px; bottom: 145px; }
.trigger-lab[data-kind='10'] .trigger-lab__palette { left: 72px; right: auto; }
.trigger-lab[data-kind='11'][data-phase='gesture'] .trigger-lab__keys { animation: trigger-carry 1s ease-in-out; }
.trigger-lab__selected { background: var(--green-400); }
.trigger-lab[data-kind='10'] .trigger-lab__origin { left: 88px; }
.trigger-lab[data-kind='10'] .trigger-lab__seed { left: 164px; }
.trigger-lab[data-kind='11'][data-phase='bud'] .trigger-lab__keys,
.trigger-lab[data-kind='11'][data-phase='open'] .trigger-lab__keys { visibility: hidden; }
.trigger-lab[data-kind='4'][data-phase='gesture'] .trigger-lab__pointer { animation: trigger-approach 1s ease-out; }
.trigger-lab[data-kind='4'][data-phase='gesture'] .trigger-lab__keys { outline: 2px solid var(--green-700); outline-offset: 5px; border-radius: var(--r-pill); }
// Isolated storyboards. Buttons simulate native activation; no global input is captured.
document.querySelectorAll('.trigger-lab').forEach(lab => {
const seed = lab.querySelector('[data-seed]')
const palette = lab.querySelector('[data-palette]')
const play = lab.querySelector('[data-play]')
const status = lab.querySelector('[role="status"]')
let timer
const phase = (name, step) => {
lab.dataset.phase = name
lab.querySelectorAll('[data-step]').forEach(item => item.classList.toggle('is-current', Number(item.dataset.step) === step))
}
const open = () => {
clearTimeout(timer)
seed.hidden = true
palette.hidden = false
play.setAttribute('aria-expanded', 'true')
phase('open', 2)
status.textContent = 'Click an emoji to insert it in this example. Escape cancels.'
palette.querySelector('[data-pick]').focus()
}
const reset = () => {
clearTimeout(timer)
seed.hidden = true
palette.hidden = true
play.setAttribute('aria-expanded', 'false')
const result = lab.querySelector('[data-result]')
result.textContent = result.dataset.initial || ''
phase('ready', 1)
status.textContent = 'Motion demo only · click “Watch activation” to begin.'
}
const cancel = () => {
clearTimeout(timer)
seed.hidden = true
palette.hidden = true
play.setAttribute('aria-expanded', 'false')
phase('ready', 1)
status.textContent = 'Dismissed. The message was not changed.'
play.focus()
}
play.addEventListener('click', () => {
reset()
phase('gesture', 1)
status.textContent = lab.dataset.action
timer = setTimeout(() => {
if (lab.dataset.bud === 'true') {
seed.hidden = false
phase('bud', 2)
status.textContent = 'The emoji bud is here. Click it to unfold favorites.'
seed.focus()
} else open()
}, 1050)
})
seed.addEventListener('click', open)
lab.querySelector('[data-reset]').addEventListener('click', reset)
lab.querySelector('[data-dismiss]').addEventListener('click', cancel)
lab.querySelector('[data-edge]')?.addEventListener('click', open)
lab.querySelectorAll('[data-pick]').forEach(button => button.addEventListener('click', () => {
lab.querySelector('[data-result]').replaceChildren(button.querySelector('img').cloneNode())
seed.hidden = true
palette.hidden = true
play.setAttribute('aria-expanded', 'false')
phase('done', 3)
status.textContent = 'Inserted here. In the proposed Mac flow, you keep typing in your app.'
play.focus()
}))
lab.addEventListener('keydown', event => {
if (event.key === 'Escape') { event.stopPropagation(); cancel() }
})
reset()
})
CONCEPT 3: Both Shifts Squeeze both Shift keys together Test false activations and modifier accessibility settings. This browser storyboard simulates the input using Watch activation; it does not detect a system-wide gesture or insert into another app. No claim of an unprecedented invention or native feasibility is made. Native sequence: capture verified editable target → recognize explicit activation → reveal bud or palette without modifying text → explicit emoji selection → revalidate target → insert, with one undo step. If no target is available, offer Copy explicitly. Cancel never changes text. Respect existing app controls, secure input, display bounds, and the user's opt-in setting. For buds: reveal beside the active pointer/caret, retain a generous travel corridor between pointer and bud, dismiss on continued typing, focus change, Escape, or movement away. Never insert on hover. Test the reveal delay and false positives with real users. Pocket mode is entered only via Carry in EmojiCopy. The carried emoji remains a proposal until a user confirms insertion. Clear after one successful insertion by default. Integration: mount this HTML once per demo, include the complete CSS and JS below. Source IDs must remain unique. Native implementation is a separate prototype. The browser mock uses PNGs; actual insertion must use the selected Unicode sequence.
ModifierWatch · new (Rust) — an NSEvent global monitor on flagsChanged only. It never sees a character.open_mini_window_at_cursor__handler · src-tauri/src/window.rs — the open path, unchanged.THE ACTION Press both Shift keys together, one under each pinky.
WHERE Anywhere. It needs no text field and no caret.
HANDS Both, on the keyboard. Useless the moment one hand is on the
trackpad, which is exactly when activation 4 takes over.
LEARN CURVE One thing to learn, and it explains itself in one line:
Shift gives you the other character, both Shifts give you the
other keyboard.
FIRES WHEN Both Shift keys go down inside about 120ms, no other modifier
is set, and no key or mouse press lands between the first
press and the release of both.
NEVER FIRES ON Ordinary typing. Real capitals always put a letter between the
two Shift presses, so HELLO! cannot reach it.
PRIVACY The monitor watches flagsChanged only. It never sees a
character, which makes it far lighter than activation 1.
COST One keyboard-flags monitor. No new permission, no caret work.
<div class="trigger-lab" data-kind="4" data-motion="dwell" data-bud="false" data-action="Press and hold the EmojiCopy menu bar icon"> <div class="trigger-lab__layout"><div class="trigger-lab__desktop"> <div class="trigger-lab__bar"><b>Messages</b><span>EmojiCopy <span class="jp-font"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f604.png" alt="Menu bar icon"></span></span></div> <div class="trigger-lab__canvas"><div class="trigger-lab__window"><div class="trigger-lab__title"><span class="trigger-lab__lights"><i></i><i></i><i></i></span> Team chat</div><div class="trigger-lab__conversation"><b>Casey</b><div class="trigger-lab__chat">The new design is ready!</div></div><div class="trigger-lab__composer">That looks great <span class="trigger-lab__result jp-font" data-result></span><span class="trigger-lab__caret"></span></div></div> <div class="trigger-lab__origin"><div class="trigger-lab__keys"><kbd>Hold</kbd><kbd>◉</kbd></div><svg class="trigger-lab__pointer" viewBox="0 0 26 32" aria-hidden="true"><path d="M3 2L23 20L14 21L10 30Z" fill="var(--metal-950)" stroke="var(--white)" stroke-width="2"/></svg></div> <button class="trigger-lab__seed jp-font" data-seed hidden aria-label="Open emoji favorites"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f44d.png" alt="Emoji bud"></button> <div class="trigger-lab__palette" id="lab-palette-4" data-palette hidden aria-label="Emoji favorites"><div class="trigger-lab__palette-head"><b>EmojiCopy · Favorites</b><button class="trigger-lab__dismiss" data-dismiss aria-label="Close emoji favorites">Esc ×</button></div><div class="trigger-lab__choices"><button class="trigger-lab__choice jp-font" data-pick aria-label="Insert rocket"><img src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f680.png" alt="rocket"></button><button class="trigger-lab__choice jp-font" data-pick aria-label="Insert heart eyes"><img src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f60d.png" alt="heart eyes"></button><button class="trigger-lab__choice jp-font" data-pick aria-label="Insert pizza"><img src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f355.png" alt="pizza"></button><button class="trigger-lab__choice jp-font" data-pick aria-label="Insert eyes"><img src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f440.png" alt="eyes"></button><button class="trigger-lab__choice jp-font" data-pick aria-label="Insert raised hands"><img src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f64c.png" alt="raised hands"></button><button class="trigger-lab__choice jp-font" data-pick aria-label="Insert thinking face"><img src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f914.png" alt="thinking face"></button><button class="trigger-lab__choice jp-font" data-pick aria-label="Insert clapping"><img src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f44f.png" alt="clapping"></button><button class="trigger-lab__choice jp-font" data-pick aria-label="Insert sunglasses"><img src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f60e.png" alt="sunglasses"></button><button class="trigger-lab__choice jp-font" data-pick aria-label="Insert star"><img src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f31f.png" alt="star"></button><button class="trigger-lab__choice jp-font" data-pick aria-label="Insert flexed biceps"><img src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f4aa.png" alt="flexed biceps"></button><button class="trigger-lab__choice jp-font" data-pick aria-label="Insert thumbs up"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f44d.png" alt="Thumbs up"></button><button class="trigger-lab__choice jp-font" data-pick aria-label="Insert party popper"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f389.png" alt="Party popper"></button><button class="trigger-lab__choice jp-font" data-pick aria-label="Insert heart"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/2764.png" alt="Heart"></button><button class="trigger-lab__choice jp-font" data-pick aria-label="Insert smile"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f604.png" alt="Smile"></button><button class="trigger-lab__choice jp-font" data-pick aria-label="Insert fire"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f525.png" alt="Fire"></button></div></div> </div></div><div class="trigger-lab__steps"><div class="trigger-lab__step" data-step="1"><strong>1</strong><div><b>Hold the menu icon</b><span>A progress ring confirms the hold.</span></div></div><div class="trigger-lab__step" data-step="2"><strong>2</strong><div><b>Open the full picker</b><span>Release and browse.</span></div></div><div class="trigger-lab__step" data-step="3"><strong>3</strong><div><b>Pick an emoji</b><span>Insert back into your message.</span></div></div></div></div> <div class="trigger-lab__controls"><button class="trigger-lab__play" data-play aria-controls="lab-palette-4" aria-expanded="false">Watch activation</button><button class="trigger-lab__reset" data-reset>Reset</button><span class="trigger-lab__status" role="status"></span></div> <div class="trigger-lab__tradeoff"><span><b>Why it could work</b><br>The existing icon gains a second action.</span><span><b>What to test</b><br>A normal click must retain its existing behavior.</span></div></div>
/* Trigger storyboards: desktop mockups, not native overlays. */
.trigger-lab { display: flex; flex-direction: column; gap: var(--sp-5); font-size: 1.5rem; color: var(--c-text-primary); }
.trigger-lab__layout { display: grid; grid-template-columns: minmax(0, 1fr) 230px; gap: var(--sp-6); align-items: center; }
.trigger-lab__desktop { position: relative; min-width: 0; border-radius: var(--r-lg); overflow: hidden; background: var(--metal-200); border: 1px solid var(--c-border-primary); }
.trigger-lab__bar { display: flex; align-items: center; justify-content: space-between; padding: var(--sp-3) var(--sp-4); background: var(--metal-100); color: var(--metal-700); font-size: 1.2rem; }
.trigger-lab__bar b { color: var(--metal-900); }
.trigger-lab__canvas { position: relative; height: 355px; padding: var(--sp-5); background: linear-gradient(140deg, var(--metal-100), var(--green-400)); }
.trigger-lab__window { position: absolute; inset: 22px 22px 30px; background: var(--white); border: 1px solid var(--metal-200); border-radius: var(--r-md); overflow: hidden; box-shadow: 0 12px 30px color-mix(in srgb, var(--metal-900) 12%, transparent); }
.trigger-lab__title { display: flex; align-items: center; gap: var(--sp-2); padding: var(--sp-3) var(--sp-4); border-bottom: 1px solid var(--metal-100); color: var(--metal-600); font-size: 1.3rem; }
.trigger-lab__lights { display: flex; gap: var(--sp-1); }
.trigger-lab__lights i { width: 8px; height: 8px; border-radius: var(--r-pill); background: var(--metal-300); }
.trigger-lab__conversation { display: flex; flex-direction: column; align-items: flex-start; gap: var(--sp-2); padding: var(--sp-5); color: var(--metal-600); }
.trigger-lab__conversation b { font-size: 1.2rem; }
.trigger-lab__chat { background: var(--metal-100); padding: var(--sp-3) var(--sp-4); border-radius: var(--r-md); color: var(--metal-900); font-size: 1.4rem; }
.trigger-lab__composer { position: absolute; bottom: 28px; left: 18px; right: 18px; border: 2px solid var(--green-700); border-radius: var(--r-md); padding: var(--sp-4); color: var(--metal-900); display: flex; align-items: center; gap: var(--sp-1); min-height: 26px; font-size: 1.6rem; }
.trigger-lab__caret { height: 22px; width: 2px; background: var(--green-800); }
.trigger-lab__result img { width: 25px; height: 25px; vertical-align: middle; }
.trigger-lab__origin { position: absolute; right: 21%; bottom: 105px; width: 160px; height: 120px; pointer-events: none; }
.trigger-lab__pointer { position: absolute; bottom: 0; right: 30px; width: 26px; height: 32px; filter: drop-shadow(0 2px 2px color-mix(in srgb, var(--metal-900) 25%, transparent)); }
.trigger-lab__trail { position: absolute; bottom: 0; right: 5px; width: 88px; height: 88px; color: var(--green-800); opacity: .6; }
.trigger-lab__keys { position: absolute; bottom: 62px; right: 6px; display: flex; gap: var(--sp-1); white-space: nowrap; }
.trigger-lab__keys kbd { display: grid; place-items: center; min-width: 35px; height: 33px; padding: 0 var(--sp-2); font: 600 1.6rem var(--ff-inter), sans-serif; color: var(--metal-900); background: var(--white); border: 1px solid var(--metal-300); border-radius: var(--r-sm); box-shadow: 0 3px 0 var(--metal-200); }
.trigger-lab__seed { position: absolute; right: 23%; bottom: 152px; width: 54px; height: 54px; display: grid; place-items: center; border: 1px solid var(--green-600); border-radius: var(--r-pill); background: var(--white); box-shadow: 0 0 0 6px color-mix(in srgb, var(--green-500) 22%, transparent); cursor: pointer; z-index: 3; }
.trigger-lab__seed img { width: 34px; height: 34px; }
.trigger-lab__palette { position: absolute; z-index: 4; right: 8%; bottom: 145px; width: min(285px, 83%); padding: var(--sp-3); box-sizing: border-box; border-radius: var(--r-lg); background: var(--metal-900); color: var(--white); box-shadow: 0 12px 26px color-mix(in srgb, var(--metal-900) 25%, transparent); }
.trigger-lab__palette::after { content: ''; position: absolute; bottom: -7px; right: 52px; width: 16px; height: 16px; transform: rotate(45deg); background: var(--metal-900); }
.trigger-lab__palette-head { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2); font-size: 1.2rem; padding-bottom: var(--sp-2); }
.trigger-lab__choices { display: flex; justify-content: space-between; gap: var(--sp-1); }
.trigger-lab__choice { display: grid; place-items: center; cursor: pointer; border: 0; border-radius: var(--r-sm); background: var(--metal-800); width: 44px; height: 44px; }
.trigger-lab__choice:hover { background: var(--green-700); }
.trigger-lab__choice img { width: 29px; height: 29px; }
.trigger-lab__dismiss { border: 0; background: transparent; color: var(--metal-300); cursor: pointer; font: inherit; padding: var(--sp-1); }
.trigger-lab__edge { position: absolute; right: 0; bottom: 136px; background: var(--metal-900); color: var(--white); border: 0; border-radius: var(--r-md) 0 0 var(--r-md); padding: var(--sp-3); cursor: pointer; }
.trigger-lab__edge img { width: 24px; height: 24px; }
.trigger-lab__steps { display: flex; flex-direction: column; gap: var(--sp-5); }
.trigger-lab__step { display: grid; grid-template-columns: 28px 1fr; gap: var(--sp-2); align-items: start; line-height: 1.5; }
.trigger-lab__step strong { display: grid; place-items: center; height: 26px; border-radius: var(--r-pill); background: var(--c-surface-tertiary); font-size: 1.2rem; }
.trigger-lab__step b { display: block; }
.trigger-lab__step span { font-size: 1.35rem; color: var(--c-text-secondary); }
.trigger-lab__step.is-current strong { background: var(--green-500); color: var(--green-950); }
.trigger-lab__controls { display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap; }
.trigger-lab__play, .trigger-lab__reset { border: 1px solid var(--c-border-primary); border-radius: var(--r-pill); padding: var(--sp-3) var(--sp-5); font: 600 1.4rem var(--ff-inter), sans-serif; cursor: pointer; }
.trigger-lab__play { color: var(--green-950); background: var(--green-500); }
.trigger-lab__reset { color: var(--c-text-secondary); background: var(--c-surface-primary); }
.trigger-lab__status { font-size: 1.3rem; color: var(--c-text-secondary); flex: 1; min-width: 180px; }
.trigger-lab__tradeoff { display: flex; gap: var(--sp-4); font-size: 1.35rem; line-height: 1.6; color: var(--c-text-secondary); border-top: 1px solid var(--c-border-primary); padding-top: var(--sp-4); }
.trigger-lab [hidden] { display: none !important; }
.trigger-lab button:focus-visible { outline: 3px solid var(--green-700); outline-offset: 3px; }
.trigger-lab[data-phase='gesture'] .trigger-lab__pointer { animation: trigger-orbit 1s ease-in-out; }
.trigger-lab[data-phase='gesture'] .trigger-lab__keys { animation: trigger-press .5s ease-in-out 2; }
.trigger-lab[data-phase='gesture'][data-motion='dwell'] .trigger-lab__pointer { animation: trigger-approach 1s ease-out; }
.trigger-lab[data-phase='gesture'][data-motion='carry'] .trigger-lab__pointer { animation: trigger-carry 1s ease-in-out; }
.trigger-lab[data-phase='done'] .trigger-lab__origin { opacity: .2; }
@keyframes trigger-orbit { 0%,100% { transform: translate(0,0); } 25% { transform: translate(-48px,0); } 50% { transform: translate(-48px,-48px); } 75% { transform: translate(0,-48px); } }
@keyframes trigger-press { 50% { transform: translateY(4px); } }
@keyframes trigger-approach { from { transform: translate(80px,-70px); } to { transform: translate(0,0); } }
@keyframes trigger-carry { from { transform: translate(-150px,-100px); } to { transform: translate(0,0); } }
@media (prefers-reduced-motion: reduce) { .trigger-lab *, .trigger-lab *::after { animation: none !important; } }
@media (max-width: 760px) { .trigger-lab__layout { grid-template-columns: 1fr; } .trigger-lab__steps { flex-direction: row; flex-wrap: wrap; } .trigger-lab__step { flex: 1; min-width: 145px; } .trigger-lab__tradeoff { flex-direction: column; } }
@media (max-width: 450px) { .trigger-lab__window { inset: 15px 8px; } .trigger-lab__composer { left: 10px; right: 10px; padding: var(--sp-3); font-size: 1.4rem; } .trigger-lab__origin { right: 10%; } }
.trigger-intro { font-size: 1.5rem; line-height: 1.6; color: var(--c-text-secondary); margin-bottom: var(--sp-5); }
.trigger-lab__bar img { width: 15px; height: 15px; vertical-align: middle; }
.trigger-lab__keys img { width: 34px; height: 34px; }
.trigger-lab[data-motion='keys'] .trigger-lab__pointer { display: none; }
.trigger-lab[data-kind='4'] .trigger-lab__origin { top: 4px; right: 0; bottom: auto; }
.trigger-lab[data-kind='4'] .trigger-lab__palette { width: min(340px, 90%); right: 5%; bottom: 100px; }
.trigger-lab[data-kind='4'] .trigger-lab__choices { display: grid; grid-template-columns: repeat(5, 1fr); gap: var(--sp-2); }
.trigger-lab[data-kind='8'] .trigger-lab__palette { right: 42px; bottom: 132px; }
.trigger-lab[data-kind='10'] .trigger-lab__origin { right: auto; left: 155px; bottom: 87px; }
.trigger-lab[data-kind='10'] .trigger-lab__seed { right: auto; left: 210px; bottom: 145px; }
.trigger-lab[data-kind='10'] .trigger-lab__palette { left: 72px; right: auto; }
.trigger-lab[data-kind='11'][data-phase='gesture'] .trigger-lab__keys { animation: trigger-carry 1s ease-in-out; }
.trigger-lab__selected { background: var(--green-400); }
.trigger-lab[data-kind='10'] .trigger-lab__origin { left: 88px; }
.trigger-lab[data-kind='10'] .trigger-lab__seed { left: 164px; }
.trigger-lab[data-kind='11'][data-phase='bud'] .trigger-lab__keys,
.trigger-lab[data-kind='11'][data-phase='open'] .trigger-lab__keys { visibility: hidden; }
.trigger-lab[data-kind='4'][data-phase='gesture'] .trigger-lab__pointer { animation: trigger-approach 1s ease-out; }
.trigger-lab[data-kind='4'][data-phase='gesture'] .trigger-lab__keys { outline: 2px solid var(--green-700); outline-offset: 5px; border-radius: var(--r-pill); }
// Isolated storyboards. Buttons simulate native activation; no global input is captured.
document.querySelectorAll('.trigger-lab').forEach(lab => {
const seed = lab.querySelector('[data-seed]')
const palette = lab.querySelector('[data-palette]')
const play = lab.querySelector('[data-play]')
const status = lab.querySelector('[role="status"]')
let timer
const phase = (name, step) => {
lab.dataset.phase = name
lab.querySelectorAll('[data-step]').forEach(item => item.classList.toggle('is-current', Number(item.dataset.step) === step))
}
const open = () => {
clearTimeout(timer)
seed.hidden = true
palette.hidden = false
play.setAttribute('aria-expanded', 'true')
phase('open', 2)
status.textContent = 'Click an emoji to insert it in this example. Escape cancels.'
palette.querySelector('[data-pick]').focus()
}
const reset = () => {
clearTimeout(timer)
seed.hidden = true
palette.hidden = true
play.setAttribute('aria-expanded', 'false')
const result = lab.querySelector('[data-result]')
result.textContent = result.dataset.initial || ''
phase('ready', 1)
status.textContent = 'Motion demo only · click “Watch activation” to begin.'
}
const cancel = () => {
clearTimeout(timer)
seed.hidden = true
palette.hidden = true
play.setAttribute('aria-expanded', 'false')
phase('ready', 1)
status.textContent = 'Dismissed. The message was not changed.'
play.focus()
}
play.addEventListener('click', () => {
reset()
phase('gesture', 1)
status.textContent = lab.dataset.action
timer = setTimeout(() => {
if (lab.dataset.bud === 'true') {
seed.hidden = false
phase('bud', 2)
status.textContent = 'The emoji bud is here. Click it to unfold favorites.'
seed.focus()
} else open()
}, 1050)
})
seed.addEventListener('click', open)
lab.querySelector('[data-reset]').addEventListener('click', reset)
lab.querySelector('[data-dismiss]').addEventListener('click', cancel)
lab.querySelector('[data-edge]')?.addEventListener('click', open)
lab.querySelectorAll('[data-pick]').forEach(button => button.addEventListener('click', () => {
lab.querySelector('[data-result]').replaceChildren(button.querySelector('img').cloneNode())
seed.hidden = true
palette.hidden = true
play.setAttribute('aria-expanded', 'false')
phase('done', 3)
status.textContent = 'Inserted here. In the proposed Mac flow, you keep typing in your app.'
play.focus()
}))
lab.addEventListener('keydown', event => {
if (event.key === 'Escape') { event.stopPropagation(); cancel() }
})
reset()
})
CONCEPT 4: Hold the icon Press and hold the EmojiCopy menu bar icon A normal click must retain its existing behavior. This browser storyboard simulates the input using Watch activation; it does not detect a system-wide gesture or insert into another app. No claim of an unprecedented invention or native feasibility is made. Native sequence: capture verified editable target → recognize explicit activation → reveal bud or palette without modifying text → explicit emoji selection → revalidate target → insert, with one undo step. If no target is available, offer Copy explicitly. Cancel never changes text. Respect existing app controls, secure input, display bounds, and the user's opt-in setting. For buds: reveal beside the active pointer/caret, retain a generous travel corridor between pointer and bud, dismiss on continued typing, focus change, Escape, or movement away. Never insert on hover. Test the reveal delay and false positives with real users. Pocket mode is entered only via Carry in EmojiCopy. The carried emoji remains a proposal until a user confirms insertion. Clear after one successful insertion by default. Integration: mount this HTML once per demo, include the complete CSS and JS below. Source IDs must remain unique. Native implementation is a separate prototype. The browser mock uses PNGs; actual insertion must use the selected Unicode sequence.
TrayIconBuilder.on_tray_icon_event · src-tauri/src/lib.rs:171 — already handles Left/Up. This adds a Down branch and a timer.HoldRing · new — the progress ring drawn around the icon while the press is held.THE ACTION Press the menu bar icon and keep holding.
WHERE The menu bar. Does not exist inside a full-screen app, or for
anyone who hides the icon behind Bartender or Ice.
HANDS Pointer only. No keyboard at all, which is what makes it the
right partner for activation 3.
LEARN CURVE One thing to learn, and nothing on screen hints at it, so this
one lives or dies on onboarding.
FIRES WHEN The press is held past 500ms and the pointer has not travelled
more than a few points.
STANDS DOWN Whenever Command is held, because Command drag is how macOS
reorders the menu bar.
WORST CASE The big window instead of the small one. There is no
destructive failure anywhere in this activation.
COST The cheapest on the page. No new crates, no permission, about a
dozen lines beside the handler already running in lib.rs.
// lib.rs already answers MouseButton::Left + MouseButtonState::Up by calling
// open_mini_window_at_tray__handler. Add the Down branch and a timer.
.on_tray_icon_event(|tray, event| match event {
TrayIconEvent::Click { button: MouseButton::Left,
button_state: MouseButtonState::Down, .. } => {
// Command drag reorders the menu bar. Never fight it.
if command_held() { return }
start_hold_timer(tray.app_handle().clone(), Duration::from_millis(500))
}
TrayIconEvent::Click { button: MouseButton::Left,
button_state: MouseButtonState::Up, .. } => {
if cancel_hold_timer() {
// released early: today's behavior, unchanged and no slower
open_mini_window_at_tray__handler(tray.app_handle().clone())
}
// otherwise the timer already opened the full window
}
_ => {}
})
Start with 5 for everyday use. Try 6 for one-handed access, 7 for mouse users, or 8 for a visible reminder. These are optional alternatives, not four new defaults.
<div class="trigger-lab" data-kind="5" data-motion="keys" data-bud="false" data-action="Press your chosen shortcut"> <div class="trigger-lab__layout"><div class="trigger-lab__desktop"> <div class="trigger-lab__bar"><b>Messages</b><span>EmojiCopy <span class="jp-font"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f604.png" alt="Menu bar icon"></span></span></div> <div class="trigger-lab__canvas"><div class="trigger-lab__window"><div class="trigger-lab__title"><span class="trigger-lab__lights"><i></i><i></i><i></i></span> Team chat</div><div class="trigger-lab__conversation"><b>Casey</b><div class="trigger-lab__chat">The new design is ready!</div></div><div class="trigger-lab__composer">That looks great <span class="trigger-lab__result jp-font" data-result></span><span class="trigger-lab__caret"></span></div></div> <div class="trigger-lab__origin"><div class="trigger-lab__keys"><kbd>⌃</kbd><kbd>⌥</kbd><kbd>E</kbd></div><svg class="trigger-lab__pointer" viewBox="0 0 26 32" aria-hidden="true"><path d="M3 2L23 20L14 21L10 30Z" fill="var(--metal-950)" stroke="var(--white)" stroke-width="2"/></svg></div> <button class="trigger-lab__seed jp-font" data-seed hidden aria-label="Open emoji favorites"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f44d.png" alt="Emoji bud"></button> <div class="trigger-lab__palette" id="lab-palette-5" data-palette hidden aria-label="Emoji favorites"><div class="trigger-lab__palette-head"><b>EmojiCopy · Favorites</b><button class="trigger-lab__dismiss" data-dismiss aria-label="Close emoji favorites">Esc ×</button></div><div class="trigger-lab__choices"><button class="trigger-lab__choice jp-font" data-pick aria-label="Insert thumbs up"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f44d.png" alt="Thumbs up"></button><button class="trigger-lab__choice jp-font" data-pick aria-label="Insert party popper"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f389.png" alt="Party popper"></button><button class="trigger-lab__choice jp-font" data-pick aria-label="Insert heart"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/2764.png" alt="Heart"></button><button class="trigger-lab__choice jp-font" data-pick aria-label="Insert smile"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f604.png" alt="Smile"></button><button class="trigger-lab__choice jp-font" data-pick aria-label="Insert fire"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f525.png" alt="Fire"></button></div></div> </div></div><div class="trigger-lab__steps"><div class="trigger-lab__step" data-step="1"><strong>1</strong><div><b>Press your shortcut</b><span>The example is Control–Option–E.</span></div></div><div class="trigger-lab__step" data-step="2"><strong>2</strong><div><b>Picker appears</b><span>Close to the field you are using.</span></div></div><div class="trigger-lab__step" data-step="3"><strong>3</strong><div><b>Pick an emoji</b><span>Keep writing without switching apps.</span></div></div></div></div> <div class="trigger-lab__controls"><button class="trigger-lab__play" data-play aria-controls="lab-palette-5" aria-expanded="false">Watch activation</button><button class="trigger-lab__reset" data-reset>Reset</button><span class="trigger-lab__status" role="status"></span></div> <div class="trigger-lab__tradeoff"><span><b>Why it could work</b><br>Predictable and easy to personalize.</span><span><b>What to test</b><br>Check shortcut conflicts during setup.</span></div></div>
/* Trigger storyboards: desktop mockups, not native overlays. */
.trigger-lab { display: flex; flex-direction: column; gap: var(--sp-5); font-size: 1.5rem; color: var(--c-text-primary); }
.trigger-lab__layout { display: grid; grid-template-columns: minmax(0, 1fr) 230px; gap: var(--sp-6); align-items: center; }
.trigger-lab__desktop { position: relative; min-width: 0; border-radius: var(--r-lg); overflow: hidden; background: var(--metal-200); border: 1px solid var(--c-border-primary); }
.trigger-lab__bar { display: flex; align-items: center; justify-content: space-between; padding: var(--sp-3) var(--sp-4); background: var(--metal-100); color: var(--metal-700); font-size: 1.2rem; }
.trigger-lab__bar b { color: var(--metal-900); }
.trigger-lab__canvas { position: relative; height: 355px; padding: var(--sp-5); background: linear-gradient(140deg, var(--metal-100), var(--green-400)); }
.trigger-lab__window { position: absolute; inset: 22px 22px 30px; background: var(--white); border: 1px solid var(--metal-200); border-radius: var(--r-md); overflow: hidden; box-shadow: 0 12px 30px color-mix(in srgb, var(--metal-900) 12%, transparent); }
.trigger-lab__title { display: flex; align-items: center; gap: var(--sp-2); padding: var(--sp-3) var(--sp-4); border-bottom: 1px solid var(--metal-100); color: var(--metal-600); font-size: 1.3rem; }
.trigger-lab__lights { display: flex; gap: var(--sp-1); }
.trigger-lab__lights i { width: 8px; height: 8px; border-radius: var(--r-pill); background: var(--metal-300); }
.trigger-lab__conversation { display: flex; flex-direction: column; align-items: flex-start; gap: var(--sp-2); padding: var(--sp-5); color: var(--metal-600); }
.trigger-lab__conversation b { font-size: 1.2rem; }
.trigger-lab__chat { background: var(--metal-100); padding: var(--sp-3) var(--sp-4); border-radius: var(--r-md); color: var(--metal-900); font-size: 1.4rem; }
.trigger-lab__composer { position: absolute; bottom: 28px; left: 18px; right: 18px; border: 2px solid var(--green-700); border-radius: var(--r-md); padding: var(--sp-4); color: var(--metal-900); display: flex; align-items: center; gap: var(--sp-1); min-height: 26px; font-size: 1.6rem; }
.trigger-lab__caret { height: 22px; width: 2px; background: var(--green-800); }
.trigger-lab__result img { width: 25px; height: 25px; vertical-align: middle; }
.trigger-lab__origin { position: absolute; right: 21%; bottom: 105px; width: 160px; height: 120px; pointer-events: none; }
.trigger-lab__pointer { position: absolute; bottom: 0; right: 30px; width: 26px; height: 32px; filter: drop-shadow(0 2px 2px color-mix(in srgb, var(--metal-900) 25%, transparent)); }
.trigger-lab__trail { position: absolute; bottom: 0; right: 5px; width: 88px; height: 88px; color: var(--green-800); opacity: .6; }
.trigger-lab__keys { position: absolute; bottom: 62px; right: 6px; display: flex; gap: var(--sp-1); white-space: nowrap; }
.trigger-lab__keys kbd { display: grid; place-items: center; min-width: 35px; height: 33px; padding: 0 var(--sp-2); font: 600 1.6rem var(--ff-inter), sans-serif; color: var(--metal-900); background: var(--white); border: 1px solid var(--metal-300); border-radius: var(--r-sm); box-shadow: 0 3px 0 var(--metal-200); }
.trigger-lab__seed { position: absolute; right: 23%; bottom: 152px; width: 54px; height: 54px; display: grid; place-items: center; border: 1px solid var(--green-600); border-radius: var(--r-pill); background: var(--white); box-shadow: 0 0 0 6px color-mix(in srgb, var(--green-500) 22%, transparent); cursor: pointer; z-index: 3; }
.trigger-lab__seed img { width: 34px; height: 34px; }
.trigger-lab__palette { position: absolute; z-index: 4; right: 8%; bottom: 145px; width: min(285px, 83%); padding: var(--sp-3); box-sizing: border-box; border-radius: var(--r-lg); background: var(--metal-900); color: var(--white); box-shadow: 0 12px 26px color-mix(in srgb, var(--metal-900) 25%, transparent); }
.trigger-lab__palette::after { content: ''; position: absolute; bottom: -7px; right: 52px; width: 16px; height: 16px; transform: rotate(45deg); background: var(--metal-900); }
.trigger-lab__palette-head { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2); font-size: 1.2rem; padding-bottom: var(--sp-2); }
.trigger-lab__choices { display: flex; justify-content: space-between; gap: var(--sp-1); }
.trigger-lab__choice { display: grid; place-items: center; cursor: pointer; border: 0; border-radius: var(--r-sm); background: var(--metal-800); width: 44px; height: 44px; }
.trigger-lab__choice:hover { background: var(--green-700); }
.trigger-lab__choice img { width: 29px; height: 29px; }
.trigger-lab__dismiss { border: 0; background: transparent; color: var(--metal-300); cursor: pointer; font: inherit; padding: var(--sp-1); }
.trigger-lab__edge { position: absolute; right: 0; bottom: 136px; background: var(--metal-900); color: var(--white); border: 0; border-radius: var(--r-md) 0 0 var(--r-md); padding: var(--sp-3); cursor: pointer; }
.trigger-lab__edge img { width: 24px; height: 24px; }
.trigger-lab__steps { display: flex; flex-direction: column; gap: var(--sp-5); }
.trigger-lab__step { display: grid; grid-template-columns: 28px 1fr; gap: var(--sp-2); align-items: start; line-height: 1.5; }
.trigger-lab__step strong { display: grid; place-items: center; height: 26px; border-radius: var(--r-pill); background: var(--c-surface-tertiary); font-size: 1.2rem; }
.trigger-lab__step b { display: block; }
.trigger-lab__step span { font-size: 1.35rem; color: var(--c-text-secondary); }
.trigger-lab__step.is-current strong { background: var(--green-500); color: var(--green-950); }
.trigger-lab__controls { display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap; }
.trigger-lab__play, .trigger-lab__reset { border: 1px solid var(--c-border-primary); border-radius: var(--r-pill); padding: var(--sp-3) var(--sp-5); font: 600 1.4rem var(--ff-inter), sans-serif; cursor: pointer; }
.trigger-lab__play { color: var(--green-950); background: var(--green-500); }
.trigger-lab__reset { color: var(--c-text-secondary); background: var(--c-surface-primary); }
.trigger-lab__status { font-size: 1.3rem; color: var(--c-text-secondary); flex: 1; min-width: 180px; }
.trigger-lab__tradeoff { display: flex; gap: var(--sp-4); font-size: 1.35rem; line-height: 1.6; color: var(--c-text-secondary); border-top: 1px solid var(--c-border-primary); padding-top: var(--sp-4); }
.trigger-lab [hidden] { display: none !important; }
.trigger-lab button:focus-visible { outline: 3px solid var(--green-700); outline-offset: 3px; }
.trigger-lab[data-phase='gesture'] .trigger-lab__pointer { animation: trigger-orbit 1s ease-in-out; }
.trigger-lab[data-phase='gesture'] .trigger-lab__keys { animation: trigger-press .5s ease-in-out 2; }
.trigger-lab[data-phase='gesture'][data-motion='dwell'] .trigger-lab__pointer { animation: trigger-approach 1s ease-out; }
.trigger-lab[data-phase='gesture'][data-motion='carry'] .trigger-lab__pointer { animation: trigger-carry 1s ease-in-out; }
.trigger-lab[data-phase='done'] .trigger-lab__origin { opacity: .2; }
@keyframes trigger-orbit { 0%,100% { transform: translate(0,0); } 25% { transform: translate(-48px,0); } 50% { transform: translate(-48px,-48px); } 75% { transform: translate(0,-48px); } }
@keyframes trigger-press { 50% { transform: translateY(4px); } }
@keyframes trigger-approach { from { transform: translate(80px,-70px); } to { transform: translate(0,0); } }
@keyframes trigger-carry { from { transform: translate(-150px,-100px); } to { transform: translate(0,0); } }
@media (prefers-reduced-motion: reduce) { .trigger-lab *, .trigger-lab *::after { animation: none !important; } }
@media (max-width: 760px) { .trigger-lab__layout { grid-template-columns: 1fr; } .trigger-lab__steps { flex-direction: row; flex-wrap: wrap; } .trigger-lab__step { flex: 1; min-width: 145px; } .trigger-lab__tradeoff { flex-direction: column; } }
@media (max-width: 450px) { .trigger-lab__window { inset: 15px 8px; } .trigger-lab__composer { left: 10px; right: 10px; padding: var(--sp-3); font-size: 1.4rem; } .trigger-lab__origin { right: 10%; } }
.trigger-intro { font-size: 1.5rem; line-height: 1.6; color: var(--c-text-secondary); margin-bottom: var(--sp-5); }
.trigger-lab__bar img { width: 15px; height: 15px; vertical-align: middle; }
.trigger-lab__keys img { width: 34px; height: 34px; }
.trigger-lab[data-motion='keys'] .trigger-lab__pointer { display: none; }
.trigger-lab[data-kind='4'] .trigger-lab__origin { top: 4px; right: 0; bottom: auto; }
.trigger-lab[data-kind='4'] .trigger-lab__palette { width: min(340px, 90%); right: 5%; bottom: 100px; }
.trigger-lab[data-kind='4'] .trigger-lab__choices { display: grid; grid-template-columns: repeat(5, 1fr); gap: var(--sp-2); }
.trigger-lab[data-kind='8'] .trigger-lab__palette { right: 42px; bottom: 132px; }
.trigger-lab[data-kind='10'] .trigger-lab__origin { right: auto; left: 155px; bottom: 87px; }
.trigger-lab[data-kind='10'] .trigger-lab__seed { right: auto; left: 210px; bottom: 145px; }
.trigger-lab[data-kind='10'] .trigger-lab__palette { left: 72px; right: auto; }
.trigger-lab[data-kind='11'][data-phase='gesture'] .trigger-lab__keys { animation: trigger-carry 1s ease-in-out; }
.trigger-lab__selected { background: var(--green-400); }
.trigger-lab[data-kind='10'] .trigger-lab__origin { left: 88px; }
.trigger-lab[data-kind='10'] .trigger-lab__seed { left: 164px; }
.trigger-lab[data-kind='11'][data-phase='bud'] .trigger-lab__keys,
.trigger-lab[data-kind='11'][data-phase='open'] .trigger-lab__keys { visibility: hidden; }
.trigger-lab[data-kind='4'][data-phase='gesture'] .trigger-lab__pointer { animation: trigger-approach 1s ease-out; }
.trigger-lab[data-kind='4'][data-phase='gesture'] .trigger-lab__keys { outline: 2px solid var(--green-700); outline-offset: 5px; border-radius: var(--r-pill); }
// Isolated storyboards. Buttons simulate native activation; no global input is captured.
document.querySelectorAll('.trigger-lab').forEach(lab => {
const seed = lab.querySelector('[data-seed]')
const palette = lab.querySelector('[data-palette]')
const play = lab.querySelector('[data-play]')
const status = lab.querySelector('[role="status"]')
let timer
const phase = (name, step) => {
lab.dataset.phase = name
lab.querySelectorAll('[data-step]').forEach(item => item.classList.toggle('is-current', Number(item.dataset.step) === step))
}
const open = () => {
clearTimeout(timer)
seed.hidden = true
palette.hidden = false
play.setAttribute('aria-expanded', 'true')
phase('open', 2)
status.textContent = 'Click an emoji to insert it in this example. Escape cancels.'
palette.querySelector('[data-pick]').focus()
}
const reset = () => {
clearTimeout(timer)
seed.hidden = true
palette.hidden = true
play.setAttribute('aria-expanded', 'false')
const result = lab.querySelector('[data-result]')
result.textContent = result.dataset.initial || ''
phase('ready', 1)
status.textContent = 'Motion demo only · click “Watch activation” to begin.'
}
const cancel = () => {
clearTimeout(timer)
seed.hidden = true
palette.hidden = true
play.setAttribute('aria-expanded', 'false')
phase('ready', 1)
status.textContent = 'Dismissed. The message was not changed.'
play.focus()
}
play.addEventListener('click', () => {
reset()
phase('gesture', 1)
status.textContent = lab.dataset.action
timer = setTimeout(() => {
if (lab.dataset.bud === 'true') {
seed.hidden = false
phase('bud', 2)
status.textContent = 'The emoji bud is here. Click it to unfold favorites.'
seed.focus()
} else open()
}, 1050)
})
seed.addEventListener('click', open)
lab.querySelector('[data-reset]').addEventListener('click', reset)
lab.querySelector('[data-dismiss]').addEventListener('click', cancel)
lab.querySelector('[data-edge]')?.addEventListener('click', open)
lab.querySelectorAll('[data-pick]').forEach(button => button.addEventListener('click', () => {
lab.querySelector('[data-result]').replaceChildren(button.querySelector('img').cloneNode())
seed.hidden = true
palette.hidden = true
play.setAttribute('aria-expanded', 'false')
phase('done', 3)
status.textContent = 'Inserted here. In the proposed Mac flow, you keep typing in your app.'
play.focus()
}))
lab.addEventListener('keydown', event => {
if (event.key === 'Escape') { event.stopPropagation(); cancel() }
})
reset()
})
CONCEPT 5: Your own shortcut Press your chosen shortcut Check shortcut conflicts during setup. This browser storyboard simulates the input using Watch activation; it does not detect a system-wide gesture or insert into another app. No claim of an unprecedented invention or native feasibility is made. Native sequence: capture verified editable target → recognize explicit activation → reveal bud or palette without modifying text → explicit emoji selection → revalidate target → insert, with one undo step. If no target is available, offer Copy explicitly. Cancel never changes text. Respect existing app controls, secure input, display bounds, and the user's opt-in setting. For buds: reveal beside the active pointer/caret, retain a generous travel corridor between pointer and bud, dismiss on continued typing, focus change, Escape, or movement away. Never insert on hover. Test the reveal delay and false positives with real users. Pocket mode is entered only via Carry in EmojiCopy. The carried emoji remains a proposal until a user confirms insertion. Clear after one successful insertion by default. Integration: mount this HTML once per demo, include the complete CSS and JS below. Source IDs must remain unique. Native implementation is a separate prototype. The browser mock uses PNGs; actual insertion must use the selected Unicode sequence.
<div class="trigger-lab" data-kind="6" data-motion="keys" data-bud="false" data-action="Tap and release Option twice"> <div class="trigger-lab__layout"><div class="trigger-lab__desktop"> <div class="trigger-lab__bar"><b>Messages</b><span>EmojiCopy <span class="jp-font"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f604.png" alt="Menu bar icon"></span></span></div> <div class="trigger-lab__canvas"><div class="trigger-lab__window"><div class="trigger-lab__title"><span class="trigger-lab__lights"><i></i><i></i><i></i></span> Team chat</div><div class="trigger-lab__conversation"><b>Casey</b><div class="trigger-lab__chat">The new design is ready!</div></div><div class="trigger-lab__composer">That looks great <span class="trigger-lab__result jp-font" data-result></span><span class="trigger-lab__caret"></span></div></div> <div class="trigger-lab__origin"><div class="trigger-lab__keys"><kbd>⌥</kbd><kbd>⌥</kbd></div><svg class="trigger-lab__pointer" viewBox="0 0 26 32" aria-hidden="true"><path d="M3 2L23 20L14 21L10 30Z" fill="var(--metal-950)" stroke="var(--white)" stroke-width="2"/></svg></div> <button class="trigger-lab__seed jp-font" data-seed hidden aria-label="Open emoji favorites"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f44d.png" alt="Emoji bud"></button> <div class="trigger-lab__palette" id="lab-palette-6" data-palette hidden aria-label="Emoji favorites"><div class="trigger-lab__palette-head"><b>EmojiCopy · Favorites</b><button class="trigger-lab__dismiss" data-dismiss aria-label="Close emoji favorites">Esc ×</button></div><div class="trigger-lab__choices"><button class="trigger-lab__choice jp-font" data-pick aria-label="Insert thumbs up"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f44d.png" alt="Thumbs up"></button><button class="trigger-lab__choice jp-font" data-pick aria-label="Insert party popper"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f389.png" alt="Party popper"></button><button class="trigger-lab__choice jp-font" data-pick aria-label="Insert heart"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/2764.png" alt="Heart"></button><button class="trigger-lab__choice jp-font" data-pick aria-label="Insert smile"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f604.png" alt="Smile"></button><button class="trigger-lab__choice jp-font" data-pick aria-label="Insert fire"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f525.png" alt="Fire"></button></div></div> </div></div><div class="trigger-lab__steps"><div class="trigger-lab__step" data-step="1"><strong>1</strong><div><b>Tap Option twice</b><span>Two separate taps with one thumb.</span></div></div><div class="trigger-lab__step" data-step="2"><strong>2</strong><div><b>Favorites appear</b><span>A small palette, not a full window.</span></div></div><div class="trigger-lab__step" data-step="3"><strong>3</strong><div><b>Pick an emoji</b><span>Insert and continue typing.</span></div></div></div></div> <div class="trigger-lab__controls"><button class="trigger-lab__play" data-play aria-controls="lab-palette-6" aria-expanded="false">Watch activation</button><button class="trigger-lab__reset" data-reset>Reset</button><span class="trigger-lab__status" role="status"></span></div> <div class="trigger-lab__tradeoff"><span><b>Why it could work</b><br>One hand, one key.</span><span><b>What to test</b><br>Timing and accidental activation need native testing.</span></div></div>
/* Trigger storyboards: desktop mockups, not native overlays. */
.trigger-lab { display: flex; flex-direction: column; gap: var(--sp-5); font-size: 1.5rem; color: var(--c-text-primary); }
.trigger-lab__layout { display: grid; grid-template-columns: minmax(0, 1fr) 230px; gap: var(--sp-6); align-items: center; }
.trigger-lab__desktop { position: relative; min-width: 0; border-radius: var(--r-lg); overflow: hidden; background: var(--metal-200); border: 1px solid var(--c-border-primary); }
.trigger-lab__bar { display: flex; align-items: center; justify-content: space-between; padding: var(--sp-3) var(--sp-4); background: var(--metal-100); color: var(--metal-700); font-size: 1.2rem; }
.trigger-lab__bar b { color: var(--metal-900); }
.trigger-lab__canvas { position: relative; height: 355px; padding: var(--sp-5); background: linear-gradient(140deg, var(--metal-100), var(--green-400)); }
.trigger-lab__window { position: absolute; inset: 22px 22px 30px; background: var(--white); border: 1px solid var(--metal-200); border-radius: var(--r-md); overflow: hidden; box-shadow: 0 12px 30px color-mix(in srgb, var(--metal-900) 12%, transparent); }
.trigger-lab__title { display: flex; align-items: center; gap: var(--sp-2); padding: var(--sp-3) var(--sp-4); border-bottom: 1px solid var(--metal-100); color: var(--metal-600); font-size: 1.3rem; }
.trigger-lab__lights { display: flex; gap: var(--sp-1); }
.trigger-lab__lights i { width: 8px; height: 8px; border-radius: var(--r-pill); background: var(--metal-300); }
.trigger-lab__conversation { display: flex; flex-direction: column; align-items: flex-start; gap: var(--sp-2); padding: var(--sp-5); color: var(--metal-600); }
.trigger-lab__conversation b { font-size: 1.2rem; }
.trigger-lab__chat { background: var(--metal-100); padding: var(--sp-3) var(--sp-4); border-radius: var(--r-md); color: var(--metal-900); font-size: 1.4rem; }
.trigger-lab__composer { position: absolute; bottom: 28px; left: 18px; right: 18px; border: 2px solid var(--green-700); border-radius: var(--r-md); padding: var(--sp-4); color: var(--metal-900); display: flex; align-items: center; gap: var(--sp-1); min-height: 26px; font-size: 1.6rem; }
.trigger-lab__caret { height: 22px; width: 2px; background: var(--green-800); }
.trigger-lab__result img { width: 25px; height: 25px; vertical-align: middle; }
.trigger-lab__origin { position: absolute; right: 21%; bottom: 105px; width: 160px; height: 120px; pointer-events: none; }
.trigger-lab__pointer { position: absolute; bottom: 0; right: 30px; width: 26px; height: 32px; filter: drop-shadow(0 2px 2px color-mix(in srgb, var(--metal-900) 25%, transparent)); }
.trigger-lab__trail { position: absolute; bottom: 0; right: 5px; width: 88px; height: 88px; color: var(--green-800); opacity: .6; }
.trigger-lab__keys { position: absolute; bottom: 62px; right: 6px; display: flex; gap: var(--sp-1); white-space: nowrap; }
.trigger-lab__keys kbd { display: grid; place-items: center; min-width: 35px; height: 33px; padding: 0 var(--sp-2); font: 600 1.6rem var(--ff-inter), sans-serif; color: var(--metal-900); background: var(--white); border: 1px solid var(--metal-300); border-radius: var(--r-sm); box-shadow: 0 3px 0 var(--metal-200); }
.trigger-lab__seed { position: absolute; right: 23%; bottom: 152px; width: 54px; height: 54px; display: grid; place-items: center; border: 1px solid var(--green-600); border-radius: var(--r-pill); background: var(--white); box-shadow: 0 0 0 6px color-mix(in srgb, var(--green-500) 22%, transparent); cursor: pointer; z-index: 3; }
.trigger-lab__seed img { width: 34px; height: 34px; }
.trigger-lab__palette { position: absolute; z-index: 4; right: 8%; bottom: 145px; width: min(285px, 83%); padding: var(--sp-3); box-sizing: border-box; border-radius: var(--r-lg); background: var(--metal-900); color: var(--white); box-shadow: 0 12px 26px color-mix(in srgb, var(--metal-900) 25%, transparent); }
.trigger-lab__palette::after { content: ''; position: absolute; bottom: -7px; right: 52px; width: 16px; height: 16px; transform: rotate(45deg); background: var(--metal-900); }
.trigger-lab__palette-head { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2); font-size: 1.2rem; padding-bottom: var(--sp-2); }
.trigger-lab__choices { display: flex; justify-content: space-between; gap: var(--sp-1); }
.trigger-lab__choice { display: grid; place-items: center; cursor: pointer; border: 0; border-radius: var(--r-sm); background: var(--metal-800); width: 44px; height: 44px; }
.trigger-lab__choice:hover { background: var(--green-700); }
.trigger-lab__choice img { width: 29px; height: 29px; }
.trigger-lab__dismiss { border: 0; background: transparent; color: var(--metal-300); cursor: pointer; font: inherit; padding: var(--sp-1); }
.trigger-lab__edge { position: absolute; right: 0; bottom: 136px; background: var(--metal-900); color: var(--white); border: 0; border-radius: var(--r-md) 0 0 var(--r-md); padding: var(--sp-3); cursor: pointer; }
.trigger-lab__edge img { width: 24px; height: 24px; }
.trigger-lab__steps { display: flex; flex-direction: column; gap: var(--sp-5); }
.trigger-lab__step { display: grid; grid-template-columns: 28px 1fr; gap: var(--sp-2); align-items: start; line-height: 1.5; }
.trigger-lab__step strong { display: grid; place-items: center; height: 26px; border-radius: var(--r-pill); background: var(--c-surface-tertiary); font-size: 1.2rem; }
.trigger-lab__step b { display: block; }
.trigger-lab__step span { font-size: 1.35rem; color: var(--c-text-secondary); }
.trigger-lab__step.is-current strong { background: var(--green-500); color: var(--green-950); }
.trigger-lab__controls { display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap; }
.trigger-lab__play, .trigger-lab__reset { border: 1px solid var(--c-border-primary); border-radius: var(--r-pill); padding: var(--sp-3) var(--sp-5); font: 600 1.4rem var(--ff-inter), sans-serif; cursor: pointer; }
.trigger-lab__play { color: var(--green-950); background: var(--green-500); }
.trigger-lab__reset { color: var(--c-text-secondary); background: var(--c-surface-primary); }
.trigger-lab__status { font-size: 1.3rem; color: var(--c-text-secondary); flex: 1; min-width: 180px; }
.trigger-lab__tradeoff { display: flex; gap: var(--sp-4); font-size: 1.35rem; line-height: 1.6; color: var(--c-text-secondary); border-top: 1px solid var(--c-border-primary); padding-top: var(--sp-4); }
.trigger-lab [hidden] { display: none !important; }
.trigger-lab button:focus-visible { outline: 3px solid var(--green-700); outline-offset: 3px; }
.trigger-lab[data-phase='gesture'] .trigger-lab__pointer { animation: trigger-orbit 1s ease-in-out; }
.trigger-lab[data-phase='gesture'] .trigger-lab__keys { animation: trigger-press .5s ease-in-out 2; }
.trigger-lab[data-phase='gesture'][data-motion='dwell'] .trigger-lab__pointer { animation: trigger-approach 1s ease-out; }
.trigger-lab[data-phase='gesture'][data-motion='carry'] .trigger-lab__pointer { animation: trigger-carry 1s ease-in-out; }
.trigger-lab[data-phase='done'] .trigger-lab__origin { opacity: .2; }
@keyframes trigger-orbit { 0%,100% { transform: translate(0,0); } 25% { transform: translate(-48px,0); } 50% { transform: translate(-48px,-48px); } 75% { transform: translate(0,-48px); } }
@keyframes trigger-press { 50% { transform: translateY(4px); } }
@keyframes trigger-approach { from { transform: translate(80px,-70px); } to { transform: translate(0,0); } }
@keyframes trigger-carry { from { transform: translate(-150px,-100px); } to { transform: translate(0,0); } }
@media (prefers-reduced-motion: reduce) { .trigger-lab *, .trigger-lab *::after { animation: none !important; } }
@media (max-width: 760px) { .trigger-lab__layout { grid-template-columns: 1fr; } .trigger-lab__steps { flex-direction: row; flex-wrap: wrap; } .trigger-lab__step { flex: 1; min-width: 145px; } .trigger-lab__tradeoff { flex-direction: column; } }
@media (max-width: 450px) { .trigger-lab__window { inset: 15px 8px; } .trigger-lab__composer { left: 10px; right: 10px; padding: var(--sp-3); font-size: 1.4rem; } .trigger-lab__origin { right: 10%; } }
.trigger-intro { font-size: 1.5rem; line-height: 1.6; color: var(--c-text-secondary); margin-bottom: var(--sp-5); }
.trigger-lab__bar img { width: 15px; height: 15px; vertical-align: middle; }
.trigger-lab__keys img { width: 34px; height: 34px; }
.trigger-lab[data-motion='keys'] .trigger-lab__pointer { display: none; }
.trigger-lab[data-kind='4'] .trigger-lab__origin { top: 4px; right: 0; bottom: auto; }
.trigger-lab[data-kind='4'] .trigger-lab__palette { width: min(340px, 90%); right: 5%; bottom: 100px; }
.trigger-lab[data-kind='4'] .trigger-lab__choices { display: grid; grid-template-columns: repeat(5, 1fr); gap: var(--sp-2); }
.trigger-lab[data-kind='8'] .trigger-lab__palette { right: 42px; bottom: 132px; }
.trigger-lab[data-kind='10'] .trigger-lab__origin { right: auto; left: 155px; bottom: 87px; }
.trigger-lab[data-kind='10'] .trigger-lab__seed { right: auto; left: 210px; bottom: 145px; }
.trigger-lab[data-kind='10'] .trigger-lab__palette { left: 72px; right: auto; }
.trigger-lab[data-kind='11'][data-phase='gesture'] .trigger-lab__keys { animation: trigger-carry 1s ease-in-out; }
.trigger-lab__selected { background: var(--green-400); }
.trigger-lab[data-kind='10'] .trigger-lab__origin { left: 88px; }
.trigger-lab[data-kind='10'] .trigger-lab__seed { left: 164px; }
.trigger-lab[data-kind='11'][data-phase='bud'] .trigger-lab__keys,
.trigger-lab[data-kind='11'][data-phase='open'] .trigger-lab__keys { visibility: hidden; }
.trigger-lab[data-kind='4'][data-phase='gesture'] .trigger-lab__pointer { animation: trigger-approach 1s ease-out; }
.trigger-lab[data-kind='4'][data-phase='gesture'] .trigger-lab__keys { outline: 2px solid var(--green-700); outline-offset: 5px; border-radius: var(--r-pill); }
// Isolated storyboards. Buttons simulate native activation; no global input is captured.
document.querySelectorAll('.trigger-lab').forEach(lab => {
const seed = lab.querySelector('[data-seed]')
const palette = lab.querySelector('[data-palette]')
const play = lab.querySelector('[data-play]')
const status = lab.querySelector('[role="status"]')
let timer
const phase = (name, step) => {
lab.dataset.phase = name
lab.querySelectorAll('[data-step]').forEach(item => item.classList.toggle('is-current', Number(item.dataset.step) === step))
}
const open = () => {
clearTimeout(timer)
seed.hidden = true
palette.hidden = false
play.setAttribute('aria-expanded', 'true')
phase('open', 2)
status.textContent = 'Click an emoji to insert it in this example. Escape cancels.'
palette.querySelector('[data-pick]').focus()
}
const reset = () => {
clearTimeout(timer)
seed.hidden = true
palette.hidden = true
play.setAttribute('aria-expanded', 'false')
const result = lab.querySelector('[data-result]')
result.textContent = result.dataset.initial || ''
phase('ready', 1)
status.textContent = 'Motion demo only · click “Watch activation” to begin.'
}
const cancel = () => {
clearTimeout(timer)
seed.hidden = true
palette.hidden = true
play.setAttribute('aria-expanded', 'false')
phase('ready', 1)
status.textContent = 'Dismissed. The message was not changed.'
play.focus()
}
play.addEventListener('click', () => {
reset()
phase('gesture', 1)
status.textContent = lab.dataset.action
timer = setTimeout(() => {
if (lab.dataset.bud === 'true') {
seed.hidden = false
phase('bud', 2)
status.textContent = 'The emoji bud is here. Click it to unfold favorites.'
seed.focus()
} else open()
}, 1050)
})
seed.addEventListener('click', open)
lab.querySelector('[data-reset]').addEventListener('click', reset)
lab.querySelector('[data-dismiss]').addEventListener('click', cancel)
lab.querySelector('[data-edge]')?.addEventListener('click', open)
lab.querySelectorAll('[data-pick]').forEach(button => button.addEventListener('click', () => {
lab.querySelector('[data-result]').replaceChildren(button.querySelector('img').cloneNode())
seed.hidden = true
palette.hidden = true
play.setAttribute('aria-expanded', 'false')
phase('done', 3)
status.textContent = 'Inserted here. In the proposed Mac flow, you keep typing in your app.'
play.focus()
}))
lab.addEventListener('keydown', event => {
if (event.key === 'Escape') { event.stopPropagation(); cancel() }
})
reset()
})
CONCEPT 6: Double-tap Option Tap and release Option twice Timing and accidental activation need native testing. This browser storyboard simulates the input using Watch activation; it does not detect a system-wide gesture or insert into another app. No claim of an unprecedented invention or native feasibility is made. Native sequence: capture verified editable target → recognize explicit activation → reveal bud or palette without modifying text → explicit emoji selection → revalidate target → insert, with one undo step. If no target is available, offer Copy explicitly. Cancel never changes text. Respect existing app controls, secure input, display bounds, and the user's opt-in setting. For buds: reveal beside the active pointer/caret, retain a generous travel corridor between pointer and bud, dismiss on continued typing, focus change, Escape, or movement away. Never insert on hover. Test the reveal delay and false positives with real users. Pocket mode is entered only via Carry in EmojiCopy. The carried emoji remains a proposal until a user confirms insertion. Clear after one successful insertion by default. Integration: mount this HTML once per demo, include the complete CSS and JS below. Source IDs must remain unique. Native implementation is a separate prototype. The browser mock uses PNGs; actual insertion must use the selected Unicode sequence.
<div class="trigger-lab" data-kind="7" data-motion="dwell" data-bud="false" data-action="Press your assigned mouse side button"> <div class="trigger-lab__layout"><div class="trigger-lab__desktop"> <div class="trigger-lab__bar"><b>Messages</b><span>EmojiCopy <span class="jp-font"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f604.png" alt="Menu bar icon"></span></span></div> <div class="trigger-lab__canvas"><div class="trigger-lab__window"><div class="trigger-lab__title"><span class="trigger-lab__lights"><i></i><i></i><i></i></span> Team chat</div><div class="trigger-lab__conversation"><b>Casey</b><div class="trigger-lab__chat">The new design is ready!</div></div><div class="trigger-lab__composer">That looks great <span class="trigger-lab__result jp-font" data-result></span><span class="trigger-lab__caret"></span></div></div> <div class="trigger-lab__origin"><div class="trigger-lab__keys"><kbd>Mouse</kbd><kbd>Side key</kbd></div><svg class="trigger-lab__pointer" viewBox="0 0 26 32" aria-hidden="true"><path d="M3 2L23 20L14 21L10 30Z" fill="var(--metal-950)" stroke="var(--white)" stroke-width="2"/></svg></div> <button class="trigger-lab__seed jp-font" data-seed hidden aria-label="Open emoji favorites"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f44d.png" alt="Emoji bud"></button> <div class="trigger-lab__palette" id="lab-palette-7" data-palette hidden aria-label="Emoji favorites"><div class="trigger-lab__palette-head"><b>EmojiCopy · Favorites</b><button class="trigger-lab__dismiss" data-dismiss aria-label="Close emoji favorites">Esc ×</button></div><div class="trigger-lab__choices"><button class="trigger-lab__choice jp-font" data-pick aria-label="Insert thumbs up"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f44d.png" alt="Thumbs up"></button><button class="trigger-lab__choice jp-font" data-pick aria-label="Insert party popper"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f389.png" alt="Party popper"></button><button class="trigger-lab__choice jp-font" data-pick aria-label="Insert heart"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/2764.png" alt="Heart"></button><button class="trigger-lab__choice jp-font" data-pick aria-label="Insert smile"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f604.png" alt="Smile"></button><button class="trigger-lab__choice jp-font" data-pick aria-label="Insert fire"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f525.png" alt="Fire"></button></div></div> </div></div><div class="trigger-lab__steps"><div class="trigger-lab__step" data-step="1"><strong>1</strong><div><b>Press the side button</b><span>Keep your pointer near the message.</span></div></div><div class="trigger-lab__step" data-step="2"><strong>2</strong><div><b>Favorites meet you</b><span>The palette opens above the pointer.</span></div></div><div class="trigger-lab__step" data-step="3"><strong>3</strong><div><b>Click an emoji</b><span>No trip to the menu bar.</span></div></div></div></div> <div class="trigger-lab__controls"><button class="trigger-lab__play" data-play aria-controls="lab-palette-7" aria-expanded="false">Watch activation</button><button class="trigger-lab__reset" data-reset>Reset</button><span class="trigger-lab__status" role="status"></span></div> <div class="trigger-lab__tradeoff"><span><b>Why it could work</b><br>A short journey for mouse users.</span><span><b>What to test</b><br>Opt-in; never take over an existing mouse assignment silently.</span></div></div>
/* Trigger storyboards: desktop mockups, not native overlays. */
.trigger-lab { display: flex; flex-direction: column; gap: var(--sp-5); font-size: 1.5rem; color: var(--c-text-primary); }
.trigger-lab__layout { display: grid; grid-template-columns: minmax(0, 1fr) 230px; gap: var(--sp-6); align-items: center; }
.trigger-lab__desktop { position: relative; min-width: 0; border-radius: var(--r-lg); overflow: hidden; background: var(--metal-200); border: 1px solid var(--c-border-primary); }
.trigger-lab__bar { display: flex; align-items: center; justify-content: space-between; padding: var(--sp-3) var(--sp-4); background: var(--metal-100); color: var(--metal-700); font-size: 1.2rem; }
.trigger-lab__bar b { color: var(--metal-900); }
.trigger-lab__canvas { position: relative; height: 355px; padding: var(--sp-5); background: linear-gradient(140deg, var(--metal-100), var(--green-400)); }
.trigger-lab__window { position: absolute; inset: 22px 22px 30px; background: var(--white); border: 1px solid var(--metal-200); border-radius: var(--r-md); overflow: hidden; box-shadow: 0 12px 30px color-mix(in srgb, var(--metal-900) 12%, transparent); }
.trigger-lab__title { display: flex; align-items: center; gap: var(--sp-2); padding: var(--sp-3) var(--sp-4); border-bottom: 1px solid var(--metal-100); color: var(--metal-600); font-size: 1.3rem; }
.trigger-lab__lights { display: flex; gap: var(--sp-1); }
.trigger-lab__lights i { width: 8px; height: 8px; border-radius: var(--r-pill); background: var(--metal-300); }
.trigger-lab__conversation { display: flex; flex-direction: column; align-items: flex-start; gap: var(--sp-2); padding: var(--sp-5); color: var(--metal-600); }
.trigger-lab__conversation b { font-size: 1.2rem; }
.trigger-lab__chat { background: var(--metal-100); padding: var(--sp-3) var(--sp-4); border-radius: var(--r-md); color: var(--metal-900); font-size: 1.4rem; }
.trigger-lab__composer { position: absolute; bottom: 28px; left: 18px; right: 18px; border: 2px solid var(--green-700); border-radius: var(--r-md); padding: var(--sp-4); color: var(--metal-900); display: flex; align-items: center; gap: var(--sp-1); min-height: 26px; font-size: 1.6rem; }
.trigger-lab__caret { height: 22px; width: 2px; background: var(--green-800); }
.trigger-lab__result img { width: 25px; height: 25px; vertical-align: middle; }
.trigger-lab__origin { position: absolute; right: 21%; bottom: 105px; width: 160px; height: 120px; pointer-events: none; }
.trigger-lab__pointer { position: absolute; bottom: 0; right: 30px; width: 26px; height: 32px; filter: drop-shadow(0 2px 2px color-mix(in srgb, var(--metal-900) 25%, transparent)); }
.trigger-lab__trail { position: absolute; bottom: 0; right: 5px; width: 88px; height: 88px; color: var(--green-800); opacity: .6; }
.trigger-lab__keys { position: absolute; bottom: 62px; right: 6px; display: flex; gap: var(--sp-1); white-space: nowrap; }
.trigger-lab__keys kbd { display: grid; place-items: center; min-width: 35px; height: 33px; padding: 0 var(--sp-2); font: 600 1.6rem var(--ff-inter), sans-serif; color: var(--metal-900); background: var(--white); border: 1px solid var(--metal-300); border-radius: var(--r-sm); box-shadow: 0 3px 0 var(--metal-200); }
.trigger-lab__seed { position: absolute; right: 23%; bottom: 152px; width: 54px; height: 54px; display: grid; place-items: center; border: 1px solid var(--green-600); border-radius: var(--r-pill); background: var(--white); box-shadow: 0 0 0 6px color-mix(in srgb, var(--green-500) 22%, transparent); cursor: pointer; z-index: 3; }
.trigger-lab__seed img { width: 34px; height: 34px; }
.trigger-lab__palette { position: absolute; z-index: 4; right: 8%; bottom: 145px; width: min(285px, 83%); padding: var(--sp-3); box-sizing: border-box; border-radius: var(--r-lg); background: var(--metal-900); color: var(--white); box-shadow: 0 12px 26px color-mix(in srgb, var(--metal-900) 25%, transparent); }
.trigger-lab__palette::after { content: ''; position: absolute; bottom: -7px; right: 52px; width: 16px; height: 16px; transform: rotate(45deg); background: var(--metal-900); }
.trigger-lab__palette-head { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2); font-size: 1.2rem; padding-bottom: var(--sp-2); }
.trigger-lab__choices { display: flex; justify-content: space-between; gap: var(--sp-1); }
.trigger-lab__choice { display: grid; place-items: center; cursor: pointer; border: 0; border-radius: var(--r-sm); background: var(--metal-800); width: 44px; height: 44px; }
.trigger-lab__choice:hover { background: var(--green-700); }
.trigger-lab__choice img { width: 29px; height: 29px; }
.trigger-lab__dismiss { border: 0; background: transparent; color: var(--metal-300); cursor: pointer; font: inherit; padding: var(--sp-1); }
.trigger-lab__edge { position: absolute; right: 0; bottom: 136px; background: var(--metal-900); color: var(--white); border: 0; border-radius: var(--r-md) 0 0 var(--r-md); padding: var(--sp-3); cursor: pointer; }
.trigger-lab__edge img { width: 24px; height: 24px; }
.trigger-lab__steps { display: flex; flex-direction: column; gap: var(--sp-5); }
.trigger-lab__step { display: grid; grid-template-columns: 28px 1fr; gap: var(--sp-2); align-items: start; line-height: 1.5; }
.trigger-lab__step strong { display: grid; place-items: center; height: 26px; border-radius: var(--r-pill); background: var(--c-surface-tertiary); font-size: 1.2rem; }
.trigger-lab__step b { display: block; }
.trigger-lab__step span { font-size: 1.35rem; color: var(--c-text-secondary); }
.trigger-lab__step.is-current strong { background: var(--green-500); color: var(--green-950); }
.trigger-lab__controls { display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap; }
.trigger-lab__play, .trigger-lab__reset { border: 1px solid var(--c-border-primary); border-radius: var(--r-pill); padding: var(--sp-3) var(--sp-5); font: 600 1.4rem var(--ff-inter), sans-serif; cursor: pointer; }
.trigger-lab__play { color: var(--green-950); background: var(--green-500); }
.trigger-lab__reset { color: var(--c-text-secondary); background: var(--c-surface-primary); }
.trigger-lab__status { font-size: 1.3rem; color: var(--c-text-secondary); flex: 1; min-width: 180px; }
.trigger-lab__tradeoff { display: flex; gap: var(--sp-4); font-size: 1.35rem; line-height: 1.6; color: var(--c-text-secondary); border-top: 1px solid var(--c-border-primary); padding-top: var(--sp-4); }
.trigger-lab [hidden] { display: none !important; }
.trigger-lab button:focus-visible { outline: 3px solid var(--green-700); outline-offset: 3px; }
.trigger-lab[data-phase='gesture'] .trigger-lab__pointer { animation: trigger-orbit 1s ease-in-out; }
.trigger-lab[data-phase='gesture'] .trigger-lab__keys { animation: trigger-press .5s ease-in-out 2; }
.trigger-lab[data-phase='gesture'][data-motion='dwell'] .trigger-lab__pointer { animation: trigger-approach 1s ease-out; }
.trigger-lab[data-phase='gesture'][data-motion='carry'] .trigger-lab__pointer { animation: trigger-carry 1s ease-in-out; }
.trigger-lab[data-phase='done'] .trigger-lab__origin { opacity: .2; }
@keyframes trigger-orbit { 0%,100% { transform: translate(0,0); } 25% { transform: translate(-48px,0); } 50% { transform: translate(-48px,-48px); } 75% { transform: translate(0,-48px); } }
@keyframes trigger-press { 50% { transform: translateY(4px); } }
@keyframes trigger-approach { from { transform: translate(80px,-70px); } to { transform: translate(0,0); } }
@keyframes trigger-carry { from { transform: translate(-150px,-100px); } to { transform: translate(0,0); } }
@media (prefers-reduced-motion: reduce) { .trigger-lab *, .trigger-lab *::after { animation: none !important; } }
@media (max-width: 760px) { .trigger-lab__layout { grid-template-columns: 1fr; } .trigger-lab__steps { flex-direction: row; flex-wrap: wrap; } .trigger-lab__step { flex: 1; min-width: 145px; } .trigger-lab__tradeoff { flex-direction: column; } }
@media (max-width: 450px) { .trigger-lab__window { inset: 15px 8px; } .trigger-lab__composer { left: 10px; right: 10px; padding: var(--sp-3); font-size: 1.4rem; } .trigger-lab__origin { right: 10%; } }
.trigger-intro { font-size: 1.5rem; line-height: 1.6; color: var(--c-text-secondary); margin-bottom: var(--sp-5); }
.trigger-lab__bar img { width: 15px; height: 15px; vertical-align: middle; }
.trigger-lab__keys img { width: 34px; height: 34px; }
.trigger-lab[data-motion='keys'] .trigger-lab__pointer { display: none; }
.trigger-lab[data-kind='4'] .trigger-lab__origin { top: 4px; right: 0; bottom: auto; }
.trigger-lab[data-kind='4'] .trigger-lab__palette { width: min(340px, 90%); right: 5%; bottom: 100px; }
.trigger-lab[data-kind='4'] .trigger-lab__choices { display: grid; grid-template-columns: repeat(5, 1fr); gap: var(--sp-2); }
.trigger-lab[data-kind='8'] .trigger-lab__palette { right: 42px; bottom: 132px; }
.trigger-lab[data-kind='10'] .trigger-lab__origin { right: auto; left: 155px; bottom: 87px; }
.trigger-lab[data-kind='10'] .trigger-lab__seed { right: auto; left: 210px; bottom: 145px; }
.trigger-lab[data-kind='10'] .trigger-lab__palette { left: 72px; right: auto; }
.trigger-lab[data-kind='11'][data-phase='gesture'] .trigger-lab__keys { animation: trigger-carry 1s ease-in-out; }
.trigger-lab__selected { background: var(--green-400); }
.trigger-lab[data-kind='10'] .trigger-lab__origin { left: 88px; }
.trigger-lab[data-kind='10'] .trigger-lab__seed { left: 164px; }
.trigger-lab[data-kind='11'][data-phase='bud'] .trigger-lab__keys,
.trigger-lab[data-kind='11'][data-phase='open'] .trigger-lab__keys { visibility: hidden; }
.trigger-lab[data-kind='4'][data-phase='gesture'] .trigger-lab__pointer { animation: trigger-approach 1s ease-out; }
.trigger-lab[data-kind='4'][data-phase='gesture'] .trigger-lab__keys { outline: 2px solid var(--green-700); outline-offset: 5px; border-radius: var(--r-pill); }
// Isolated storyboards. Buttons simulate native activation; no global input is captured.
document.querySelectorAll('.trigger-lab').forEach(lab => {
const seed = lab.querySelector('[data-seed]')
const palette = lab.querySelector('[data-palette]')
const play = lab.querySelector('[data-play]')
const status = lab.querySelector('[role="status"]')
let timer
const phase = (name, step) => {
lab.dataset.phase = name
lab.querySelectorAll('[data-step]').forEach(item => item.classList.toggle('is-current', Number(item.dataset.step) === step))
}
const open = () => {
clearTimeout(timer)
seed.hidden = true
palette.hidden = false
play.setAttribute('aria-expanded', 'true')
phase('open', 2)
status.textContent = 'Click an emoji to insert it in this example. Escape cancels.'
palette.querySelector('[data-pick]').focus()
}
const reset = () => {
clearTimeout(timer)
seed.hidden = true
palette.hidden = true
play.setAttribute('aria-expanded', 'false')
const result = lab.querySelector('[data-result]')
result.textContent = result.dataset.initial || ''
phase('ready', 1)
status.textContent = 'Motion demo only · click “Watch activation” to begin.'
}
const cancel = () => {
clearTimeout(timer)
seed.hidden = true
palette.hidden = true
play.setAttribute('aria-expanded', 'false')
phase('ready', 1)
status.textContent = 'Dismissed. The message was not changed.'
play.focus()
}
play.addEventListener('click', () => {
reset()
phase('gesture', 1)
status.textContent = lab.dataset.action
timer = setTimeout(() => {
if (lab.dataset.bud === 'true') {
seed.hidden = false
phase('bud', 2)
status.textContent = 'The emoji bud is here. Click it to unfold favorites.'
seed.focus()
} else open()
}, 1050)
})
seed.addEventListener('click', open)
lab.querySelector('[data-reset]').addEventListener('click', reset)
lab.querySelector('[data-dismiss]').addEventListener('click', cancel)
lab.querySelector('[data-edge]')?.addEventListener('click', open)
lab.querySelectorAll('[data-pick]').forEach(button => button.addEventListener('click', () => {
lab.querySelector('[data-result]').replaceChildren(button.querySelector('img').cloneNode())
seed.hidden = true
palette.hidden = true
play.setAttribute('aria-expanded', 'false')
phase('done', 3)
status.textContent = 'Inserted here. In the proposed Mac flow, you keep typing in your app.'
play.focus()
}))
lab.addEventListener('keydown', event => {
if (event.key === 'Escape') { event.stopPropagation(); cancel() }
})
reset()
})
CONCEPT 7: Palette at the pointer Press your assigned mouse side button Opt-in; never take over an existing mouse assignment silently. This browser storyboard simulates the input using Watch activation; it does not detect a system-wide gesture or insert into another app. No claim of an unprecedented invention or native feasibility is made. Native sequence: capture verified editable target → recognize explicit activation → reveal bud or palette without modifying text → explicit emoji selection → revalidate target → insert, with one undo step. If no target is available, offer Copy explicitly. Cancel never changes text. Respect existing app controls, secure input, display bounds, and the user's opt-in setting. For buds: reveal beside the active pointer/caret, retain a generous travel corridor between pointer and bud, dismiss on continued typing, focus change, Escape, or movement away. Never insert on hover. Test the reveal delay and false positives with real users. Pocket mode is entered only via Carry in EmojiCopy. The carried emoji remains a proposal until a user confirms insertion. Clear after one successful insertion by default. Integration: mount this HTML once per demo, include the complete CSS and JS below. Source IDs must remain unique. Native implementation is a separate prototype. The browser mock uses PNGs; actual insertion must use the selected Unicode sequence.
<div class="trigger-lab" data-kind="8" data-motion="dwell" data-bud="false" data-action="Click the EmojiCopy tab on the screen edge"> <div class="trigger-lab__layout"><div class="trigger-lab__desktop"> <div class="trigger-lab__bar"><b>Messages</b><span>EmojiCopy <span class="jp-font"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f604.png" alt="Menu bar icon"></span></span></div> <div class="trigger-lab__canvas"><div class="trigger-lab__window"><div class="trigger-lab__title"><span class="trigger-lab__lights"><i></i><i></i><i></i></span> Team chat</div><div class="trigger-lab__conversation"><b>Casey</b><div class="trigger-lab__chat">The new design is ready!</div></div><div class="trigger-lab__composer">That looks great <span class="trigger-lab__result jp-font" data-result></span><span class="trigger-lab__caret"></span></div></div> <div class="trigger-lab__origin"><svg class="trigger-lab__pointer" viewBox="0 0 26 32" aria-hidden="true"><path d="M3 2L23 20L14 21L10 30Z" fill="var(--metal-950)" stroke="var(--white)" stroke-width="2"/></svg></div> <button class="trigger-lab__seed jp-font" data-seed hidden aria-label="Open emoji favorites"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f44d.png" alt="Emoji bud"></button> <div class="trigger-lab__palette" id="lab-palette-8" data-palette hidden aria-label="Emoji favorites"><div class="trigger-lab__palette-head"><b>EmojiCopy · Favorites</b><button class="trigger-lab__dismiss" data-dismiss aria-label="Close emoji favorites">Esc ×</button></div><div class="trigger-lab__choices"><button class="trigger-lab__choice jp-font" data-pick aria-label="Insert thumbs up"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f44d.png" alt="Thumbs up"></button><button class="trigger-lab__choice jp-font" data-pick aria-label="Insert party popper"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f389.png" alt="Party popper"></button><button class="trigger-lab__choice jp-font" data-pick aria-label="Insert heart"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/2764.png" alt="Heart"></button><button class="trigger-lab__choice jp-font" data-pick aria-label="Insert smile"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f604.png" alt="Smile"></button><button class="trigger-lab__choice jp-font" data-pick aria-label="Insert fire"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f525.png" alt="Fire"></button></div></div> <button class="trigger-lab__edge jp-font" data-edge aria-label="Open from edge tab"><img class="" src="https://cdn.jsdelivr.net/joypixels/assets/11.0/png/unicode/64/1f604.png" alt="EmojiCopy"></button> </div></div><div class="trigger-lab__steps"><div class="trigger-lab__step" data-step="1"><strong>1</strong><div><b>Click the edge tab</b><span>A small, always-visible entry point.</span></div></div><div class="trigger-lab__step" data-step="2"><strong>2</strong><div><b>Favorites unfold</b><span>The palette opens inward from the edge.</span></div></div><div class="trigger-lab__step" data-step="3"><strong>3</strong><div><b>Click an emoji</b><span>Insert in the last verified text field.</span></div></div></div></div> <div class="trigger-lab__controls"><button class="trigger-lab__play" data-play aria-controls="lab-palette-8" aria-expanded="false">Watch activation</button><button class="trigger-lab__reset" data-reset>Reset</button><span class="trigger-lab__status" role="status"></span></div> <div class="trigger-lab__tradeoff"><span><b>Why it could work</b><br>Easy to find even if shortcuts are forgotten.</span><span><b>What to test</b><br>It occupies screen space; people must be able to hide or move it.</span></div></div>
/* Trigger storyboards: desktop mockups, not native overlays. */
.trigger-lab { display: flex; flex-direction: column; gap: var(--sp-5); font-size: 1.5rem; color: var(--c-text-primary); }
.trigger-lab__layout { display: grid; grid-template-columns: minmax(0, 1fr) 230px; gap: var(--sp-6); align-items: center; }
.trigger-lab__desktop { position: relative; min-width: 0; border-radius: var(--r-lg); overflow: hidden; background: var(--metal-200); border: 1px solid var(--c-border-primary); }
.trigger-lab__bar { display: flex; align-items: center; justify-content: space-between; padding: var(--sp-3) var(--sp-4); background: var(--metal-100); color: var(--metal-700); font-size: 1.2rem; }
.trigger-lab__bar b { color: var(--metal-900); }
.trigger-lab__canvas { position: relative; height: 355px; padding: var(--sp-5); background: linear-gradient(140deg, var(--metal-100), var(--green-400)); }
.trigger-lab__window { position: absolute; inset: 22px 22px 30px; background: var(--white); border: 1px solid var(--metal-200); border-radius: var(--r-md); overflow: hidden; box-shadow: 0 12px 30px color-mix(in srgb, var(--metal-900) 12%, transparent); }
.trigger-lab__title { display: flex; align-items: center; gap: var(--sp-2); padding: var(--sp-3) var(--sp-4); border-bottom: 1px solid var(--metal-100); color: var(--metal-600); font-size: 1.3rem; }
.trigger-lab__lights { display: flex; gap: var(--sp-1); }
.trigger-lab__lights i { width: 8px; height: 8px; border-radius: var(--r-pill); background: var(--metal-300); }
.trigger-lab__conversation { display: flex; flex-direction: column; align-items: flex-start; gap: var(--sp-2); padding: var(--sp-5); color: var(--metal-600); }
.trigger-lab__conversation b { font-size: 1.2rem; }
.trigger-lab__chat { background: var(--metal-100); padding: var(--sp-3) var(--sp-4); border-radius: var(--r-md); color: var(--metal-900); font-size: 1.4rem; }
.trigger-lab__composer { position: absolute; bottom: 28px; left: 18px; right: 18px; border: 2px solid var(--green-700); border-radius: var(--r-md); padding: var(--sp-4); color: var(--metal-900); display: flex; align-items: center; gap: var(--sp-1); min-height: 26px; font-size: 1.6rem; }
.trigger-lab__caret { height: 22px; width: 2px; background: var(--green-800); }
.trigger-lab__result img { width: 25px; height: 25px; vertical-align: middle; }
.trigger-lab__origin { position: absolute; right: 21%; bottom: 105px; width: 160px; height: 120px; pointer-events: none; }
.trigger-lab__pointer { position: absolute; bottom: 0; right: 30px; width: 26px; height: 32px; filter: drop-shadow(0 2px 2px color-mix(in srgb, var(--metal-900) 25%, transparent)); }
.trigger-lab__trail { position: absolute; bottom: 0; right: 5px; width: 88px; height: 88px; color: var(--green-800); opacity: .6; }
.trigger-lab__keys { position: absolute; bottom: 62px; right: 6px; display: flex; gap: var(--sp-1); white-space: nowrap; }
.trigger-lab__keys kbd { display: grid; place-items: center; min-width: 35px; height: 33px; padding: 0 var(--sp-2); font: 600 1.6rem var(--ff-inter), sans-serif; color: var(--metal-900); background: var(--white); border: 1px solid var(--metal-300); border-radius: var(--r-sm); box-shadow: 0 3px 0 var(--metal-200); }
.trigger-lab__seed { position: absolute; right: 23%; bottom: 152px; width: 54px; height: 54px; display: grid; place-items: center; border: 1px solid var(--green-600); border-radius: var(--r-pill); background: var(--white); box-shadow: 0 0 0 6px color-mix(in srgb, var(--green-500) 22%, transparent); cursor: pointer; z-index: 3; }
.trigger-lab__seed img { width: 34px; height: 34px; }
.trigger-lab__palette { position: absolute; z-index: 4; right: 8%; bottom: 145px; width: min(285px, 83%); padding: var(--sp-3); box-sizing: border-box; border-radius: var(--r-lg); background: var(--metal-900); color: var(--white); box-shadow: 0 12px 26px color-mix(in srgb, var(--metal-900) 25%, transparent); }
.trigger-lab__palette::after { content: ''; position: absolute; bottom: -7px; right: 52px; width: 16px; height: 16px; transform: rotate(45deg); background: var(--metal-900); }
.trigger-lab__palette-head { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2); font-size: 1.2rem; padding-bottom: var(--sp-2); }
.trigger-lab__choices { display: flex; justify-content: space-between; gap: var(--sp-1); }
.trigger-lab__choice { display: grid; place-items: center; cursor: pointer; border: 0; border-radius: var(--r-sm); background: var(--metal-800); width: 44px; height: 44px; }
.trigger-lab__choice:hover { background: var(--green-700); }
.trigger-lab__choice img { width: 29px; height: 29px; }
.trigger-lab__dismiss { border: 0; background: transparent; color: var(--metal-300); cursor: pointer; font: inherit; padding: var(--sp-1); }
.trigger-lab__edge { position: absolute; right: 0; bottom: 136px; background: var(--metal-900); color: var(--white); border: 0; border-radius: var(--r-md) 0 0 var(--r-md); padding: var(--sp-3); cursor: pointer; }
.trigger-lab__edge img { width: 24px; height: 24px; }
.trigger-lab__steps { display: flex; flex-direction: column; gap: var(--sp-5); }
.trigger-lab__step { display: grid; grid-template-columns: 28px 1fr; gap: var(--sp-2); align-items: start; line-height: 1.5; }
.trigger-lab__step strong { display: grid; place-items: center; height: 26px; border-radius: var(--r-pill); background: var(--c-surface-tertiary); font-size: 1.2rem; }
.trigger-lab__step b { display: block; }
.trigger-lab__step span { font-size: 1.35rem; color: var(--c-text-secondary); }
.trigger-lab__step.is-current strong { background: var(--green-500); color: var(--green-950); }
.trigger-lab__controls { display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap; }
.trigger-lab__play, .trigger-lab__reset { border: 1px solid var(--c-border-primary); border-radius: var(--r-pill); padding: var(--sp-3) var(--sp-5); font: 600 1.4rem var(--ff-inter), sans-serif; cursor: pointer; }
.trigger-lab__play { color: var(--green-950); background: var(--green-500); }
.trigger-lab__reset { color: var(--c-text-secondary); background: var(--c-surface-primary); }
.trigger-lab__status { font-size: 1.3rem; color: var(--c-text-secondary); flex: 1; min-width: 180px; }
.trigger-lab__tradeoff { display: flex; gap: var(--sp-4); font-size: 1.35rem; line-height: 1.6; color: var(--c-text-secondary); border-top: 1px solid var(--c-border-primary); padding-top: var(--sp-4); }
.trigger-lab [hidden] { display: none !important; }
.trigger-lab button:focus-visible { outline: 3px solid var(--green-700); outline-offset: 3px; }
.trigger-lab[data-phase='gesture'] .trigger-lab__pointer { animation: trigger-orbit 1s ease-in-out; }
.trigger-lab[data-phase='gesture'] .trigger-lab__keys { animation: trigger-press .5s ease-in-out 2; }
.trigger-lab[data-phase='gesture'][data-motion='dwell'] .trigger-lab__pointer { animation: trigger-approach 1s ease-out; }
.trigger-lab[data-phase='gesture'][data-motion='carry'] .trigger-lab__pointer { animation: trigger-carry 1s ease-in-out; }
.trigger-lab[data-phase='done'] .trigger-lab__origin { opacity: .2; }
@keyframes trigger-orbit { 0%,100% { transform: translate(0,0); } 25% { transform: translate(-48px,0); } 50% { transform: translate(-48px,-48px); } 75% { transform: translate(0,-48px); } }
@keyframes trigger-press { 50% { transform: translateY(4px); } }
@keyframes trigger-approach { from { transform: translate(80px,-70px); } to { transform: translate(0,0); } }
@keyframes trigger-carry { from { transform: translate(-150px,-100px); } to { transform: translate(0,0); } }
@media (prefers-reduced-motion: reduce) { .trigger-lab *, .trigger-lab *::after { animation: none !important; } }
@media (max-width: 760px) { .trigger-lab__layout { grid-template-columns: 1fr; } .trigger-lab__steps { flex-direction: row; flex-wrap: wrap; } .trigger-lab__step { flex: 1; min-width: 145px; } .trigger-lab__tradeoff { flex-direction: column; } }
@media (max-width: 450px) { .trigger-lab__window { inset: 15px 8px; } .trigger-lab__composer { left: 10px; right: 10px; padding: var(--sp-3); font-size: 1.4rem; } .trigger-lab__origin { right: 10%; } }
.trigger-intro { font-size: 1.5rem; line-height: 1.6; color: var(--c-text-secondary); margin-bottom: var(--sp-5); }
.trigger-lab__bar img { width: 15px; height: 15px; vertical-align: middle; }
.trigger-lab__keys img { width: 34px; height: 34px; }
.trigger-lab[data-motion='keys'] .trigger-lab__pointer { display: none; }
.trigger-lab[data-kind='4'] .trigger-lab__origin { top: 4px; right: 0; bottom: auto; }
.trigger-lab[data-kind='4'] .trigger-lab__palette { width: min(340px, 90%); right: 5%; bottom: 100px; }
.trigger-lab[data-kind='4'] .trigger-lab__choices { display: grid; grid-template-columns: repeat(5, 1fr); gap: var(--sp-2); }
.trigger-lab[data-kind='8'] .trigger-lab__palette { right: 42px; bottom: 132px; }
.trigger-lab[data-kind='10'] .trigger-lab__origin { right: auto; left: 155px; bottom: 87px; }
.trigger-lab[data-kind='10'] .trigger-lab__seed { right: auto; left: 210px; bottom: 145px; }
.trigger-lab[data-kind='10'] .trigger-lab__palette { left: 72px; right: auto; }
.trigger-lab[data-kind='11'][data-phase='gesture'] .trigger-lab__keys { animation: trigger-carry 1s ease-in-out; }
.trigger-lab__selected { background: var(--green-400); }
.trigger-lab[data-kind='10'] .trigger-lab__origin { left: 88px; }
.trigger-lab[data-kind='10'] .trigger-lab__seed { left: 164px; }
.trigger-lab[data-kind='11'][data-phase='bud'] .trigger-lab__keys,
.trigger-lab[data-kind='11'][data-phase='open'] .trigger-lab__keys { visibility: hidden; }
.trigger-lab[data-kind='4'][data-phase='gesture'] .trigger-lab__pointer { animation: trigger-approach 1s ease-out; }
.trigger-lab[data-kind='4'][data-phase='gesture'] .trigger-lab__keys { outline: 2px solid var(--green-700); outline-offset: 5px; border-radius: var(--r-pill); }
// Isolated storyboards. Buttons simulate native activation; no global input is captured.
document.querySelectorAll('.trigger-lab').forEach(lab => {
const seed = lab.querySelector('[data-seed]')
const palette = lab.querySelector('[data-palette]')
const play = lab.querySelector('[data-play]')
const status = lab.querySelector('[role="status"]')
let timer
const phase = (name, step) => {
lab.dataset.phase = name
lab.querySelectorAll('[data-step]').forEach(item => item.classList.toggle('is-current', Number(item.dataset.step) === step))
}
const open = () => {
clearTimeout(timer)
seed.hidden = true
palette.hidden = false
play.setAttribute('aria-expanded', 'true')
phase('open', 2)
status.textContent = 'Click an emoji to insert it in this example. Escape cancels.'
palette.querySelector('[data-pick]').focus()
}
const reset = () => {
clearTimeout(timer)
seed.hidden = true
palette.hidden = true
play.setAttribute('aria-expanded', 'false')
const result = lab.querySelector('[data-result]')
result.textContent = result.dataset.initial || ''
phase('ready', 1)
status.textContent = 'Motion demo only · click “Watch activation” to begin.'
}
const cancel = () => {
clearTimeout(timer)
seed.hidden = true
palette.hidden = true
play.setAttribute('aria-expanded', 'false')
phase('ready', 1)
status.textContent = 'Dismissed. The message was not changed.'
play.focus()
}
play.addEventListener('click', () => {
reset()
phase('gesture', 1)
status.textContent = lab.dataset.action
timer = setTimeout(() => {
if (lab.dataset.bud === 'true') {
seed.hidden = false
phase('bud', 2)
status.textContent = 'The emoji bud is here. Click it to unfold favorites.'
seed.focus()
} else open()
}, 1050)
})
seed.addEventListener('click', open)
lab.querySelector('[data-reset]').addEventListener('click', reset)
lab.querySelector('[data-dismiss]').addEventListener('click', cancel)
lab.querySelector('[data-edge]')?.addEventListener('click', open)
lab.querySelectorAll('[data-pick]').forEach(button => button.addEventListener('click', () => {
lab.querySelector('[data-result]').replaceChildren(button.querySelector('img').cloneNode())
seed.hidden = true
palette.hidden = true
play.setAttribute('aria-expanded', 'false')
phase('done', 3)
status.textContent = 'Inserted here. In the proposed Mac flow, you keep typing in your app.'
play.focus()
}))
lab.addEventListener('keydown', event => {
if (event.key === 'Escape') { event.stopPropagation(); cancel() }
})
reset()
})
CONCEPT 8: A small edge tab Click the EmojiCopy tab on the screen edge It occupies screen space; people must be able to hide or move it. This browser storyboard simulates the input using Watch activation; it does not detect a system-wide gesture or insert into another app. No claim of an unprecedented invention or native feasibility is made. Native sequence: capture verified editable target → recognize explicit activation → reveal bud or palette without modifying text → explicit emoji selection → revalidate target → insert, with one undo step. If no target is available, offer Copy explicitly. Cancel never changes text. Respect existing app controls, secure input, display bounds, and the user's opt-in setting. For buds: reveal beside the active pointer/caret, retain a generous travel corridor between pointer and bud, dismiss on continued typing, focus change, Escape, or movement away. Never insert on hover. Test the reveal delay and false positives with real users. Pocket mode is entered only via Carry in EmojiCopy. The carried emoji remains a proposal until a user confirms insertion. Clear after one successful insertion by default. Integration: mount this HTML once per demo, include the complete CSS and JS below. Source IDs must remain unique. Native implementation is a separate prototype. The browser mock uses PNGs; actual insertion must use the selected Unicode sequence.
ColonRowPanel · new — non-activating NSPanel, 320 × 46. Not drawn in this state.TriggerObserver · new (Rust) — listen-only keystroke buffer that arms a session..row single line of fixed cells · flex, because the cell count varies with the match count (R10)// TriggerObserver — listen-only. NSEvent global monitor, never an event tap,
// so we are never on anyone's keystroke path and can neither consume nor delay.
// Accessibility is already granted (input.rs request_accessibility_permission),
// and it is a superset of Input Monitoring for listen-only observation, so this
// adds ZERO new permission prompts.
const ARMING_PREFIX: &[char] = &['(', '[', '{', '"', '\''];
enum Session {
Idle,
Armed { query: String }, // colon seen. NOTHING is drawn yet.
Drawing { query: String }, // >= 2 chars AND >= 1 match
}
fn on_key(&mut self, ch: char) {
// Self-event immunity. Every synthetic event we post comes straight back
// through this monitor. Tag ours with CGEventSourceSetUserData and drop them.
if event_is_ours(&event) { return }
match self.session {
Session::Idle if ch == ':' && self.arms() => {
self.session = Session::Armed { query: String::new() }
}
_ => {}
}
}
// A colon arms only when the character before it in OUR OWN rolling buffer is
// nothing at all (start of a typing run), whitespace, or an opening bracket.
// The buffer is ours because we cannot read the host app's text.
fn arms(&self) -> bool {
match self.buffer.chars().last() {
None => true,
Some(c) if c.is_whitespace() => true,
Some(c) if ARMING_PREFIX.contains(&c) => true,
_ => false,
}
}
/* The trigger text lives in the USER'S document, not in our panel. We never
draw it. This class only exists so the mockup can show what the user sees. */
.trig { color: #1D1C1D; }
/* the host app's own caret, not ours */
.caret {
display: inline-block;
width: 1.5px;
height: 1.1em;
background: #1D1C1D;
vertical-align: -.22em;
margin: 0 1px;
}
.caret--live { animation: ec-blink 1.1s steps(1) infinite; }
@keyframes ec-blink { 50% { opacity: 0; } }
ColonRowPanel · new — 320 × 46 non-activating NSPanel, eight 34pt cells, directional beak.compute_mini_window_placement · src-tauri/src/window.rs — the four pointer classes and work-area clamping, generalized to take a caret rect instead of a cursor point.CaretAnchor · new (Rust) — three-tier caret resolution, returns None rather than guessing..row · flex row, gap 2px, because the cell count is variable and the trailing keycap must sit at the end (R10, R11)// A NEW panel. Do NOT convert the mini window: it is 405x335, calls set_focus()
// on show, and hides on Focused(false), so it steals focus and would dismiss
// itself the instant it stopped being key. Verified in src/window.rs and src/lib.rs.
use tauri_nspanel::{PanelBuilder, PanelLevel, StyleMask};
let panel = PanelBuilder::new(app, "colon-row")
// nonactivating MUST be set at creation. Flipping it after init leaves
// AppKit and the WindowServer disagreeing: the window looks focused and
// receives no keys.
.style_mask(StyleMask::empty().nonactivating_panel())
.level(PanelLevel::Status)
.collection_behavior(
CollectionBehavior::new()
.can_join_all_spaces()
.stationary()
.full_screen_auxiliary(),
)
.build()?
// Without this the panel hides itself instantly, because our app is never active.
panel.set_hides_on_deactivate(false);
// The host app never loses focus at any point in the interaction.
panel.set_can_become_key_window(false);
// --- caret anchor, three tiers -------------------------------------------
// Runs OFF the UI thread with a low messaging timeout: every AX call is a
// cross-process message that blocks until the target answers, and the system
// default is around six seconds.
fn caret_rect() -> Option<CGRect> {
let system = AXUIElementCreateSystemWide();
AXUIElementSetMessagingTimeout(system, 0.2);
let focused = copy_attr(system, kAXFocusedUIElementAttribute)?;
// Tier 1. Ask for a ONE-character range at the caret index, never a
// zero-length range: a zero-length range returns kAXErrorNoValue in most
// macOS text fields, which is exactly the bare-caret case we care about.
let range = copy_attr(focused, kAXSelectedTextRangeAttribute)?;
if let Some(r) = copy_param_attr(focused, kAXBoundsForRangeParameterizedAttribute,
one_char_at(range)) {
return Some(flip_to_appkit(r)) // AX is top-left origin, AppKit is bottom-left
}
// Tier 2. The focused element's own frame. Anchor to its bottom-left.
if let Some(f) = copy_attr(focused, kAXFrameAttribute) {
return Some(flip_to_appkit(f))
}
// Tier 3. DO NOT DRAW. Never fall back to the mouse position: the hands are
// on the keyboard during a colon trigger and the pointer may be hundreds of
// points from where the user is looking.
None
}
// Never poll the AX tree. Drive updates from an AXObserver on
// kAXFocusedUIElementChangedNotification against the frontmost pid, re-registered
// on NSWorkspace didActivateApplicationNotification because observers are per-pid.
/* 320 x 46 points. Eight 34pt cells plus padding. No text of its own: the
query is already visible in the user's document, so repeating it is noise. */
.row {
position: absolute;
width: 320px;
padding: 5px;
display: flex;
align-items: center;
gap: 2px;
background: #1F2129;
border: 1px solid #353841;
border-radius: 12px;
box-shadow: 0 14px 38px rgba(0, 0, 0, .46);
}
.row__cell {
flex: none;
width: 34px;
height: 34px;
border-radius: 8px;
display: grid;
place-items: center;
}
.row__cell img { width: 22px; height: 22px; display: block; }
.row__cell--on { background: var(--green-600); }
/* trailing cell prints the LIVE full-picker shortcut, sourced from the
registration that actually succeeded, never a hardcoded string */
.row__more {
flex: none;
width: 34px;
height: 34px;
border-radius: 8px;
display: grid;
place-items: center;
background: #2A2D36;
}
.row__div { flex: none; width: 1px; height: 20px; margin: 0 3px; background: #343843; }
/* same beak artwork as the mini window, at 22 x 16 */
.row__beak {
position: absolute;
left: 22px;
top: -11px;
width: 22px;
height: 16px;
background: #1F2129;
transform: scale(-1, -1);
mask-image: var(--beak-svg);
mask-repeat: no-repeat;
mask-position: center;
mask-size: 100% 100%;
}
/* beak flips below the panel when the row is anchored above the caret */
.row--above .row__beak { top: auto; bottom: -11px; transform: none; }
TriggerObserver · new (Rust) — owns the two-phase state machine shown across these four frames..strip-set--4 four equal columns · grid, because the frames must stay column-aligned as a filmstrip (R10)// Two conditions, both required, evaluated on every keystroke while armed.
// This is the whole anti-flicker design and it is worth stating as one function.
fn should_draw(&self) -> bool {
self.query.chars().count() >= 2 && !self.matches.is_empty()
}
fn on_key_while_armed(&mut self, ch: char) {
if ch.is_whitespace() { return self.end() } // space always ends it
if !QUERY_ALPHABET.contains(ch) { return self.end() } // anything exotic ends it
self.query.push(ch);
self.matches = search(&self.query, LIMIT_8);
if self.should_draw() {
self.panel.show_at(caret_rect());
} else {
// Note this is a HIDE, not an empty state. There is no "no results"
// panel anywhere in this feature. If a query that was drawing stops
// matching, the panel goes and the session ends: re-drawing on a later
// keystroke would be the flicker we designed the two-character floor
// to avoid.
self.panel.hide();
if self.query.chars().count() >= 2 { self.end() }
}
}
/* Filmstrip. Grid so the four frames stay column-aligned regardless of how
much caption each one carries. Sibling spacing is the parent gap (R11). */
.strip-set {
display: grid;
gap: var(--sp-3);
width: 100%;
}
.strip-set--4 { grid-template-columns: repeat(4, 1fr); }
@media (max-width: 900px) {
.strip-set--4 { grid-template-columns: repeat(2, 1fr); }
}
.cell {
display: flex;
flex-direction: column;
gap: var(--sp-2);
}
.cell__mock {
position: relative;
min-height: 92px;
padding: 12px;
overflow: hidden;
border: 1px solid var(--c-border-primary);
border-radius: var(--r-sm);
background: var(--white);
color: #1D1C1D;
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', var(--ff-inter), sans-serif;
font-size: 12px;
}
.cell__cap {
color: var(--c-text-secondary);
font-size: 1.25rem;
line-height: 1.5;
}
.cell__cap b { color: var(--c-text-primary); font-weight: 700; }
/* flat selectors, not &__el nesting: this block is browser-parsed (R6) */
.cell__cap--good b { color: var(--green-900); }
.cell__cap--bad b { color: var(--destructive-400); }
insert_via_keystroke · src-tauri/src/input.rs — reused for the CGEvent post only. The activate call and the 150ms sleep are deleted from this path.TextSwap · new (Rust) — N+2 backspaces, then the emoji, with abandon-on-interruption.// COMMIT PATH 1 of 3, and the default. Typing the second colon on an exact
// shortname match swaps the run. This is the Slack, Discord and GitHub habit
// millions of people already have in their fingers.
//
// It is the ONLY auto-fire the design permits, because the user typed the
// terminator themselves on an exact match rather than us guessing at a prefix.
// Crucially it needs no key interception at all, so a listen-only observer is
// sufficient and we are never on anyone's keystroke path.
fn commit_closing_colon(&mut self, emoji: &str, shortcode_len: usize) {
// The user SEES `:tada:` land in their own document before it is removed.
// Embrace this. It is exactly what Slack does and it is what makes the
// trigger legible. Taking key status so the query never lands would buy a
// focus flicker on every single colon, which is a far worse trade.
// ABANDON ON INTERRUPTION. The most common shape in the language is
// `:tada: ship it` — closing colon, space, more words. If a real keystroke
// has already arrived by the time the backspaces are due, abandon
// PERMANENTLY: leave `:tada:` in the text as literal characters and never
// retry. That turns the one dangerous failure, a deletion racing the user's
// next word and eating it, into the same silent no-op a no-match produces.
if self.real_key_arrived_since(self.commit_seq) { return }
for _ in 0..(shortcode_len + 2) {
post_key(KEYCODE_DELETE, tagged_as_ours()); // keycode 51
spin(INTER_KEY_GAP); // some apps coalesce or drop fast synthetic events
}
insert(emoji);
}
1. CLOSING COLON — default, free. Listen-only. No key is ever swallowed. Ships on. 2. CLICK — default, free. A non-activating panel receives mouse events without becoming key, so this takes no focus and costs nothing. Ships on. 3. RETURN, TAB AND ARROWS — opt-in switch, off by default. This is the only path that requires SWALLOWING a key, and a global NSEvent monitor cannot swallow anything. It needs an active CGEventTap at kCGEventTapOptionDefault, which puts the app synchronously on every keystroke system-wide, plus a kCGEventTapDisabledByTimeout re-enable in the callback because macOS silently kills slow taps across sleep and wake. Grammarly measured that class of per-keystroke work at a 9 percent swing in users disabling their product. So it is a switch with a named cost, and when on, the tap swallows Return, Tab and arrows ONLY while the row is visible and passes every other event through untouched.
:tada: ship it.
TextSwap · new (Rust) — the abandon rule lives here and it is the safety property of the whole feature.// THE ONE DANGEROUS FAILURE in this entire feature is a synthetic deletion
// racing the user's next word. Everything else fails silently; this one
// destroys text the user wrote.
//
// The fix is not to make the backspaces faster. It is to give up.
struct Swap {
commit_seq: u64, // the observer's keystroke counter at commit time
}
fn real_key_arrived_since(&self, seq: u64) -> bool {
// Counts only keys the USER pressed. Our own synthetic events are tagged
// with CGEventSourceSetUserData and never increment this counter.
self.user_key_seq > seq
}
// Called on the swap task, which is off the observer thread:
if self.real_key_arrived_since(self.commit_seq) {
// Abandon PERMANENTLY. Do not retry, do not queue, do not attempt a
// partial swap. `:tada:` stays in the document as literal characters.
return
}
// This also hands the user a working escape that needs no instruction:
// keep typing.
OffList · new — the pre-populated per-app suppression list, labelled with real app names..spec one column, 1px gap over a border-coloured ground · grid, because the hairlines are gaps rather than borders (R10)// LAYER 1 — the arming rule. Kills every specimen above without drawing
// anything, so the user never sees a flicker. See card A.
// LAYER 2 — shipped off, pre-populated, and never silent about it.
const OFF_BY_DEFAULT: &[&str] = &[
// terminals
"com.apple.Terminal", "com.googlecode.iterm2", "dev.warp.Warp-Stable",
"io.alacritty", "net.kovidgoyal.kitty", "com.github.wez.wezterm",
// code editors and IDEs
"com.apple.dt.Xcode", "com.microsoft.VSCode", "com.jetbrains.intellij",
"com.sublimetext.4", "com.panic.Nova",
// password managers
"com.1password.1password", "com.bitwarden.desktop",
"com.lastpass.LastPass", "keybase.Electron",
// virtual machines
"com.parallels.desktop.console", "com.vmware.fusion",
"net.kovidgoyal.utm", "org.virtualbox.app.VirtualBox",
// apps that already convert colon shortcodes themselves
"com.tinyspeck.slackmacgap", "com.hnc.Discord",
];
// Every row is labelled with the app's localizedName, NEVER a bundle id and
// never a process name. Rocket's list shows "Electron" for VS Code and "idea"
// for IntelliJ, and its documented fix is to hand-edit the row.
// window.rs:73 already holds the NSRunningApplication object and reads only
// bundleIdentifier. Add one msg_send for localizedName.
// LAYER 3 — fully dormant, no buffer kept at all.
fn must_stand_down() -> bool {
// Secure input is SYSTEM-WIDE, not per-field. It survives the enabling
// process going to the background, and password managers routinely leave
// it stuck on for a whole session. There is no API to clear another
// process's secure input, so we detect, stand down, and say so.
IsSecureEventInputEnabled()
|| focused_subrole() == Some(kAXSecureTextFieldSubrole)
}
:tada: works with nothing drawn.
CaretAnchor · new (Rust) — three tiers, and tier 3 is "do not draw" rather than a guess.frontmost_app_bundle_id · src-tauri/src/window.rs:61 — already shipping. Add localizedName beside it.// THE CARET IS THE HARD PART AND THE FALLBACK IS A DESIGN DECISION, NOT A
// DEGRADATION. Getting this wrong is the single most damaging failure the
// whole category has.
match caret_rect() {
// TIER 1 lands in native AppKit apps: Notes, Mail, Messages, TextEdit,
// Pages, Craft, Bear.
// TIER 2 is the focused element's own frame.
Some(rect) => self.panel.show_at(rect),
// TIER 3. Draw NOTHING. Do not fall back to the mouse position.
// Rocket and Input Source Pro both fall back to a window default. During a
// colon trigger the hands are on the keyboard and the pointer may be
// hundreds of points from where the user is looking, so a row appearing
// near the mouse is worse than no row at all.
//
// The trigger degrades to blind exact-match expansion: `:tada:` still
// swaps, nothing is ever drawn, and the tray icon carries a quiet state so
// a curious user has somewhere to find out why. This is what espanso does
// and it needs no geometry.
None => self.set_blind_mode(true),
}
// Coverage, stated honestly rather than estimated optimistically:
// Tier 1 MISSES in most Chromium and Electron content, which includes
// browsers, Notion, Linear desktop and Figma. An Apple forum report has
// Electron returning XOrigin always 0 and YOrigin equal to the screen height.
TrayIconBuilder · src-tauri/src/lib.rs:145 — the tray already exists. This adds a title beside the icon and nothing else.ReachDetector · new (Rust) — reuses the trigger's own rolling buffer. No new observer..mac__tray icon then title · flex, because the title appears and disappears and the icon must not move (R10)// This is the closest thing in the set to shake-to-find-cursor. The user is
// already performing the micro-behavior out of a feeling ("I want an emoji here
// and this is how I know to get one"), and the app reads that exact act as
// intent and answers it. Nothing to learn, nothing to dismiss.
//
// It reads the WANT, not the absence of the want, so it can never shade into
// "you have not used us lately".
const EMOTICONS: &[&str] = &[":)", ":-)", ":(", ":D", ":P", "<3", ";)", ":'(", "xD"];
fn on_buffer_change(&mut self, buf: &str) {
// Same rolling buffer the colon trigger already keeps. No second observer,
// no extra per-keystroke cost.
let reached = EMOTICONS.iter().any(|e| buf.ends_with(e))
|| self.system_picker_just_opened();
if reached && self.ration.allows(self.frontmost_bundle_id()) {
self.tray.set_title(&format!("Emoji {}", self.live_shortcut()));
self.ration.spend(self.frontmost_bundle_id());
}
}
// CLEARS on the next keystroke, on app switch, or after a short beat,
// whichever comes first.
//
// RATION: three appearances for the life of the install, one per app, and it
// retires permanently the first time the user opens EmojiCopy by keyboard.
// There is no timer anywhere in this. Nothing fires on idle, on arrival, on
// absence, or on how long since you last used us.
Tray title while shown: Emoji ⌘⌥␣
Settings row: Shortcut answer
Settings sub: The menu bar shows your shortcut when you reach for
an emoji another way. Three times, then never.
global_shortcut().register · src-tauri/src/lib.rs:102 — already shipping, and currently the source of two live collisions. This card is the fix.ShortcutChoice · new — one screen, two options, neither preselected..setup__pair two equal columns · grid, because neither option may be visually dominant (R10)// WHAT SHIPS TODAY (lib.rs:105-123, verified). The loop registers EVERY chord
// that succeeds and keeps a single bool. Every result goes to a println and
// nothing else. macOS list, in order:
//
// META|CONTROL + Space <-- macOS "Emoji and Symbols". WE ARE TAKING IT
// SILENTLY, and getting neither credit nor consent.
// CONTROL|SHIFT + Space
// META|SHIFT + Space
// META|CONTROL|SHIFT + Space
// CONTROL + KeyE <-- Cocoa "move to end of line" in EVERY text field
// on the machine. Registering it globally breaks
// that binding system-wide.
//
// plus normal_shortcut META|ALT+Space, normal_shortcut_2 META|SHIFT+E,
// normal_shortcut_3 CONTROL|SHIFT+E.
//
// let mut registered_compact_shortcut = false;
// for compact_shortcut in &compact_shortcuts {
// match app.global_shortcut().register(compact_shortcut.clone()) {
// Ok(_) => { registered_compact_shortcut = true; println!(...) }
// Err(error) => { println!(...) }
// }
// }
//
// Consequence: NO KEYCAP ANYWHERE IN THE PRODUCT CAN BE HONEST, because nothing
// records which chord actually won.
// WHAT SHOULD SHIP.
static LIVE_SHORTCUT: Mutex<Option<Shortcut>> = Mutex::new(None);
// Explicit precedence, first success wins, then STOP. Both collisions removed
// from the automatic set: META|CONTROL+Space only via the user's own choice on
// this screen, CONTROL+KeyE never.
for candidate in PRECEDENCE {
if app.global_shortcut().register(candidate.clone()).is_ok() {
*LIVE_SHORTCUT.lock().unwrap() = Some(candidate);
break
}
}
// Expose it to the webview. Every keycap in the product prints from this.
Title: Emoji key
Sub: Pick the key that opens EmojiCopy. You can change this later.
Option 1: Use ⌃⌘␣ for EmojiCopy
Option 1 sub: macOS uses this key for its own emoji picker. Turn that off in
Keyboard settings and EmojiCopy takes it over.
Option 2: Keep ⌃⌘␣ for the macOS picker
Option 2 sub: EmojiCopy stays on ⌘⌥␣.
set_ignores_mouse_events(true). It is a hint and can never become a control.
FieldMarkPanel · new — click-through Status-level panel. Shares the caret chain and the buffer with the trigger..mark glyph then keycap · flex, so a glance carries the mark and the chord together (R10)// This is the only version of the in-field icon that cannot reproduce any of
// the documented failures of the category.
let mark = PanelBuilder::new(app, "field-mark")
.style_mask(StyleMask::empty().nonactivating_panel())
.level(PanelLevel::Status)
.build()?;
// IT EATS NO CLICKS. This single line is what separates it from Grammarly's
// badge and 1Password's field icon, both of which are controls sitting in
// somebody else's text field.
mark.set_ignores_mouse_events(true);
mark.set_hides_on_deactivate(false);
mark.set_can_become_key_window(false);
// PLACEMENT: below the caret BASELINE, outside the field's own box, in the
// gutter the host is not using. Never inside a bounding box someone else owns,
// so it cannot fight a show-password eye, a clear glyph or a character counter.
// VACATES on: next keystroke, click, arrow, focus change, app switch.
// RATION: one per app, three per install, retired permanently on first
// shortcut use. Never on field focus, never on arrival, never on a timer.
Settings row: Field mark
Sub: A small mark near your cursor when you reach for an emoji
another way. Three times, then never.
Second line: Turning this off does not change anything else.
lib.rs:143.
Menu::with_items · src-tauri/src/lib.rs:143 — today: two MenuItems with None::<&str> as the accelerator. Pass real accelerators.PickerFooter · new — one row pinned to the bottom edge of the mini panel..mini__foot · flex with margin-left auto on the keys, so the row survives a label being added later (R10)/* Pinned to the panel's bottom edge, present on BOTH entry paths so the grid
never reflows between a tray open and a keyboard open. */
.mini__foot {
position: absolute;
right: 0;
bottom: 0;
left: 0;
display: flex;
align-items: center;
gap: 6px;
padding: 8px 14px 10px;
border-top: 1px solid #2A2D36;
background: #1F2129;
}
.mini__foot-keys { display: flex; gap: 4px; margin-left: auto; }
.kbd {
display: inline-grid;
place-items: center;
min-width: 16px;
height: 16px;
padding: 0 4px;
border-radius: 4px;
background: #2E313B;
color: var(--metal-300);
font-size: 9.5px;
font-weight: 600;
line-height: 1;
}
HeldKeys · new — a keydown and keyup handler inside the webview. No Rust, no permission, no panel..mini__grid eight columns · grid, and the held state must not change a single track (R10)<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
const held = ref(false)
// Modifier state only. This never inspects which key was pressed, so it is
// not a keystroke observer and needs nothing from Rust.
function onKey(event) {
held.value = event.metaKey
}
// A window blur while the modifier is down would strand the painted state.
function clear() {
held.value = false
}
onMounted(() => {
window.addEventListener('keydown', onKey)
window.addEventListener('keyup', onKey)
window.addEventListener('blur', clear)
})
onBeforeUnmount(() => {
window.removeEventListener('keydown', onKey)
window.removeEventListener('keyup', onKey)
window.removeEventListener('blur', clear)
})
</script>
<template>
<div :class="['picker', { 'picker--held': held }]">
<slot />
</div>
</template>
desktop.js:151.
activate_app_by_bundle_id · src-tauri/src/input.rs:37 — already shipping. Restoring the paste target is what makes the footer line true.clipboard-manager:allow-write-text · capabilities/default.json — the plugin is already permitted. Nothing new to add.accessibility_trusted() · new (Rust) — the NON-prompting check, used for every poll and every branch.1. THE TAP IS A NO-OP.
ui/utils/services/desktop.js:151-154 — when the check returns false the
function logs and bare-returns. The catch at 162-168 also only logs.
Nothing is inserted, copied, or shown.
2. THERE IS NO CLIPBOARD FALLBACK TO FALL BACK TO.
ui/utils/services/emoji.js:279 —
if (hasAutoCopy && mode() !== 'mini') navigator.clipboard.writeText(...)
Mini mode is the ONLY place insert runs, and the only place it can fail. It
is also the one mode excluded from the clipboard write. The graceful
degradation everyone assumes exists is switched off at the source.
3. DEAD EVENT LISTENER.
desktop.js:124-132 listens for 'accessibility-permission-required'.
Grepping every emit in src-tauri/src returns only oauth_code,
mini-window-debug-info, mini-placement and mini-mode. It has never fired
and never can.
4. THE ONLY RENDERER CANNOT RUN WHERE THE FAILURE HAPPENS.
desktop.js:100-117 renderAccessibilityPermissionRequired is guarded by
`if (mode() !== 'full') return`. The failure is in mini mode. It is also a
monospace developer debug panel and must never be promoted to a user surface.
5. THE PROMPTING API IS CALLED ON EVERY TAP.
desktop.js:149 invokes request_accessibility_permission, and input.rs:10-34
passes kAXTrustedCheckOptionPrompt = YES. See the notes below for why this
is the most expensive of the five.
6. THE AUTO INSERT SETTING IS BYPASSED IN MINI MODE.
emoji.js:275 — `if (mode() === 'mini' || hasAutoInsert)`. Mini always
attempts insert regardless of the setting, so any copy pointing a user at
that toggle is false.
7. THE COMBO BRANCH DOUBLE-WRITES.
emoji.js:304-307 writes the clipboard and then also calls insertDesktop in
mini mode. Once a fallback lands inside insertEmojiDesktop this fires twice.
8. A NEW WINDOW LABEL GETS NO PERMISSIONS.
capabilities/default.json — "windows": ["main", "mini"]. The setup window in
card N will load and then fail every invoke. Add "setup" before building it.
SetupWindow · new — label "setup", 460 × 520, DECORATED, focusable, never registered for the blur-hide..setup__body column · flex, so the four copy blocks share one parent gap rather than carrying margins (R10, R11)// THE WINDOW. It must be DECORATED and it must never join the blur-hide
// handler in lib.rs. The mini window cannot host this: it is borderless,
// transparent and always-on-top, so it would float OVER the very dialog it is
// priming for, and it hides the instant that dialog takes focus.
//
// Before building it: add "setup" to capabilities/default.json "windows",
// or every invoke from this window fails with a permission denial.
#[tauri::command]
pub async fn open_accessibility_setting(app: tauri::AppHandle) {
// Both, in the same click, in this order.
//
// 1. The PROMPTING call. This is the ONLY place in the whole product that
// passes kAXTrustedCheckOptionPrompt = YES. It registers us in the
// Accessibility list. If macOS still has a prompt left it shows one; if
// not, nothing appears. We never read the result and never wait on it.
let _ = request_accessibility_permission().await;
// 2. The deep link. Verified: this is the exact format string Apple's own
// dialog builds, found in the universalAccessAuthWarn binary.
app.opener().open_url(
"x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension\
?Privacy_Accessibility",
None::<&str>,
);
// Legacy alias, also still resolving on Ventura through Tahoe:
// x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility
// NEVER com.apple.Accessibility-Settings.extension. That opens the
// VoiceOver and Zoom pane and it is the common wrong answer in search results.
}
// 3. The watcher. Poll the NON-prompting check while the setup window is
// visible and emit on change. This is what lets the screen advance itself.
fn watch(app: tauri::AppHandle) {
let mut last = false;
loop {
let now = unsafe { AXIsProcessTrusted() };
if now != last {
let _ = app.emit("accessibility-changed", now);
last = now;
}
sleep(Duration::from_millis(500));
}
}
Title: Typing into other apps
Body 1: EmojiCopy can put emoji straight into the app you came from instead
of your clipboard. macOS holds that behind one setting, called
Accessibility, in Privacy and Security.
Body 2: The setting is broad. It hands EmojiCopy more reach than it uses,
and macOS has no smaller version to ask for.
Body 3: What EmojiCopy does with it: put the character you picked where your
cursor is, and note which app you came from so it can put you back
there.
Body 4: Nothing you type is stored, and nothing you type leaves your Mac.
Buttons: Open the setting (primary) · Use the clipboard (quiet)
WHY THE SCOPE IS CONCEDED BEFORE THE USE IS EXPLAINED: the user is already
thinking the permission is oversized. Naming it first turns a suspicion into a
shared observation. Pretending it is small is the move Mac users still cite
against Dropbox.
TWO LINES THAT MUST NEVER BE WRITTEN, even though both are true today:
"We never see what you type" and "We cannot read your keyboard". They are true
right now (there is no CGEventTapCreate and no keyboard listener anywhere in
src-tauri/src), and both become FALSE the day the colon trigger ships. The two
lines above them stay true on both sides of that date, provided the trigger
stays local and stateless.
SetupWindow · new — the same 460 × 520 window. Content swaps in place, the window never closes and reopens.AccessibilityWatcher · new (Rust) — 500ms non-prompting poll while setup is visible, sparse after. Emits accessibility-changed.WAITING
Title: Waiting on the switch
Body: System Settings is open at Privacy and Security, then Accessibility.
Find EmojiCopy in that list and turn it on.
This screen updates on its own when you do.
No need to quit EmojiCopy. Leave it running.
Status: Watching for the switch
Buttons: Open System Settings (re-fires the deep link ONLY, never the prompt)
Use the clipboard (quiet)
GRANTED
Title: Typing on
Body: Pick an emoji and it goes into the app you came from.
Your clipboard stays as you left it.
Button: Done
DECLINED
Title: Clipboard mode
Body: EmojiCopy works without the setting. Pick an emoji and it goes on
your clipboard for you to paste where you want it.
Typing into other apps can be turned on later from the menu bar icon
or Settings.
Button: Done
THE WAY BACK IN, and there is no third route.
Tray menu item: Turn on typing (shown only while the setting is off)
Settings row: Typing into other apps
Sub when off: Off. Emoji go to your clipboard.
Sub when on: On. Emoji go into the app you came from.
GRANT LOST, shown once on the next blocked tap after typing has worked before.
Title: Typing off
Body: The Accessibility setting is off for EmojiCopy, so picks are going to
your clipboard again. This usually follows an app update.
Open the list, find EmojiCopy, and turn it back on.
ON BUT NOT TYPING, reached from the Settings row link.
Title: Switch on, no typing
Body: Some apps block typed input from other apps. Password fields do it on
purpose, and Terminal does it when secure keyboard entry is on. In
those, use the clipboard.
If nothing anywhere is taking the emoji, the entry in the
Accessibility list may be pointing at an older copy of EmojiCopy.
Turn EmojiCopy off in that list and turn it back on.