emoji picker, modal
This commit is contained in:
parent
e2ff53e6d8
commit
a04d89d034
|
|
@ -2,6 +2,8 @@ import { render as renderApp } from 'hono/jsx/dom'
|
|||
import { define, Styles } from 'forge'
|
||||
import type { App, AppState } from '../shared/types'
|
||||
import { theme } from './themes'
|
||||
import { Modal, initModal } from './tags/modal'
|
||||
import { openEmojiPicker } from './tags/emoji-picker'
|
||||
|
||||
// UI state (survives re-renders)
|
||||
let selectedApp: string | null = localStorage.getItem('selectedApp')
|
||||
|
|
@ -335,11 +337,21 @@ const toggleSidebar = () => {
|
|||
render()
|
||||
}
|
||||
|
||||
const OpenEmojiPicker = define('OpenEmojiPicker', {
|
||||
cursor: 'pointer',
|
||||
|
||||
render({ props, parts: { Root } }) {
|
||||
return <Root onClick={() => openEmojiPicker((emoji) => {
|
||||
console.log('Selected:', emoji)
|
||||
})}>{props.children}</Root>
|
||||
}
|
||||
})
|
||||
|
||||
const AppDetail = ({ app }: { app: App }) => (
|
||||
<>
|
||||
<MainHeader>
|
||||
<MainTitle>
|
||||
{app.icon ?? <StatusDot state={app.state} />}
|
||||
<OpenEmojiPicker>{app.icon ?? <StatusDot state={app.state} />}</OpenEmojiPicker>
|
||||
|
||||
{app.name}
|
||||
</MainTitle>
|
||||
|
|
@ -481,6 +493,7 @@ const Dashboard = () => {
|
|||
<EmptyState>Select an app to view details</EmptyState>
|
||||
)}
|
||||
</Main>
|
||||
<Modal />
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
|
@ -489,6 +502,9 @@ const render = () => {
|
|||
renderApp(<Dashboard />, document.getElementById('app')!)
|
||||
}
|
||||
|
||||
// Initialize modal with render function
|
||||
initModal(render)
|
||||
|
||||
// Set theme based on system preference
|
||||
const setTheme = () => {
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
|
|
|
|||
141
src/client/tags/emoji-picker.tsx
Normal file
141
src/client/tags/emoji-picker.tsx
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
import { define } from 'forge'
|
||||
import { theme } from '../themes'
|
||||
import { openModal, closeModal, rerenderModal } from './modal'
|
||||
|
||||
type Category = 'People' | 'Gestures' | 'Animals' | 'Food' | 'Activities' | 'Travel' | 'Objects' | 'Symbols' | 'Nature'
|
||||
|
||||
const CATEGORY_ICONS: Record<Category, string> = {
|
||||
'People': '😀',
|
||||
'Gestures': '👋',
|
||||
'Animals': '🐶',
|
||||
'Food': '🍎',
|
||||
'Activities': '⚽',
|
||||
'Travel': '🚗',
|
||||
'Objects': '💡',
|
||||
'Symbols': '❤️',
|
||||
'Nature': '🌸',
|
||||
}
|
||||
|
||||
const EMOJI_CATEGORIES: Record<Category, string[]> = {
|
||||
'People': ['😀', '😃', '😄', '😁', '😆', '😅', '🤣', '😂', '🙂', '🙃', '😉', '😊', '😇', '🥰', '😍', '🤩', '😘', '😗', '😚', '😙', '🥲', '😋', '😛', '😜', '🤪', '😝', '🤑', '🤗', '🤭', '🤫', '🤔', '🤐', '🤨', '😐', '😑', '😶', '😏', '😒', '🙄', '😬', '🤥', '😌', '😔', '😪', '🤤', '😴', '😷', '🤒', '🤕', '🤢', '🤮', '🤧', '🥵', '🥶', '🥴', '😵', '🤯', '🤠', '🥳', '🥸', '😎', '🤓', '🧐', '😕', '😟', '🙁', '😮', '😯', '😲', '😳', '🥺', '😦', '😧', '😨', '😰', '😥', '😢', '😭', '😱', '😖', '😣', '😞', '😓', '😩', '😫', '🥱', '😤', '😡', '😠', '🤬', '😈', '👿', '💀', '☠️', '💩', '🤡', '👹', '👺', '👻', '👽', '👾', '🤖'],
|
||||
'Gestures': ['👋', '🤚', '🖐️', '✋', '🖖', '👌', '🤌', '🤏', '✌️', '🤞', '🤟', '🤘', '🤙', '👈', '👉', '👆', '🖕', '👇', '☝️', '👍', '👎', '✊', '👊', '🤛', '🤜', '👏', '🙌', '👐', '🤲', '🤝', '🙏', '✍️', '💅', '🤳', '💪', '🦾', '🦿', '🦵', '🦶', '👂', '🦻', '👃', '🧠', '🫀', '🫁', '🦷', '🦴', '👀', '👁️', '👅', '👄'],
|
||||
'Animals': ['🐶', '🐱', '🐭', '🐹', '🐰', '🦊', '🐻', '🐼', '🐻❄️', '🐨', '🐯', '🦁', '🐮', '🐷', '🐸', '🐵', '🙈', '🙉', '🙊', '🐒', '🐔', '🐧', '🐦', '🐤', '🐣', '🐥', '🦆', '🦅', '🦉', '🦇', '🐺', '🐗', '🐴', '🦄', '🐝', '🪱', '🐛', '🦋', '🐌', '🐞', '🐜', '🪰', '🪲', '🪳', '🦟', '🦗', '🕷️', '🦂', '🐢', '🐍', '🦎', '🦖', '🦕', '🐙', '🦑', '🦐', '🦞', '🦀', '🐡', '🐠', '🐟', '🐬', '🐳', '🐋', '🦈', '🐊', '🐅', '🐆', '🦓', '🦍', '🦧', '🦣', '🐘', '🦛', '🦏', '🐪', '🐫', '🦒', '🦘', '🦬', '🐃', '🐂', '🐄', '🐎', '🐖', '🐏', '🐑', '🦙', '🐐', '🦌', '🐕', '🐩', '🦮', '🐕🦺', '🐈', '🐈⬛', '🪶', '🐓', '🦃', '🦤', '🦚', '🦜', '🦢', '🦩', '🕊️', '🐇', '🦝', '🦨', '🦡', '🦫', '🦦', '🦥', '🐁', '🐀', '🐿️', '🦔'],
|
||||
'Food': ['🍎', '🍐', '🍊', '🍋', '🍌', '🍉', '🍇', '🍓', '🫐', '🍈', '🍒', '🍑', '🥭', '🍍', '🥥', '🥝', '🍅', '🍆', '🥑', '🥦', '🥬', '🥒', '🌶️', '🫑', '🌽', '🥕', '🫒', '🧄', '🧅', '🥔', '🍠', '🥐', '🥯', '🍞', '🥖', '🥨', '🧀', '🥚', '🍳', '🧈', '🥞', '🧇', '🥓', '🥩', '🍗', '🍖', '🦴', '🌭', '🍔', '🍟', '🍕', '🫓', '🥪', '🥙', '🧆', '🌮', '🌯', '🫔', '🥗', '🥘', '🫕', '🥫', '🍝', '🍜', '🍲', '🍛', '🍣', '🍱', '🥟', '🦪', '🍤', '🍙', '🍚', '🍘', '🍥', '🥠', '🥮', '🍢', '🍡', '🍧', '🍨', '🍦', '🥧', '🧁', '🍰', '🎂', '🍮', '🍭', '🍬', '🍫', '🍿', '🍩', '🍪', '🌰', '🥜', '🍯', '🥛', '🍼', '🫖', '☕', '🍵', '🧃', '🥤', '🧋', '🍶', '🍺', '🍻', '🥂', '🍷', '🥃', '🍸', '🍹', '🧉', '🍾', '🧊'],
|
||||
'Activities': ['⚽', '🏀', '🏈', '⚾', '🥎', '🎾', '🏐', '🏉', '🥏', '🎱', '🪀', '🏓', '🏸', '🏒', '🏑', '🥍', '🏏', '🪃', '🥅', '⛳', '🪁', '🏹', '🎣', '🤿', '🥊', '🥋', '🎽', '🛹', '🛼', '🛷', '⛸️', '🥌', '🎿', '⛷️', '🏂', '🪂', '🏋️', '🤼', '🤸', '⛹️', '🤺', '🤾', '🏌️', '🏇', '🧘', '🏄', '🏊', '🤽', '🚣', '🧗', '🚵', '🚴', '🏆', '🥇', '🥈', '🥉', '🏅', '🎖️', '🏵️', '🎗️', '🎫', '🎟️', '🎪', '🎭', '🎨', '🎬', '🎤', '🎧', '🎼', '🎹', '🥁', '🪘', '🎷', '🎺', '🪗', '🎸', '🪕', '🎻', '🎲', '♟️', '🎯', '🎳', '🎮', '🎰', '🧩'],
|
||||
'Travel': ['🚗', '🚕', '🚙', '🚌', '🚎', '🏎️', '🚓', '🚑', '🚒', '🚐', '🛻', '🚚', '🚛', '🚜', '🦯', '🦽', '🦼', '🛴', '🚲', '🛵', '🏍️', '🛺', '🚨', '🚔', '🚍', '🚘', '🚖', '🚡', '🚠', '🚟', '🚃', '🚋', '🚞', '🚝', '🚄', '🚅', '🚈', '🚂', '🚆', '🚇', '🚊', '🚉', '✈️', '🛫', '🛬', '🛩️', '💺', '🛰️', '🚀', '🛸', '🚁', '🛶', '⛵', '🚤', '🛥️', '🛳️', '⛴️', '🚢', '⚓', '🪝', '⛽', '🚧', '🚦', '🚥', '🚏', '🗺️', '🗿', '🗽', '🗼', '🏰', '🏯', '🏟️', '🎡', '🎢', '🎠', '⛲', '⛱️', '🏖️', '🏝️', '🏜️', '🌋', '⛰️', '🏔️', '🗻', '🏕️', '⛺', '🛖', '🏠', '🏡', '🏘️', '🏚️', '🏗️', '🏭', '🏢', '🏬', '🏣', '🏤', '🏥', '🏦', '🏨', '🏪', '🏫', '🏩', '💒', '🏛️', '⛪', '🕌', '🕍', '🛕', '🕋', '⛩️'],
|
||||
'Objects': ['⌚', '📱', '📲', '💻', '⌨️', '🖥️', '🖨️', '🖱️', '🖲️', '🕹️', '🗜️', '💽', '💾', '💿', '📀', '📼', '📷', '📸', '📹', '🎥', '📽️', '🎞️', '📞', '☎️', '📟', '📠', '📺', '📻', '🎙️', '🎚️', '🎛️', '🧭', '⏱️', '⏲️', '⏰', '🕰️', '⌛', '⏳', '📡', '🔋', '🔌', '💡', '🔦', '🕯️', '🪔', '🧯', '🛢️', '💸', '💵', '💴', '💶', '💷', '🪙', '💰', '💳', '💎', '⚖️', '🪜', '🧰', '🪛', '🔧', '🔨', '⚒️', '🛠️', '⛏️', '🪚', '🔩', '⚙️', '🪤', '🧱', '⛓️', '🧲', '🔫', '💣', '🧨', '🪓', '🔪', '🗡️', '⚔️', '🛡️', '🚬', '⚰️', '🪦', '⚱️', '🏺', '🔮', '📿', '🧿', '💈', '⚗️', '🔭', '🔬', '🕳️', '🩹', '🩺', '💊', '💉', '🩸', '🧬', '🦠', '🧫', '🧪'],
|
||||
'Symbols': ['❤️', '🧡', '💛', '💚', '💙', '💜', '🖤', '🤍', '🤎', '💔', '❣️', '💕', '💞', '💓', '💗', '💖', '💘', '💝', '💟', '☮️', '✝️', '☪️', '🕉️', '☸️', '✡️', '🔯', '🕎', '☯️', '☦️', '🛐', '⛎', '♈', '♉', '♊', '♋', '♌', '♍', '♎', '♏', '♐', '♑', '♒', '♓', '🆔', '⚛️', '🉑', '☢️', '☣️', '📴', '📳', '🈶', '🈚', '🈸', '🈺', '🈷️', '✴️', '🆚', '💮', '🉐', '㊙️', '㊗️', '🈴', '🈵', '🈹', '🈲', '🅰️', '🅱️', '🆎', '🆑', '🅾️', '🆘', '❌', '⭕', '🛑', '⛔', '📛', '🚫', '💯', '💢', '♨️', '🚷', '🚯', '🚳', '🚱', '🔞', '📵', '🚭', '❗', '❕', '❓', '❔', '‼️', '⁉️', '🔅', '🔆', '〽️', '⚠️', '🚸', '🔱', '⚜️', '🔰', '♻️', '✅', '🈯', '💹', '❇️', '✳️', '❎', '🌐', '💠', 'Ⓜ️', '🌀', '💤', '🏧', '🚾', '♿', '🅿️', '🛗', '🈳', '🈂️', '🛂', '🛃', '🛄', '🛅', '🚹', '🚺', '🚼', '⚧️', '🚻', '🚮', '🎦', '📶', '🈁', '🔣', 'ℹ️', '🔤', '🔡', '🔠', '🆖', '🆗', '🆙', '🆒', '🆕', '🆓', '0️⃣', '1️⃣', '2️⃣', '3️⃣', '4️⃣', '5️⃣', '6️⃣', '7️⃣', '8️⃣', '9️⃣', '🔟', '🔢', '#️⃣', '*️⃣', '⏏️', '▶️', '⏸️', '⏯️', '⏹️', '⏺️', '⏭️', '⏮️', '⏩', '⏪', '⏫', '⏬', '◀️', '🔼', '🔽', '➡️', '⬅️', '⬆️', '⬇️', '↗️', '↘️', '↙️', '↖️', '↕️', '↔️', '↪️', '↩️', '⤴️', '⤵️', '🔀', '🔁', '🔂', '🔄', '🔃', '🎵', '🎶', '➕', '➖', '➗', '✖️', '♾️', '💲', '💱', '™️', '©️', '®️', '〰️', '➰', '➿', '🔚', '🔙', '🔛', '🔝', '🔜', '✔️', '☑️', '🔘', '🔴', '🟠', '🟡', '🟢', '🔵', '🟣', '⚫', '⚪', '🟤', '🔺', '🔻', '🔸', '🔹', '🔶', '🔷', '🔳', '🔲', '▪️', '▫️', '◾', '◽', '◼️', '◻️', '🟥', '🟧', '🟨', '🟩', '🟦', '🟪', '⬛', '⬜', '🟫', '🔈', '🔇', '🔉', '🔊', '🔔', '🔕', '📣', '📢', '👁️🗨️', '💬', '💭', '🗯️', '♠️', '♣️', '♥️', '♦️', '🃏', '🎴', '🀄', '🕐', '🕑', '🕒', '🕓', '🕔', '🕕', '🕖', '🕗', '🕘', '🕙', '🕚', '🕛', '🕜', '🕝', '🕞', '🕟', '🕠', '🕡', '🕢', '🕣', '🕤', '🕥', '🕦', '🕧'],
|
||||
'Nature': ['🌸', '💮', '🏵️', '🌹', '🥀', '🌺', '🌻', '🌼', '🌷', '🌱', '🪴', '🌲', '🌳', '🌴', '🌵', '🌾', '🌿', '☘️', '🍀', '🍁', '🍂', '🍃', '🍄', '🌰', '🦀', '🦞', '🦐', '🦑', '🌍', '🌎', '🌏', '🌐', '🪨', '🌑', '🌒', '🌓', '🌔', '🌕', '🌖', '🌗', '🌘', '🌙', '🌚', '🌛', '🌜', '☀️', '🌝', '🌞', '🪐', '⭐', '🌟', '🌠', '🌌', '☁️', '⛅', '⛈️', '🌤️', '🌥️', '🌦️', '🌧️', '🌨️', '🌩️', '🌪️', '🌫️', '🌬️', '🌀', '🌈', '🌂', '☂️', '☔', '⛱️', '⚡', '❄️', '☃️', '⛄', '☄️', '🔥', '💧', '🌊'],
|
||||
}
|
||||
|
||||
let selectedCategory: Category = 'People'
|
||||
let onSelectCallback: ((emoji: string) => void) | null = null
|
||||
|
||||
const Container = define('EmojiPickerContainer', {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 12,
|
||||
})
|
||||
|
||||
const EmojiGrid = define('EmojiGrid', {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(10, 1fr)',
|
||||
gap: 4,
|
||||
overflow: 'auto',
|
||||
alignContent: 'start',
|
||||
height: 280,
|
||||
})
|
||||
|
||||
const TabBar = define('EmojiTabBar', {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
gap: 4,
|
||||
paddingTop: 12,
|
||||
borderTop: `1px solid ${theme('colors-border')}`,
|
||||
marginTop: 'auto',
|
||||
})
|
||||
|
||||
const TabButton = define('EmojiTabButton', {
|
||||
base: 'button',
|
||||
padding: '8px 10px',
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
borderRadius: theme('radius-md'),
|
||||
cursor: 'pointer',
|
||||
fontSize: 18,
|
||||
lineHeight: 1,
|
||||
opacity: 0.5,
|
||||
selectors: {
|
||||
'&:hover': { opacity: 0.8, background: theme('colors-bgHover') },
|
||||
},
|
||||
variants: {
|
||||
active: { opacity: 1, background: theme('colors-bgSelected') },
|
||||
},
|
||||
})
|
||||
|
||||
const EmojiButton = define('EmojiButton', {
|
||||
base: 'button',
|
||||
padding: 6,
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
borderRadius: theme('radius-md'),
|
||||
cursor: 'pointer',
|
||||
fontSize: 22,
|
||||
lineHeight: 1,
|
||||
selectors: {
|
||||
'&:hover': { background: theme('colors-bgHover') },
|
||||
},
|
||||
})
|
||||
|
||||
const getEmojis = (): string[] => {
|
||||
return EMOJI_CATEGORIES[selectedCategory]
|
||||
}
|
||||
|
||||
const selectCategory = (category: Category) => {
|
||||
selectedCategory = category
|
||||
rerenderModal()
|
||||
}
|
||||
|
||||
const handleSelect = (emoji: string) => {
|
||||
onSelectCallback?.(emoji)
|
||||
closeModal()
|
||||
}
|
||||
|
||||
const CATEGORIES: Category[] = ['People', 'Gestures', 'Animals', 'Food', 'Activities', 'Travel', 'Objects', 'Symbols', 'Nature']
|
||||
|
||||
const EmojiPickerContent = define('EmojiPickerContent', {
|
||||
render() {
|
||||
const emojis = getEmojis()
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<EmojiGrid>
|
||||
{emojis.map((emoji, i) => (
|
||||
<EmojiButton key={i} onClick={() => handleSelect(emoji)}>
|
||||
{emoji}
|
||||
</EmojiButton>
|
||||
))}
|
||||
</EmojiGrid>
|
||||
|
||||
<TabBar>
|
||||
{CATEGORIES.map(cat => (
|
||||
<TabButton
|
||||
key={cat}
|
||||
active={cat === selectedCategory ? true : undefined}
|
||||
onClick={() => selectCategory(cat)}
|
||||
title={cat}
|
||||
>
|
||||
{CATEGORY_ICONS[cat]}
|
||||
</TabButton>
|
||||
))}
|
||||
</TabBar>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
export const openEmojiPicker = (onSelect: (emoji: string) => void) => {
|
||||
selectedCategory = 'People'
|
||||
onSelectCallback = onSelect
|
||||
openModal('Choose Emoji', () => <EmojiPickerContent />)
|
||||
}
|
||||
105
src/client/tags/modal.tsx
Normal file
105
src/client/tags/modal.tsx
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
import type { Child } from 'hono/jsx'
|
||||
import { define } from 'forge'
|
||||
import { theme } from '../themes'
|
||||
|
||||
let modalTitle: string | null = null
|
||||
let modalContent: (() => Child) | null = null
|
||||
let renderFn: (() => void) | null = null
|
||||
|
||||
export const initModal = (render: () => void) => {
|
||||
renderFn = render
|
||||
}
|
||||
|
||||
export const openModal = (title: string, content: () => Child) => {
|
||||
modalTitle = title
|
||||
modalContent = content
|
||||
renderFn?.()
|
||||
}
|
||||
|
||||
export const closeModal = () => {
|
||||
modalTitle = null
|
||||
modalContent = null
|
||||
renderFn?.()
|
||||
}
|
||||
|
||||
export const rerenderModal = () => {
|
||||
renderFn?.()
|
||||
}
|
||||
|
||||
// ESC key handler
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && modalContent) {
|
||||
closeModal()
|
||||
}
|
||||
})
|
||||
|
||||
const ModalBackdrop = define('ModalBackdrop', {
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
background: 'rgba(0, 0, 0, 0.5)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: 1000,
|
||||
})
|
||||
|
||||
const ModalBox = define('ModalBox', {
|
||||
background: theme('colors-bg'),
|
||||
borderRadius: theme('radius-md'),
|
||||
border: `1px solid ${theme('colors-border')}`,
|
||||
boxShadow: '0 4px 24px rgba(0, 0, 0, 0.2)',
|
||||
maxWidth: 500,
|
||||
width: '90%',
|
||||
maxHeight: '80vh',
|
||||
overflow: 'auto',
|
||||
})
|
||||
|
||||
const ModalHeader = define('ModalHeader', {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: '16px 20px',
|
||||
borderBottom: `1px solid ${theme('colors-border')}`,
|
||||
})
|
||||
|
||||
const ModalTitle = define('ModalTitle', {
|
||||
fontSize: 16,
|
||||
fontWeight: 600,
|
||||
margin: 0,
|
||||
})
|
||||
|
||||
const ModalCloseButton = define('ModalCloseButton', {
|
||||
base: 'button',
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
padding: 4,
|
||||
fontSize: 18,
|
||||
color: theme('colors-textMuted'),
|
||||
lineHeight: 1,
|
||||
selectors: {
|
||||
'&:hover': { color: theme('colors-text') },
|
||||
},
|
||||
})
|
||||
|
||||
const ModalBody = define('ModalBody', {
|
||||
padding: 20,
|
||||
})
|
||||
|
||||
export const Modal = () => {
|
||||
if (!modalContent) return null
|
||||
|
||||
return (
|
||||
<ModalBackdrop onClick={closeModal}>
|
||||
<ModalBox onClick={(e: MouseEvent) => e.stopPropagation()}>
|
||||
<ModalHeader>
|
||||
<ModalTitle>{modalTitle}</ModalTitle>
|
||||
<ModalCloseButton onClick={closeModal}>×</ModalCloseButton>
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
{modalContent()}
|
||||
</ModalBody>
|
||||
</ModalBox>
|
||||
</ModalBackdrop>
|
||||
)
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user