diff --git a/src/client/index.tsx b/src/client/index.tsx index 6439b94..41e8a67 100644 --- a/src/client/index.tsx +++ b/src/client/index.tsx @@ -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 openEmojiPicker((emoji) => { + console.log('Selected:', emoji) + })}>{props.children} + } +}) + const AppDetail = ({ app }: { app: App }) => ( <> - {app.icon ?? } + {app.icon ?? }   {app.name} @@ -481,6 +493,7 @@ const Dashboard = () => { Select an app to view details )} + ) } @@ -489,6 +502,9 @@ const render = () => { renderApp(, 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 diff --git a/src/client/tags/emoji-picker.tsx b/src/client/tags/emoji-picker.tsx new file mode 100644 index 0000000..06d34bf --- /dev/null +++ b/src/client/tags/emoji-picker.tsx @@ -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 = { + 'People': '๐Ÿ˜€', + 'Gestures': '๐Ÿ‘‹', + 'Animals': '๐Ÿถ', + 'Food': '๐ŸŽ', + 'Activities': 'โšฝ', + 'Travel': '๐Ÿš—', + 'Objects': '๐Ÿ’ก', + 'Symbols': 'โค๏ธ', + 'Nature': '๐ŸŒธ', +} + +const EMOJI_CATEGORIES: Record = { + '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 ( + + + {emojis.map((emoji, i) => ( + handleSelect(emoji)}> + {emoji} + + ))} + + + + {CATEGORIES.map(cat => ( + selectCategory(cat)} + title={cat} + > + {CATEGORY_ICONS[cat]} + + ))} + + + ) + } +}) + +export const openEmojiPicker = (onSelect: (emoji: string) => void) => { + selectedCategory = 'People' + onSelectCallback = onSelect + openModal('Choose Emoji', () => ) +} diff --git a/src/client/tags/modal.tsx b/src/client/tags/modal.tsx new file mode 100644 index 0000000..d709e09 --- /dev/null +++ b/src/client/tags/modal.tsx @@ -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 ( + + e.stopPropagation()}> + + {modalTitle} + ร— + + + {modalContent()} + + + + ) +}