diff --git a/backend/api/pages.ts b/backend/api/pages.ts index 95fd9d863..32407740c 100644 --- a/backend/api/pages.ts +++ b/backend/api/pages.ts @@ -150,7 +150,7 @@ async function routes(app: FastifyInstance) { schema: { summary: 'Search pages', description: - 'Postgres full-text search over the pages of a site, ranked by relevance. `query` may be left out, in which case the filters alone decide the results — which is what a search for nothing but tags is.\n\nReadable without a session, for the same reason reading a page is: an anonymous request only matches published pages. Drafts are included only for someone who may write pages, and password-protected pages only for someone who may edit them, since a result carries an excerpt of the page text. A page marked as not searchable never appears, whoever is asking.\n\n`highlight` is an excerpt with the matched terms wrapped in ``, and is the only field carrying markup — the excerpt is escaped before those are added. It is absent unless term highlighting is enabled in the search settings.', + 'Postgres full-text search over the pages of a site, ranked by relevance. `query` may be left out, in which case the filters alone decide the results — which is what a search for nothing but tags is.\n\nReadable without a session, for the same reason reading a page is: an anonymous request only matches published pages. Drafts are included only for someone who may write pages. A page marked as not searchable never appears, whoever is asking.\n\nA password-protected page is listed like any other — its title and description are not what the password covers — but for a searcher who would have to enter that password it can only be matched on those two, never on the text behind the lock, and it comes back with no `highlight`.\n\n`highlight` is an excerpt with the matched terms wrapped in ``, and is the only field carrying markup — the excerpt is escaped before those are added. It is absent unless term highlighting is enabled in the search settings.', tags: ['Pages'], params: siteIdParam, querystring: { @@ -262,9 +262,10 @@ async function routes(app: FastifyInstance) { includeDrafts: ['write:pages', 'manage:pages', 'manage:system'].some((p) => permissions.includes(p) ), - // -> Same rule as the page view: a protected page's text is for whoever holds the password, - // and a search excerpt is that text - hideProtected: !mayBypassPassword(req) + // -> Same rule as the page view: a protected page's text is for whoever holds the password, and + // a search excerpt is that text. Its title and description are not covered, so the page is + // still listed — see `hideProtectedContent`. + hideProtectedContent: !mayBypassPassword(req) }) } ) diff --git a/backend/locales/en.json b/backend/locales/en.json index 63abe1e0f..5a3b98f65 100644 --- a/backend/locales/en.json +++ b/backend/locales/en.json @@ -1513,8 +1513,6 @@ "common.page.locked": "This page is password protected.", "common.page.lockedHint": "Enter the password to read it.", "common.page.lockedWrongPassword": "That password is not correct.", - "common.page.unlock": "Unlock", - "common.page.unlockTitle": "Unlock this page", "common.page.printFormat": "Print Format", "common.page.private": "Private", "common.page.published": "Published", @@ -1524,6 +1522,8 @@ "common.page.tags": "Tags", "common.page.tagsMatching": "Pages matching tags", "common.page.toc": "Table of Contents", + "common.page.unlock": "Unlock", + "common.page.unlockTitle": "Unlock this page", "common.page.unpublished": "Unpublished", "common.page.unpublishedWarning": "This page is not published.", "common.page.versionId": "Version ID {id}", @@ -1590,6 +1590,8 @@ "editor.assets.uploadFailed": "File upload failed.", "editor.backToEditor": "Back to Editor", "editor.ckeditor.stats": "{chars} chars, {words} words", + "editor.codeBlock.filter": "Filter languages...", + "editor.codeBlock.noResults": "No language matches that.", "editor.conflict.editable": "(editable)", "editor.conflict.infoGeneric": "A more recent version of this page was saved by {authorName}, {date}", "editor.conflict.leftPanelInfo": "Your current edit, based on page version from {date}", @@ -1613,6 +1615,20 @@ "editor.conflict.whatToDoLocal": "Use your current local version and ignore the latest changes.", "editor.conflict.whatToDoRemote": "Use the remote version (latest) and discard your changes.", "editor.createPage": "Create Page", + "editor.emoji.activities": "Activities", + "editor.emoji.animalsNature": "Animals & Nature", + "editor.emoji.flags": "Flags", + "editor.emoji.foodDrink": "Food & Drink", + "editor.emoji.frequentlyUsed": "Frequently Used", + "editor.emoji.noResults": "No emoji matches that.", + "editor.emoji.objects": "Objects", + "editor.emoji.peopleBody": "People & Body", + "editor.emoji.pick": "Pick an emoji...", + "editor.emoji.results": "Results", + "editor.emoji.search": "Search", + "editor.emoji.smileysEmotion": "Smileys & Emotion", + "editor.emoji.symbols": "Symbols", + "editor.emoji.travelPlaces": "Travel & Places", "editor.markup.admonitionDanger": "Danger / Important Admonition", "editor.markup.admonitionInfo": "Info / Note Admonition", "editor.markup.admonitionSuccess": "Tip / Success Admonition", @@ -1767,6 +1783,18 @@ "editor.settings.markdownFontSizeHint": "The font size to use in the editor.", "editor.settings.markdownPreviewShown": "Display Render Preview", "editor.settings.markdownPreviewShownHint": "Whether to display a preview of the rendered content.", + "editor.tableEditor.addColumn": "Add Column", + "editor.tableEditor.addRow": "Add Row", + "editor.tableEditor.align": "Alignment", + "editor.tableEditor.alignCenter": "Center", + "editor.tableEditor.alignLeft": "Left", + "editor.tableEditor.alignRight": "Right", + "editor.tableEditor.bodyCell": "Row {row}, column {column}", + "editor.tableEditor.headerCell": "Heading of column {column}", + "editor.tableEditor.markdown": "Markdown", + "editor.tableEditor.pasteHint": "Paste a table from a spreadsheet into any cell to fill the grid.", + "editor.tableEditor.removeColumn": "Remove Column", + "editor.tableEditor.removeRow": "Remove Row", "editor.tableEditor.title": "Table Editor", "editor.togglePreviewPane": "Toggle Preview Pane", "editor.toggleScrollSync": "Toggle Scroll Sync", @@ -1890,6 +1918,14 @@ "iconPicker.selection": "Selected icon", "iconPicker.set": "Set", "iconPicker.setsFailed": "Failed to load the icon sets.", + "linkPicker.emptyFolder": "There are no pages in this folder.", + "linkPicker.linkUrl": "Link URL", + "linkPicker.loadFailed": "Failed to load the page tree.", + "linkPicker.openInNewTab": "Open in a new tab", + "linkPicker.page": "Page", + "linkPicker.selection": "Link target", + "linkPicker.title": "Insert Link", + "linkPicker.url": "URL", "navEdit.clearItems": "Clear All Items", "navEdit.editMenuItems": "Edit Menu Items", "navEdit.emptyMenuText": "Click the Add button to add your first menu item.", diff --git a/backend/models/search.ts b/backend/models/search.ts index 4f6dcb55f..9ea9fe102 100644 --- a/backend/models/search.ts +++ b/backend/models/search.ts @@ -91,10 +91,11 @@ export interface SearchPagesParams { /** Whether unpublished pages belong in the results, which is an editor's view of the wiki. */ includeDrafts?: boolean /** - * Leave out password-protected pages. Set for anyone who would have to enter the password to read - * one, because a result carries an excerpt of the page text — see `highlight` below. + * Keep a password-protected page's *body* out of the results, for a searcher who would have to enter + * the password to read it. The page itself still appears — its title and description are not what + * the password covers — but it can only be matched on those, and comes back with no excerpt. */ - hideProtected?: boolean + hideProtectedContent?: boolean } /** @@ -218,7 +219,7 @@ class Search { limit = 25, publicOnly = false, includeDrafts = false, - hideProtected = true + hideProtectedContent = true }: SearchPagesParams): Promise { const terms = query.trim() const hasQuery = terms.length > 0 @@ -243,15 +244,21 @@ class Search { } else if (!includeDrafts) { conditions.push(sql`p."publishState" <> 'draft'`) } - if (hideProtected) { + if (hideProtectedContent && hasQuery) { /* - A result is not just a title: `highlight` below is an excerpt of the page's own text, cut from - `searchContent`. Handing that to someone who would be shown a lock screen on the page itself - would give away through search exactly what the password withholds — so a protected page is - absent from their results entirely rather than present without its excerpt, which would still - confirm that a page matching their terms is there. + A protected page is findable by name, not by what it says. + + `indexPage` stores the three parts of a page under distinct weights — title `A`, description + `B`, body `C` — so `ts_filter` can drop the body and ask whether the query still matches. A + protected page therefore surfaces when the terms are in its title or description, both of which + it shows to everyone anyway, and stays out when they are only in the text behind the password. + Otherwise a search for a distinctive phrase would confirm the phrase is in there, which is the + thing the password is for. + + Written with the cheap test first: for a page with no password the OR short-circuits and + `ts_filter` never runs. */ - conditions.push(sql`p.password IS NULL`) + conditions.push(sql`(p.password IS NULL OR ts_filter(p.ts, '{a,b}') @@ ${tsQuery})`) } if (publishState) { conditions.push(sql`p."publishState" = ${publishState}`) @@ -281,11 +288,19 @@ class Search { }[effectiveOrderBy] const { termHighlighting } = this.getConfig() + const headline = sql`ts_headline(${dict}, coalesce(p."searchContent", ''), ${tsQuery}, + ${`StartSel=${HL_START},StopSel=${HL_STOP},MaxWords=25,MinWords=10,MaxFragments=1`})` + /* + The excerpt is cut from the page's own text, so a protected page has none to give a searcher who + would be shown a lock screen on the page itself. `CASE` rather than a filter on the rows: the page + still belongs in the results, it just arrives without the part the password covers. + */ const highlight = - hasQuery && termHighlighting - ? sql`ts_headline(${dict}, coalesce(p."searchContent", ''), ${tsQuery}, - ${`StartSel=${HL_START},StopSel=${HL_STOP},MaxWords=25,MinWords=10,MaxFragments=1`})` - : sql`NULL` + !hasQuery || !termHighlighting + ? sql`NULL` + : hideProtectedContent + ? sql`CASE WHEN p.password IS NULL THEN ${headline} ELSE NULL END` + : headline const rows = await WIKI.db.execute(sql` SELECT diff --git a/frontend/package-lock.json b/frontend/package-lock.json index bc09def13..1edf9513f 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -60,7 +60,6 @@ "socket.io-client": "4.8.3", "sortablejs": "1.15.7", "sortablejs-vue3": "1.3.0", - "tabulator-tables": "6.4.0", "tailwindcss": "4.3.3", "temporal-polyfill": "1.0.1", "tippy.js": "6.3.7", @@ -78,6 +77,7 @@ "devDependencies": { "@iconify-json/la": "1.2.1", "@iconify-json/mdi": "1.2.3", + "@iconify-json/tabler": "1.2.38", "@intlify/unplugin-vue-i18n": "11.2.3", "@types/lodash": "4.17.24", "@vitejs/plugin-vue": "6.0.8", @@ -88,6 +88,7 @@ "oxfmt": "0.54.0", "oxlint": "1.69.0", "sass": "1.101.0", + "unicode-emoji-json": "0.9.0", "vite": "8.1.5", "vite-plugin-vue-devtools": "8.1.2" }, @@ -907,6 +908,16 @@ "@iconify/types": "*" } }, + "node_modules/@iconify-json/tabler": { + "version": "1.2.38", + "resolved": "https://registry.npmjs.org/@iconify-json/tabler/-/tabler-1.2.38.tgz", + "integrity": "sha512-9v6ooRU/bKOK/Whv4w2aiK5gBnfCV9Ei+uD1iDDeLu2b+EnF3G3ZKkRKWEOOLG8bJyfKn6XEia6gUQ9oO7cF+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@iconify/types": "*" + } + }, "node_modules/@iconify/types": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", @@ -7545,12 +7556,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/tabulator-tables": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/tabulator-tables/-/tabulator-tables-6.4.0.tgz", - "integrity": "sha512-Lxh+leFNoBo/Yyr4USs6gxqbfo8anYUaUMmoT91pfVLtoUgl/dE+qV7ahnFrKVMCYYqGG33aIMPR7FzpPBaNYA==", - "license": "MIT" - }, "node_modules/tailwindcss": { "version": "4.3.3", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", @@ -7783,6 +7788,13 @@ "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", "license": "MIT" }, + "node_modules/unicode-emoji-json": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/unicode-emoji-json/-/unicode-emoji-json-0.9.0.tgz", + "integrity": "sha512-HdrQW/7SdbXUsTd8uTATv7LvQJkCSgbAgRCDCtOhQ6xn3EAx+xZHQtRniOXWJK3ywIqPegkNhP4U4EbuFsiQng==", + "dev": true, + "license": "MIT" + }, "node_modules/unplugin": { "version": "2.3.11", "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.11.tgz", diff --git a/frontend/package.json b/frontend/package.json index 11f4a248b..8dad1389f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,7 +12,9 @@ "ncu": "ncu -i", "ncu-u": "ncu -u", "icons": "node scripts/generate-icons.mjs", - "icons:check": "node scripts/generate-icons.mjs --check" + "icons:check": "node scripts/generate-icons.mjs --check", + "emoji": "node scripts/generate-emoji.mjs", + "emoji:check": "node scripts/generate-emoji.mjs --check" }, "dependencies": { "@quasar/extras": "2.0.1", @@ -67,7 +69,6 @@ "socket.io-client": "4.8.3", "sortablejs": "1.15.7", "sortablejs-vue3": "1.3.0", - "tabulator-tables": "6.4.0", "tailwindcss": "4.3.3", "temporal-polyfill": "1.0.1", "tippy.js": "6.3.7", @@ -85,6 +86,7 @@ "devDependencies": { "@iconify-json/la": "1.2.1", "@iconify-json/mdi": "1.2.3", + "@iconify-json/tabler": "1.2.38", "@intlify/unplugin-vue-i18n": "11.2.3", "@types/lodash": "4.17.24", "@vitejs/plugin-vue": "6.0.8", @@ -95,6 +97,7 @@ "oxfmt": "0.54.0", "oxlint": "1.69.0", "sass": "1.101.0", + "unicode-emoji-json": "0.9.0", "vite": "8.1.5", "vite-plugin-vue-devtools": "8.1.2" }, diff --git a/frontend/scripts/generate-emoji.mjs b/frontend/scripts/generate-emoji.mjs new file mode 100644 index 000000000..2c04d5b47 --- /dev/null +++ b/frontend/scripts/generate-emoji.mjs @@ -0,0 +1,123 @@ +/* + Generates `src/assets/emoji.generated.js` — the grouping the emoji picker is built from. + + Two datasets have to meet for a picker to work here: + + - `markdown-it-emoji` is the renderer's own vocabulary: `:shortcode:` is what gets written into a + page, and anything outside that map would go in as text that never becomes an emoji. It has no + notion of categories. + - `unicode-emoji-json` carries the CLDR grouping and ordering — Smileys & Emotion, Animals & Nature, + and so on — which is what the picker's tabs are, and no notion of shortcodes. + + So this writes out the intersection: each group, in Unicode's own order, as `[shortcode, character]` + pairs — the shortcode is what gets written into the page, the character is what the picker draws in its + grid. The picker cannot offer an emoji the renderer would not draw, because every pair came from the + renderer's own map. + + The characters are written here rather than looked up at runtime, even though the editor already + bundles that map for rendering. Reading it would mean importing `markdown-it-emoji/lib/data/full.mjs` + from app code — a path into the package's internals rather than an entry point it publishes, and one + more dependency for the bundler to discover. A few kB of duplicated characters is the cheaper side of + that trade. + + `unicode-emoji-json` is a devDependency for that reason: its data ends up here, and here is committed. + + Usage: node scripts/generate-emoji.mjs [--check] +*/ +import fs from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +import emojiByGroup from 'unicode-emoji-json/data-by-group.json' with { type: 'json' } +import shortcodeToEmoji from 'markdown-it-emoji/lib/data/full.mjs' + +const ROOT = fileURLToPath(new URL('../', import.meta.url)) +const OUT = path.join(ROOT, 'src/assets/emoji.generated.js') + +/** + * One shortcode per emoji, since several can point at the same character — `laughing` and `satisfied` + * are both 😆, `+1` and `thumbsup` are both 👍. + * + * The first one wins, except that a name starting with a letter beats one that does not: the map's own + * order is close to canonical, and `thumbsup` reads better in a page's source than `+1`. + */ +function buildShortcodeIndex() { + const index = new Map() + for (const [shortcode, emoji] of Object.entries(shortcodeToEmoji)) { + const current = index.get(emoji) + if (!current || (!/^[a-z]/.test(current) && /^[a-z]/.test(shortcode))) { + index.set(emoji, shortcode) + } + } + return index +} + +function build() { + const index = buildShortcodeIndex() + const groups = [] + const claimed = new Set() + + for (const group of emojiByGroup) { + const emoji = [] + for (const entry of group.emojis) { + const shortcode = index.get(entry.emoji) + // -> Skipped rather than invented: an emoji the renderer has no shortcode for cannot be written + // into a page as an emoji, so it has no business in a picker + if (shortcode && !claimed.has(shortcode)) { + claimed.add(shortcode) + emoji.push([shortcode, entry.emoji]) + } + } + if (emoji.length > 0) { + groups.push({ slug: group.slug, name: group.name, emoji }) + } + } + + return { groups, claimed } +} + +function serialize({ groups, claimed }) { + const body = groups + .map((group) => { + const pairs = group.emoji.map(([code, char]) => `['${code}', '${char}']`).join(', ') + // -> Single quotes, so the generated file matches what oxfmt would write for the rest of `src` + return ` { + slug: '${group.slug}', + name: '${group.name}', + emoji: [${pairs}] + }` + }) + .join(',\n') + + return `/* + GENERATED by scripts/generate-emoji.mjs — do not edit. + + The CLDR emoji groups, in Unicode's order, as \`[shortcode, character]\` pairs. The shortcode is one + \`markdown-it-emoji\` accepts — what a page stores and what the renderer draws — and the character is + what a picker shows. Regenerate with \`npm run emoji\`. + + ${claimed.size} emoji in ${groups.length} groups. +*/ +export const EMOJI_GROUPS = [ +${body} +] +` +} + +const data = build() +const output = serialize(data) + +if (process.argv.includes('--check')) { + const current = fs.existsSync(OUT) ? fs.readFileSync(OUT, 'utf8') : '' + if (current !== output) { + console.error('emoji.generated.js is out of date — run `npm run emoji`') + process.exit(1) + } + console.log(`OK ${data.claimed.size} emoji, data up to date`) +} else { + fs.mkdirSync(path.dirname(OUT), { recursive: true }) + fs.writeFileSync(OUT, output) + console.log( + `wrote ${data.claimed.size} emoji in ${data.groups.length} groups to src/assets/emoji.generated.js (${Buffer.byteLength(output).toLocaleString()} B)` + ) +} diff --git a/frontend/scripts/generate-icons.mjs b/frontend/scripts/generate-icons.mjs index 6515a6721..2c69d9e01 100644 --- a/frontend/scripts/generate-icons.mjs +++ b/frontend/scripts/generate-icons.mjs @@ -26,8 +26,15 @@ const ROOT = fileURLToPath(new URL('../', import.meta.url)) const SRC = path.join(ROOT, 'src') const OUT = path.join(SRC, 'assets/icons.generated.js') -/** Icon sets we bundle from. A prefix not listed here is left to resolve at runtime. */ -const SETS = ['mdi', 'la'] +/** + * Icon sets we bundle from. A prefix not listed here is left to resolve at runtime — which for an icon + * written into this repo's own source means it does not render at all unless an administrator happens to + * have added that set, so a new set has to be listed HERE and installed as `@iconify-json/`. + * + * The skip is silent on purpose: `prefix:name` also describes every permission string in the frontend + * (`write:pages`, `manage:system`), and those must not be mistaken for icons. + */ +const SETS = ['mdi', 'la', 'tabler'] /** * A quoted string that is EXACTLY an Iconify reference. Requiring the whole literal to match is what diff --git a/frontend/src/assets/emoji.generated.js b/frontend/src/assets/emoji.generated.js new file mode 100644 index 000000000..9c28b7c48 --- /dev/null +++ b/frontend/src/assets/emoji.generated.js @@ -0,0 +1,56 @@ +/* + GENERATED by scripts/generate-emoji.mjs — do not edit. + + The CLDR emoji groups, in Unicode's order, as `[shortcode, character]` pairs. The shortcode is one + `markdown-it-emoji` accepts — what a page stores and what the renderer draws — and the character is + what a picker shows. Regenerate with `npm run emoji`. + + 1860 emoji in 9 groups. +*/ +export const EMOJI_GROUPS = [ + { + slug: 'smileys_emotion', + name: 'Smileys & Emotion', + emoji: [['grinning', '😀'], ['smiley', '😃'], ['smile', '😄'], ['grin', '😁'], ['laughing', '😆'], ['sweat_smile', '😅'], ['rofl', '🤣'], ['joy', '😂'], ['slightly_smiling_face', '🙂'], ['upside_down_face', '🙃'], ['melting_face', '🫠'], ['wink', '😉'], ['blush', '😊'], ['innocent', '😇'], ['smiling_face_with_three_hearts', '🥰'], ['heart_eyes', '😍'], ['star_struck', '🤩'], ['kissing_heart', '😘'], ['kissing', '😗'], ['relaxed', '☺️'], ['kissing_closed_eyes', '😚'], ['kissing_smiling_eyes', '😙'], ['smiling_face_with_tear', '🥲'], ['yum', '😋'], ['stuck_out_tongue', '😛'], ['stuck_out_tongue_winking_eye', '😜'], ['zany_face', '🤪'], ['stuck_out_tongue_closed_eyes', '😝'], ['money_mouth_face', '🤑'], ['hugs', '🤗'], ['hand_over_mouth', '🤭'], ['face_with_open_eyes_and_hand_over_mouth', '🫢'], ['face_with_peeking_eye', '🫣'], ['shushing_face', '🤫'], ['thinking', '🤔'], ['saluting_face', '🫡'], ['zipper_mouth_face', '🤐'], ['raised_eyebrow', '🤨'], ['neutral_face', '😐'], ['expressionless', '😑'], ['no_mouth', '😶'], ['dotted_line_face', '🫥'], ['face_in_clouds', '😶‍🌫️'], ['smirk', '😏'], ['unamused', '😒'], ['roll_eyes', '🙄'], ['grimacing', '😬'], ['face_exhaling', '😮‍💨'], ['lying_face', '🤥'], ['shaking_face', '🫨'], ['relieved', '😌'], ['pensive', '😔'], ['sleepy', '😪'], ['drooling_face', '🤤'], ['sleeping', '😴'], ['mask', '😷'], ['face_with_thermometer', '🤒'], ['face_with_head_bandage', '🤕'], ['nauseated_face', '🤢'], ['vomiting_face', '🤮'], ['sneezing_face', '🤧'], ['hot_face', '🥵'], ['cold_face', '🥶'], ['woozy_face', '🥴'], ['dizzy_face', '😵'], ['face_with_spiral_eyes', '😵‍💫'], ['exploding_head', '🤯'], ['cowboy_hat_face', '🤠'], ['partying_face', '🥳'], ['disguised_face', '🥸'], ['sunglasses', '😎'], ['nerd_face', '🤓'], ['monocle_face', '🧐'], ['confused', '😕'], ['face_with_diagonal_mouth', '🫤'], ['worried', '😟'], ['slightly_frowning_face', '🙁'], ['frowning_face', '☹️'], ['open_mouth', '😮'], ['hushed', '😯'], ['astonished', '😲'], ['flushed', '😳'], ['pleading_face', '🥺'], ['face_holding_back_tears', '🥹'], ['frowning', '😦'], ['anguished', '😧'], ['fearful', '😨'], ['cold_sweat', '😰'], ['disappointed_relieved', '😥'], ['cry', '😢'], ['sob', '😭'], ['scream', '😱'], ['confounded', '😖'], ['persevere', '😣'], ['disappointed', '😞'], ['sweat', '😓'], ['weary', '😩'], ['tired_face', '😫'], ['yawning_face', '🥱'], ['triumph', '😤'], ['rage', '😡'], ['angry', '😠'], ['cursing_face', '🤬'], ['smiling_imp', '😈'], ['imp', '👿'], ['skull', '💀'], ['skull_and_crossbones', '☠️'], ['hankey', '💩'], ['clown_face', '🤡'], ['japanese_ogre', '👹'], ['japanese_goblin', '👺'], ['ghost', '👻'], ['alien', '👽'], ['space_invader', '👾'], ['robot', '🤖'], ['smiley_cat', '😺'], ['smile_cat', '😸'], ['joy_cat', '😹'], ['heart_eyes_cat', '😻'], ['smirk_cat', '😼'], ['kissing_cat', '😽'], ['scream_cat', '🙀'], ['crying_cat_face', '😿'], ['pouting_cat', '😾'], ['see_no_evil', '🙈'], ['hear_no_evil', '🙉'], ['speak_no_evil', '🙊'], ['love_letter', '💌'], ['cupid', '💘'], ['gift_heart', '💝'], ['sparkling_heart', '💖'], ['heartpulse', '💗'], ['heartbeat', '💓'], ['revolving_hearts', '💞'], ['two_hearts', '💕'], ['heart_decoration', '💟'], ['heavy_heart_exclamation', '❣️'], ['broken_heart', '💔'], ['heart_on_fire', '❤️‍🔥'], ['mending_heart', '❤️‍🩹'], ['heart', '❤️'], ['pink_heart', '🩷'], ['orange_heart', '🧡'], ['yellow_heart', '💛'], ['green_heart', '💚'], ['blue_heart', '💙'], ['light_blue_heart', '🩵'], ['purple_heart', '💜'], ['brown_heart', '🤎'], ['black_heart', '🖤'], ['grey_heart', '🩶'], ['white_heart', '🤍'], ['kiss', '💋'], ['100', '💯'], ['anger', '💢'], ['boom', '💥'], ['dizzy', '💫'], ['sweat_drops', '💦'], ['dash', '💨'], ['hole', '🕳️'], ['speech_balloon', '💬'], ['eye_speech_bubble', '👁️‍🗨️'], ['left_speech_bubble', '🗨️'], ['right_anger_bubble', '🗯️'], ['thought_balloon', '💭'], ['zzz', '💤']] + }, + { + slug: 'people_body', + name: 'People & Body', + emoji: [['wave', '👋'], ['raised_back_of_hand', '🤚'], ['raised_hand_with_fingers_splayed', '🖐️'], ['hand', '✋'], ['vulcan_salute', '🖖'], ['rightwards_hand', '🫱'], ['leftwards_hand', '🫲'], ['palm_down_hand', '🫳'], ['palm_up_hand', '🫴'], ['leftwards_pushing_hand', '🫷'], ['rightwards_pushing_hand', '🫸'], ['ok_hand', '👌'], ['pinched_fingers', '🤌'], ['pinching_hand', '🤏'], ['v', '✌️'], ['crossed_fingers', '🤞'], ['hand_with_index_finger_and_thumb_crossed', '🫰'], ['love_you_gesture', '🤟'], ['metal', '🤘'], ['call_me_hand', '🤙'], ['point_left', '👈'], ['point_right', '👉'], ['point_up_2', '👆'], ['middle_finger', '🖕'], ['point_down', '👇'], ['point_up', '☝️'], ['index_pointing_at_the_viewer', '🫵'], ['thumbsup', '👍'], ['thumbsdown', '👎'], ['fist_raised', '✊'], ['fist_oncoming', '👊'], ['fist_left', '🤛'], ['fist_right', '🤜'], ['clap', '👏'], ['raised_hands', '🙌'], ['heart_hands', '🫶'], ['open_hands', '👐'], ['palms_up_together', '🤲'], ['handshake', '🤝'], ['pray', '🙏'], ['writing_hand', '✍️'], ['nail_care', '💅'], ['selfie', '🤳'], ['muscle', '💪'], ['mechanical_arm', '🦾'], ['mechanical_leg', '🦿'], ['leg', '🦵'], ['foot', '🦶'], ['ear', '👂'], ['ear_with_hearing_aid', '🦻'], ['nose', '👃'], ['brain', '🧠'], ['anatomical_heart', '🫀'], ['lungs', '🫁'], ['tooth', '🦷'], ['bone', '🦴'], ['eyes', '👀'], ['eye', '👁️'], ['tongue', '👅'], ['lips', '👄'], ['biting_lip', '🫦'], ['baby', '👶'], ['child', '🧒'], ['boy', '👦'], ['girl', '👧'], ['adult', '🧑'], ['blond_haired_person', '👱'], ['man', '👨'], ['bearded_person', '🧔'], ['man_beard', '🧔‍♂️'], ['woman_beard', '🧔‍♀️'], ['red_haired_man', '👨‍🦰'], ['curly_haired_man', '👨‍🦱'], ['white_haired_man', '👨‍🦳'], ['bald_man', '👨‍🦲'], ['woman', '👩'], ['red_haired_woman', '👩‍🦰'], ['person_red_hair', '🧑‍🦰'], ['curly_haired_woman', '👩‍🦱'], ['person_curly_hair', '🧑‍🦱'], ['white_haired_woman', '👩‍🦳'], ['person_white_hair', '🧑‍🦳'], ['bald_woman', '👩‍🦲'], ['person_bald', '🧑‍🦲'], ['blond_haired_woman', '👱‍♀️'], ['blond_haired_man', '👱‍♂️'], ['older_adult', '🧓'], ['older_man', '👴'], ['older_woman', '👵'], ['frowning_person', '🙍'], ['frowning_man', '🙍‍♂️'], ['frowning_woman', '🙍‍♀️'], ['pouting_face', '🙎'], ['pouting_man', '🙎‍♂️'], ['pouting_woman', '🙎‍♀️'], ['no_good', '🙅'], ['no_good_man', '🙅‍♂️'], ['no_good_woman', '🙅‍♀️'], ['ok_person', '🙆'], ['ok_man', '🙆‍♂️'], ['ok_woman', '🙆‍♀️'], ['tipping_hand_person', '💁'], ['tipping_hand_man', '💁‍♂️'], ['tipping_hand_woman', '💁‍♀️'], ['raising_hand', '🙋'], ['raising_hand_man', '🙋‍♂️'], ['raising_hand_woman', '🙋‍♀️'], ['deaf_person', '🧏'], ['deaf_man', '🧏‍♂️'], ['deaf_woman', '🧏‍♀️'], ['bow', '🙇'], ['bowing_man', '🙇‍♂️'], ['bowing_woman', '🙇‍♀️'], ['facepalm', '🤦'], ['man_facepalming', '🤦‍♂️'], ['woman_facepalming', '🤦‍♀️'], ['shrug', '🤷'], ['man_shrugging', '🤷‍♂️'], ['woman_shrugging', '🤷‍♀️'], ['health_worker', '🧑‍⚕️'], ['man_health_worker', '👨‍⚕️'], ['woman_health_worker', '👩‍⚕️'], ['student', '🧑‍🎓'], ['man_student', '👨‍🎓'], ['woman_student', '👩‍🎓'], ['teacher', '🧑‍🏫'], ['man_teacher', '👨‍🏫'], ['woman_teacher', '👩‍🏫'], ['judge', '🧑‍⚖️'], ['man_judge', '👨‍⚖️'], ['woman_judge', '👩‍⚖️'], ['farmer', '🧑‍🌾'], ['man_farmer', '👨‍🌾'], ['woman_farmer', '👩‍🌾'], ['cook', '🧑‍🍳'], ['man_cook', '👨‍🍳'], ['woman_cook', '👩‍🍳'], ['mechanic', '🧑‍🔧'], ['man_mechanic', '👨‍🔧'], ['woman_mechanic', '👩‍🔧'], ['factory_worker', '🧑‍🏭'], ['man_factory_worker', '👨‍🏭'], ['woman_factory_worker', '👩‍🏭'], ['office_worker', '🧑‍💼'], ['man_office_worker', '👨‍💼'], ['woman_office_worker', '👩‍💼'], ['scientist', '🧑‍🔬'], ['man_scientist', '👨‍🔬'], ['woman_scientist', '👩‍🔬'], ['technologist', '🧑‍💻'], ['man_technologist', '👨‍💻'], ['woman_technologist', '👩‍💻'], ['singer', '🧑‍🎤'], ['man_singer', '👨‍🎤'], ['woman_singer', '👩‍🎤'], ['artist', '🧑‍🎨'], ['man_artist', '👨‍🎨'], ['woman_artist', '👩‍🎨'], ['pilot', '🧑‍✈️'], ['man_pilot', '👨‍✈️'], ['woman_pilot', '👩‍✈️'], ['astronaut', '🧑‍🚀'], ['man_astronaut', '👨‍🚀'], ['woman_astronaut', '👩‍🚀'], ['firefighter', '🧑‍🚒'], ['man_firefighter', '👨‍🚒'], ['woman_firefighter', '👩‍🚒'], ['police_officer', '👮'], ['policeman', '👮‍♂️'], ['policewoman', '👮‍♀️'], ['detective', '🕵️'], ['male_detective', '🕵️‍♂️'], ['female_detective', '🕵️‍♀️'], ['guard', '💂'], ['guardsman', '💂‍♂️'], ['guardswoman', '💂‍♀️'], ['ninja', '🥷'], ['construction_worker', '👷'], ['construction_worker_man', '👷‍♂️'], ['construction_worker_woman', '👷‍♀️'], ['person_with_crown', '🫅'], ['prince', '🤴'], ['princess', '👸'], ['person_with_turban', '👳'], ['man_with_turban', '👳‍♂️'], ['woman_with_turban', '👳‍♀️'], ['man_with_gua_pi_mao', '👲'], ['woman_with_headscarf', '🧕'], ['person_in_tuxedo', '🤵'], ['man_in_tuxedo', '🤵‍♂️'], ['woman_in_tuxedo', '🤵‍♀️'], ['person_with_veil', '👰'], ['man_with_veil', '👰‍♂️'], ['woman_with_veil', '👰‍♀️'], ['pregnant_woman', '🤰'], ['pregnant_man', '🫃'], ['pregnant_person', '🫄'], ['breast_feeding', '🤱'], ['woman_feeding_baby', '👩‍🍼'], ['man_feeding_baby', '👨‍🍼'], ['person_feeding_baby', '🧑‍🍼'], ['angel', '👼'], ['santa', '🎅'], ['mrs_claus', '🤶'], ['mx_claus', '🧑‍🎄'], ['superhero', '🦸'], ['superhero_man', '🦸‍♂️'], ['superhero_woman', '🦸‍♀️'], ['supervillain', '🦹'], ['supervillain_man', '🦹‍♂️'], ['supervillain_woman', '🦹‍♀️'], ['mage', '🧙'], ['mage_man', '🧙‍♂️'], ['mage_woman', '🧙‍♀️'], ['fairy', '🧚'], ['fairy_man', '🧚‍♂️'], ['fairy_woman', '🧚‍♀️'], ['vampire', '🧛'], ['vampire_man', '🧛‍♂️'], ['vampire_woman', '🧛‍♀️'], ['merperson', '🧜'], ['merman', '🧜‍♂️'], ['mermaid', '🧜‍♀️'], ['elf', '🧝'], ['elf_man', '🧝‍♂️'], ['elf_woman', '🧝‍♀️'], ['genie', '🧞'], ['genie_man', '🧞‍♂️'], ['genie_woman', '🧞‍♀️'], ['zombie', '🧟'], ['zombie_man', '🧟‍♂️'], ['zombie_woman', '🧟‍♀️'], ['troll', '🧌'], ['massage', '💆'], ['massage_man', '💆‍♂️'], ['massage_woman', '💆‍♀️'], ['haircut', '💇'], ['haircut_man', '💇‍♂️'], ['haircut_woman', '💇‍♀️'], ['walking', '🚶'], ['walking_man', '🚶‍♂️'], ['walking_woman', '🚶‍♀️'], ['standing_person', '🧍'], ['standing_man', '🧍‍♂️'], ['standing_woman', '🧍‍♀️'], ['kneeling_person', '🧎'], ['kneeling_man', '🧎‍♂️'], ['kneeling_woman', '🧎‍♀️'], ['person_with_probing_cane', '🧑‍🦯'], ['man_with_probing_cane', '👨‍🦯'], ['woman_with_probing_cane', '👩‍🦯'], ['person_in_motorized_wheelchair', '🧑‍🦼'], ['man_in_motorized_wheelchair', '👨‍🦼'], ['woman_in_motorized_wheelchair', '👩‍🦼'], ['person_in_manual_wheelchair', '🧑‍🦽'], ['man_in_manual_wheelchair', '👨‍🦽'], ['woman_in_manual_wheelchair', '👩‍🦽'], ['runner', '🏃'], ['running_man', '🏃‍♂️'], ['running_woman', '🏃‍♀️'], ['woman_dancing', '💃'], ['man_dancing', '🕺'], ['business_suit_levitating', '🕴️'], ['dancers', '👯'], ['dancing_men', '👯‍♂️'], ['dancing_women', '👯‍♀️'], ['sauna_person', '🧖'], ['sauna_man', '🧖‍♂️'], ['sauna_woman', '🧖‍♀️'], ['climbing', '🧗'], ['climbing_man', '🧗‍♂️'], ['climbing_woman', '🧗‍♀️'], ['person_fencing', '🤺'], ['horse_racing', '🏇'], ['skier', '⛷️'], ['snowboarder', '🏂'], ['golfing', '🏌️'], ['golfing_man', '🏌️‍♂️'], ['golfing_woman', '🏌️‍♀️'], ['surfer', '🏄'], ['surfing_man', '🏄‍♂️'], ['surfing_woman', '🏄‍♀️'], ['rowboat', '🚣'], ['rowing_man', '🚣‍♂️'], ['rowing_woman', '🚣‍♀️'], ['swimmer', '🏊'], ['swimming_man', '🏊‍♂️'], ['swimming_woman', '🏊‍♀️'], ['bouncing_ball_person', '⛹️'], ['bouncing_ball_man', '⛹️‍♂️'], ['bouncing_ball_woman', '⛹️‍♀️'], ['weight_lifting', '🏋️'], ['weight_lifting_man', '🏋️‍♂️'], ['weight_lifting_woman', '🏋️‍♀️'], ['bicyclist', '🚴'], ['biking_man', '🚴‍♂️'], ['biking_woman', '🚴‍♀️'], ['mountain_bicyclist', '🚵'], ['mountain_biking_man', '🚵‍♂️'], ['mountain_biking_woman', '🚵‍♀️'], ['cartwheeling', '🤸'], ['man_cartwheeling', '🤸‍♂️'], ['woman_cartwheeling', '🤸‍♀️'], ['wrestling', '🤼'], ['men_wrestling', '🤼‍♂️'], ['women_wrestling', '🤼‍♀️'], ['water_polo', '🤽'], ['man_playing_water_polo', '🤽‍♂️'], ['woman_playing_water_polo', '🤽‍♀️'], ['handball_person', '🤾'], ['man_playing_handball', '🤾‍♂️'], ['woman_playing_handball', '🤾‍♀️'], ['juggling_person', '🤹'], ['man_juggling', '🤹‍♂️'], ['woman_juggling', '🤹‍♀️'], ['lotus_position', '🧘'], ['lotus_position_man', '🧘‍♂️'], ['lotus_position_woman', '🧘‍♀️'], ['bath', '🛀'], ['sleeping_bed', '🛌'], ['people_holding_hands', '🧑‍🤝‍🧑'], ['two_women_holding_hands', '👭'], ['couple', '👫'], ['two_men_holding_hands', '👬'], ['couplekiss', '💏'], ['couplekiss_man_woman', '👩‍❤️‍💋‍👨'], ['couplekiss_man_man', '👨‍❤️‍💋‍👨'], ['couplekiss_woman_woman', '👩‍❤️‍💋‍👩'], ['couple_with_heart', '💑'], ['couple_with_heart_woman_man', '👩‍❤️‍👨'], ['couple_with_heart_man_man', '👨‍❤️‍👨'], ['couple_with_heart_woman_woman', '👩‍❤️‍👩'], ['family_man_woman_boy', '👨‍👩‍👦'], ['family_man_woman_girl', '👨‍👩‍👧'], ['family_man_woman_girl_boy', '👨‍👩‍👧‍👦'], ['family_man_woman_boy_boy', '👨‍👩‍👦‍👦'], ['family_man_woman_girl_girl', '👨‍👩‍👧‍👧'], ['family_man_man_boy', '👨‍👨‍👦'], ['family_man_man_girl', '👨‍👨‍👧'], ['family_man_man_girl_boy', '👨‍👨‍👧‍👦'], ['family_man_man_boy_boy', '👨‍👨‍👦‍👦'], ['family_man_man_girl_girl', '👨‍👨‍👧‍👧'], ['family_woman_woman_boy', '👩‍👩‍👦'], ['family_woman_woman_girl', '👩‍👩‍👧'], ['family_woman_woman_girl_boy', '👩‍👩‍👧‍👦'], ['family_woman_woman_boy_boy', '👩‍👩‍👦‍👦'], ['family_woman_woman_girl_girl', '👩‍👩‍👧‍👧'], ['family_man_boy', '👨‍👦'], ['family_man_boy_boy', '👨‍👦‍👦'], ['family_man_girl', '👨‍👧'], ['family_man_girl_boy', '👨‍👧‍👦'], ['family_man_girl_girl', '👨‍👧‍👧'], ['family_woman_boy', '👩‍👦'], ['family_woman_boy_boy', '👩‍👦‍👦'], ['family_woman_girl', '👩‍👧'], ['family_woman_girl_boy', '👩‍👧‍👦'], ['family_woman_girl_girl', '👩‍👧‍👧'], ['speaking_head', '🗣️'], ['bust_in_silhouette', '👤'], ['busts_in_silhouette', '👥'], ['people_hugging', '🫂'], ['family', '👪'], ['footprints', '👣']] + }, + { + slug: 'animals_nature', + name: 'Animals & Nature', + emoji: [['monkey_face', '🐵'], ['monkey', '🐒'], ['gorilla', '🦍'], ['orangutan', '🦧'], ['dog', '🐶'], ['dog2', '🐕'], ['guide_dog', '🦮'], ['service_dog', '🐕‍🦺'], ['poodle', '🐩'], ['wolf', '🐺'], ['fox_face', '🦊'], ['raccoon', '🦝'], ['cat', '🐱'], ['cat2', '🐈'], ['black_cat', '🐈‍⬛'], ['lion', '🦁'], ['tiger', '🐯'], ['tiger2', '🐅'], ['leopard', '🐆'], ['horse', '🐴'], ['moose', '🫎'], ['donkey', '🫏'], ['racehorse', '🐎'], ['unicorn', '🦄'], ['zebra', '🦓'], ['deer', '🦌'], ['bison', '🦬'], ['cow', '🐮'], ['ox', '🐂'], ['water_buffalo', '🐃'], ['cow2', '🐄'], ['pig', '🐷'], ['pig2', '🐖'], ['boar', '🐗'], ['pig_nose', '🐽'], ['ram', '🐏'], ['sheep', '🐑'], ['goat', '🐐'], ['dromedary_camel', '🐪'], ['camel', '🐫'], ['llama', '🦙'], ['giraffe', '🦒'], ['elephant', '🐘'], ['mammoth', '🦣'], ['rhinoceros', '🦏'], ['hippopotamus', '🦛'], ['mouse', '🐭'], ['mouse2', '🐁'], ['rat', '🐀'], ['hamster', '🐹'], ['rabbit', '🐰'], ['rabbit2', '🐇'], ['chipmunk', '🐿️'], ['beaver', '🦫'], ['hedgehog', '🦔'], ['bat', '🦇'], ['bear', '🐻'], ['polar_bear', '🐻‍❄️'], ['koala', '🐨'], ['panda_face', '🐼'], ['sloth', '🦥'], ['otter', '🦦'], ['skunk', '🦨'], ['kangaroo', '🦘'], ['badger', '🦡'], ['feet', '🐾'], ['turkey', '🦃'], ['chicken', '🐔'], ['rooster', '🐓'], ['hatching_chick', '🐣'], ['baby_chick', '🐤'], ['hatched_chick', '🐥'], ['bird', '🐦'], ['penguin', '🐧'], ['dove', '🕊️'], ['eagle', '🦅'], ['duck', '🦆'], ['swan', '🦢'], ['owl', '🦉'], ['dodo', '🦤'], ['feather', '🪶'], ['flamingo', '🦩'], ['peacock', '🦚'], ['parrot', '🦜'], ['wing', '🪽'], ['black_bird', '🐦‍⬛'], ['goose', '🪿'], ['frog', '🐸'], ['crocodile', '🐊'], ['turtle', '🐢'], ['lizard', '🦎'], ['snake', '🐍'], ['dragon_face', '🐲'], ['dragon', '🐉'], ['sauropod', '🦕'], ['t-rex', '🦖'], ['whale', '🐳'], ['whale2', '🐋'], ['dolphin', '🐬'], ['seal', '🦭'], ['fish', '🐟'], ['tropical_fish', '🐠'], ['blowfish', '🐡'], ['shark', '🦈'], ['octopus', '🐙'], ['shell', '🐚'], ['coral', '🪸'], ['jellyfish', '🪼'], ['crab', '🦀'], ['lobster', '🦞'], ['shrimp', '🦐'], ['squid', '🦑'], ['oyster', '🦪'], ['snail', '🐌'], ['butterfly', '🦋'], ['bug', '🐛'], ['ant', '🐜'], ['bee', '🐝'], ['beetle', '🪲'], ['lady_beetle', '🐞'], ['cricket', '🦗'], ['cockroach', '🪳'], ['spider', '🕷️'], ['spider_web', '🕸️'], ['scorpion', '🦂'], ['mosquito', '🦟'], ['fly', '🪰'], ['worm', '🪱'], ['microbe', '🦠'], ['bouquet', '💐'], ['cherry_blossom', '🌸'], ['white_flower', '💮'], ['lotus', '🪷'], ['rosette', '🏵️'], ['rose', '🌹'], ['wilted_flower', '🥀'], ['hibiscus', '🌺'], ['sunflower', '🌻'], ['blossom', '🌼'], ['tulip', '🌷'], ['hyacinth', '🪻'], ['seedling', '🌱'], ['potted_plant', '🪴'], ['evergreen_tree', '🌲'], ['deciduous_tree', '🌳'], ['palm_tree', '🌴'], ['cactus', '🌵'], ['ear_of_rice', '🌾'], ['herb', '🌿'], ['shamrock', '☘️'], ['four_leaf_clover', '🍀'], ['maple_leaf', '🍁'], ['fallen_leaf', '🍂'], ['leaves', '🍃'], ['empty_nest', '🪹'], ['nest_with_eggs', '🪺'], ['mushroom', '🍄']] + }, + { + slug: 'food_drink', + name: 'Food & Drink', + emoji: [['grapes', '🍇'], ['melon', '🍈'], ['watermelon', '🍉'], ['tangerine', '🍊'], ['lemon', '🍋'], ['banana', '🍌'], ['pineapple', '🍍'], ['mango', '🥭'], ['apple', '🍎'], ['green_apple', '🍏'], ['pear', '🍐'], ['peach', '🍑'], ['cherries', '🍒'], ['strawberry', '🍓'], ['blueberries', '🫐'], ['kiwi_fruit', '🥝'], ['tomato', '🍅'], ['olive', '🫒'], ['coconut', '🥥'], ['avocado', '🥑'], ['eggplant', '🍆'], ['potato', '🥔'], ['carrot', '🥕'], ['corn', '🌽'], ['hot_pepper', '🌶️'], ['bell_pepper', '🫑'], ['cucumber', '🥒'], ['leafy_green', '🥬'], ['broccoli', '🥦'], ['garlic', '🧄'], ['onion', '🧅'], ['peanuts', '🥜'], ['beans', '🫘'], ['chestnut', '🌰'], ['ginger_root', '🫚'], ['pea_pod', '🫛'], ['bread', '🍞'], ['croissant', '🥐'], ['baguette_bread', '🥖'], ['flatbread', '🫓'], ['pretzel', '🥨'], ['bagel', '🥯'], ['pancakes', '🥞'], ['waffle', '🧇'], ['cheese', '🧀'], ['meat_on_bone', '🍖'], ['poultry_leg', '🍗'], ['cut_of_meat', '🥩'], ['bacon', '🥓'], ['hamburger', '🍔'], ['fries', '🍟'], ['pizza', '🍕'], ['hotdog', '🌭'], ['sandwich', '🥪'], ['taco', '🌮'], ['burrito', '🌯'], ['tamale', '🫔'], ['stuffed_flatbread', '🥙'], ['falafel', '🧆'], ['egg', '🥚'], ['fried_egg', '🍳'], ['shallow_pan_of_food', '🥘'], ['stew', '🍲'], ['fondue', '🫕'], ['bowl_with_spoon', '🥣'], ['green_salad', '🥗'], ['popcorn', '🍿'], ['butter', '🧈'], ['salt', '🧂'], ['canned_food', '🥫'], ['bento', '🍱'], ['rice_cracker', '🍘'], ['rice_ball', '🍙'], ['rice', '🍚'], ['curry', '🍛'], ['ramen', '🍜'], ['spaghetti', '🍝'], ['sweet_potato', '🍠'], ['oden', '🍢'], ['sushi', '🍣'], ['fried_shrimp', '🍤'], ['fish_cake', '🍥'], ['moon_cake', '🥮'], ['dango', '🍡'], ['dumpling', '🥟'], ['fortune_cookie', '🥠'], ['takeout_box', '🥡'], ['icecream', '🍦'], ['shaved_ice', '🍧'], ['ice_cream', '🍨'], ['doughnut', '🍩'], ['cookie', '🍪'], ['birthday', '🎂'], ['cake', '🍰'], ['cupcake', '🧁'], ['pie', '🥧'], ['chocolate_bar', '🍫'], ['candy', '🍬'], ['lollipop', '🍭'], ['custard', '🍮'], ['honey_pot', '🍯'], ['baby_bottle', '🍼'], ['milk_glass', '🥛'], ['coffee', '☕'], ['teapot', '🫖'], ['tea', '🍵'], ['sake', '🍶'], ['champagne', '🍾'], ['wine_glass', '🍷'], ['cocktail', '🍸'], ['tropical_drink', '🍹'], ['beer', '🍺'], ['beers', '🍻'], ['clinking_glasses', '🥂'], ['tumbler_glass', '🥃'], ['pouring_liquid', '🫗'], ['cup_with_straw', '🥤'], ['bubble_tea', '🧋'], ['beverage_box', '🧃'], ['mate', '🧉'], ['ice_cube', '🧊'], ['chopsticks', '🥢'], ['plate_with_cutlery', '🍽️'], ['fork_and_knife', '🍴'], ['spoon', '🥄'], ['hocho', '🔪'], ['jar', '🫙'], ['amphora', '🏺']] + }, + { + slug: 'travel_places', + name: 'Travel & Places', + emoji: [['earth_africa', '🌍'], ['earth_americas', '🌎'], ['earth_asia', '🌏'], ['globe_with_meridians', '🌐'], ['world_map', '🗺️'], ['japan', '🗾'], ['compass', '🧭'], ['mountain_snow', '🏔️'], ['mountain', '⛰️'], ['volcano', '🌋'], ['mount_fuji', '🗻'], ['camping', '🏕️'], ['beach_umbrella', '🏖️'], ['desert', '🏜️'], ['desert_island', '🏝️'], ['national_park', '🏞️'], ['stadium', '🏟️'], ['classical_building', '🏛️'], ['building_construction', '🏗️'], ['bricks', '🧱'], ['rock', '🪨'], ['wood', '🪵'], ['hut', '🛖'], ['houses', '🏘️'], ['derelict_house', '🏚️'], ['house', '🏠'], ['house_with_garden', '🏡'], ['office', '🏢'], ['post_office', '🏣'], ['european_post_office', '🏤'], ['hospital', '🏥'], ['bank', '🏦'], ['hotel', '🏨'], ['love_hotel', '🏩'], ['convenience_store', '🏪'], ['school', '🏫'], ['department_store', '🏬'], ['factory', '🏭'], ['japanese_castle', '🏯'], ['european_castle', '🏰'], ['wedding', '💒'], ['tokyo_tower', '🗼'], ['statue_of_liberty', '🗽'], ['church', '⛪'], ['mosque', '🕌'], ['hindu_temple', '🛕'], ['synagogue', '🕍'], ['shinto_shrine', '⛩️'], ['kaaba', '🕋'], ['fountain', '⛲'], ['tent', '⛺'], ['foggy', '🌁'], ['night_with_stars', '🌃'], ['cityscape', '🏙️'], ['sunrise_over_mountains', '🌄'], ['sunrise', '🌅'], ['city_sunset', '🌆'], ['city_sunrise', '🌇'], ['bridge_at_night', '🌉'], ['hotsprings', '♨️'], ['carousel_horse', '🎠'], ['playground_slide', '🛝'], ['ferris_wheel', '🎡'], ['roller_coaster', '🎢'], ['barber', '💈'], ['circus_tent', '🎪'], ['steam_locomotive', '🚂'], ['railway_car', '🚃'], ['bullettrain_side', '🚄'], ['bullettrain_front', '🚅'], ['train2', '🚆'], ['metro', '🚇'], ['light_rail', '🚈'], ['station', '🚉'], ['tram', '🚊'], ['monorail', '🚝'], ['mountain_railway', '🚞'], ['train', '🚋'], ['bus', '🚌'], ['oncoming_bus', '🚍'], ['trolleybus', '🚎'], ['minibus', '🚐'], ['ambulance', '🚑'], ['fire_engine', '🚒'], ['police_car', '🚓'], ['oncoming_police_car', '🚔'], ['taxi', '🚕'], ['oncoming_taxi', '🚖'], ['car', '🚗'], ['oncoming_automobile', '🚘'], ['blue_car', '🚙'], ['pickup_truck', '🛻'], ['truck', '🚚'], ['articulated_lorry', '🚛'], ['tractor', '🚜'], ['racing_car', '🏎️'], ['motorcycle', '🏍️'], ['motor_scooter', '🛵'], ['manual_wheelchair', '🦽'], ['motorized_wheelchair', '🦼'], ['auto_rickshaw', '🛺'], ['bike', '🚲'], ['kick_scooter', '🛴'], ['skateboard', '🛹'], ['roller_skate', '🛼'], ['busstop', '🚏'], ['motorway', '🛣️'], ['railway_track', '🛤️'], ['oil_drum', '🛢️'], ['fuelpump', '⛽'], ['wheel', '🛞'], ['rotating_light', '🚨'], ['traffic_light', '🚥'], ['vertical_traffic_light', '🚦'], ['stop_sign', '🛑'], ['construction', '🚧'], ['anchor', '⚓'], ['ring_buoy', '🛟'], ['boat', '⛵'], ['canoe', '🛶'], ['speedboat', '🚤'], ['passenger_ship', '🛳️'], ['ferry', '⛴️'], ['motor_boat', '🛥️'], ['ship', '🚢'], ['airplane', '✈️'], ['small_airplane', '🛩️'], ['flight_departure', '🛫'], ['flight_arrival', '🛬'], ['parachute', '🪂'], ['seat', '💺'], ['helicopter', '🚁'], ['suspension_railway', '🚟'], ['mountain_cableway', '🚠'], ['aerial_tramway', '🚡'], ['artificial_satellite', '🛰️'], ['rocket', '🚀'], ['flying_saucer', '🛸'], ['bellhop_bell', '🛎️'], ['luggage', '🧳'], ['hourglass', '⌛'], ['hourglass_flowing_sand', '⏳'], ['watch', '⌚'], ['alarm_clock', '⏰'], ['stopwatch', '⏱️'], ['timer_clock', '⏲️'], ['mantelpiece_clock', '🕰️'], ['clock12', '🕛'], ['clock1230', '🕧'], ['clock1', '🕐'], ['clock130', '🕜'], ['clock2', '🕑'], ['clock230', '🕝'], ['clock3', '🕒'], ['clock330', '🕞'], ['clock4', '🕓'], ['clock430', '🕟'], ['clock5', '🕔'], ['clock530', '🕠'], ['clock6', '🕕'], ['clock630', '🕡'], ['clock7', '🕖'], ['clock730', '🕢'], ['clock8', '🕗'], ['clock830', '🕣'], ['clock9', '🕘'], ['clock930', '🕤'], ['clock10', '🕙'], ['clock1030', '🕥'], ['clock11', '🕚'], ['clock1130', '🕦'], ['new_moon', '🌑'], ['waxing_crescent_moon', '🌒'], ['first_quarter_moon', '🌓'], ['moon', '🌔'], ['full_moon', '🌕'], ['waning_gibbous_moon', '🌖'], ['last_quarter_moon', '🌗'], ['waning_crescent_moon', '🌘'], ['crescent_moon', '🌙'], ['new_moon_with_face', '🌚'], ['first_quarter_moon_with_face', '🌛'], ['last_quarter_moon_with_face', '🌜'], ['thermometer', '🌡️'], ['sunny', '☀️'], ['full_moon_with_face', '🌝'], ['sun_with_face', '🌞'], ['ringed_planet', '🪐'], ['star', '⭐'], ['star2', '🌟'], ['stars', '🌠'], ['milky_way', '🌌'], ['cloud', '☁️'], ['partly_sunny', '⛅'], ['cloud_with_lightning_and_rain', '⛈️'], ['sun_behind_small_cloud', '🌤️'], ['sun_behind_large_cloud', '🌥️'], ['sun_behind_rain_cloud', '🌦️'], ['cloud_with_rain', '🌧️'], ['cloud_with_snow', '🌨️'], ['cloud_with_lightning', '🌩️'], ['tornado', '🌪️'], ['fog', '🌫️'], ['wind_face', '🌬️'], ['cyclone', '🌀'], ['rainbow', '🌈'], ['closed_umbrella', '🌂'], ['open_umbrella', '☂️'], ['umbrella', '☔'], ['parasol_on_ground', '⛱️'], ['zap', '⚡'], ['snowflake', '❄️'], ['snowman_with_snow', '☃️'], ['snowman', '⛄'], ['comet', '☄️'], ['fire', '🔥'], ['droplet', '💧'], ['ocean', '🌊']] + }, + { + slug: 'activities', + name: 'Activities', + emoji: [['jack_o_lantern', '🎃'], ['christmas_tree', '🎄'], ['fireworks', '🎆'], ['sparkler', '🎇'], ['firecracker', '🧨'], ['sparkles', '✨'], ['balloon', '🎈'], ['tada', '🎉'], ['confetti_ball', '🎊'], ['tanabata_tree', '🎋'], ['bamboo', '🎍'], ['dolls', '🎎'], ['flags', '🎏'], ['wind_chime', '🎐'], ['rice_scene', '🎑'], ['red_envelope', '🧧'], ['ribbon', '🎀'], ['gift', '🎁'], ['reminder_ribbon', '🎗️'], ['tickets', '🎟️'], ['ticket', '🎫'], ['medal_military', '🎖️'], ['trophy', '🏆'], ['medal_sports', '🏅'], ['1st_place_medal', '🥇'], ['2nd_place_medal', '🥈'], ['3rd_place_medal', '🥉'], ['soccer', '⚽'], ['baseball', '⚾'], ['softball', '🥎'], ['basketball', '🏀'], ['volleyball', '🏐'], ['football', '🏈'], ['rugby_football', '🏉'], ['tennis', '🎾'], ['flying_disc', '🥏'], ['bowling', '🎳'], ['cricket_game', '🏏'], ['field_hockey', '🏑'], ['ice_hockey', '🏒'], ['lacrosse', '🥍'], ['ping_pong', '🏓'], ['badminton', '🏸'], ['boxing_glove', '🥊'], ['martial_arts_uniform', '🥋'], ['goal_net', '🥅'], ['golf', '⛳'], ['ice_skate', '⛸️'], ['fishing_pole_and_fish', '🎣'], ['diving_mask', '🤿'], ['running_shirt_with_sash', '🎽'], ['ski', '🎿'], ['sled', '🛷'], ['curling_stone', '🥌'], ['dart', '🎯'], ['yo_yo', '🪀'], ['kite', '🪁'], ['gun', '🔫'], ['8ball', '🎱'], ['crystal_ball', '🔮'], ['magic_wand', '🪄'], ['video_game', '🎮'], ['joystick', '🕹️'], ['slot_machine', '🎰'], ['game_die', '🎲'], ['jigsaw', '🧩'], ['teddy_bear', '🧸'], ['pinata', '🪅'], ['mirror_ball', '🪩'], ['nesting_dolls', '🪆'], ['spades', '♠️'], ['hearts', '♥️'], ['diamonds', '♦️'], ['clubs', '♣️'], ['chess_pawn', '♟️'], ['black_joker', '🃏'], ['mahjong', '🀄'], ['flower_playing_cards', '🎴'], ['performing_arts', '🎭'], ['framed_picture', '🖼️'], ['art', '🎨'], ['thread', '🧵'], ['sewing_needle', '🪡'], ['yarn', '🧶'], ['knot', '🪢']] + }, + { + slug: 'objects', + name: 'Objects', + emoji: [['eyeglasses', '👓'], ['dark_sunglasses', '🕶️'], ['goggles', '🥽'], ['lab_coat', '🥼'], ['safety_vest', '🦺'], ['necktie', '👔'], ['shirt', '👕'], ['jeans', '👖'], ['scarf', '🧣'], ['gloves', '🧤'], ['coat', '🧥'], ['socks', '🧦'], ['dress', '👗'], ['kimono', '👘'], ['sari', '🥻'], ['one_piece_swimsuit', '🩱'], ['swim_brief', '🩲'], ['shorts', '🩳'], ['bikini', '👙'], ['womans_clothes', '👚'], ['folding_hand_fan', '🪭'], ['purse', '👛'], ['handbag', '👜'], ['pouch', '👝'], ['shopping', '🛍️'], ['school_satchel', '🎒'], ['thong_sandal', '🩴'], ['mans_shoe', '👞'], ['athletic_shoe', '👟'], ['hiking_boot', '🥾'], ['flat_shoe', '🥿'], ['high_heel', '👠'], ['sandal', '👡'], ['ballet_shoes', '🩰'], ['boot', '👢'], ['hair_pick', '🪮'], ['crown', '👑'], ['womans_hat', '👒'], ['tophat', '🎩'], ['mortar_board', '🎓'], ['billed_cap', '🧢'], ['military_helmet', '🪖'], ['rescue_worker_helmet', '⛑️'], ['prayer_beads', '📿'], ['lipstick', '💄'], ['ring', '💍'], ['gem', '💎'], ['mute', '🔇'], ['speaker', '🔈'], ['sound', '🔉'], ['loud_sound', '🔊'], ['loudspeaker', '📢'], ['mega', '📣'], ['postal_horn', '📯'], ['bell', '🔔'], ['no_bell', '🔕'], ['musical_score', '🎼'], ['musical_note', '🎵'], ['notes', '🎶'], ['studio_microphone', '🎙️'], ['level_slider', '🎚️'], ['control_knobs', '🎛️'], ['microphone', '🎤'], ['headphones', '🎧'], ['radio', '📻'], ['saxophone', '🎷'], ['trumpet', '🎺'], ['accordion', '🪗'], ['guitar', '🎸'], ['musical_keyboard', '🎹'], ['violin', '🎻'], ['banjo', '🪕'], ['drum', '🥁'], ['long_drum', '🪘'], ['maracas', '🪇'], ['flute', '🪈'], ['iphone', '📱'], ['calling', '📲'], ['phone', '☎️'], ['telephone_receiver', '📞'], ['pager', '📟'], ['fax', '📠'], ['battery', '🔋'], ['low_battery', '🪫'], ['electric_plug', '🔌'], ['computer', '💻'], ['desktop_computer', '🖥️'], ['printer', '🖨️'], ['keyboard', '⌨️'], ['computer_mouse', '🖱️'], ['trackball', '🖲️'], ['minidisc', '💽'], ['floppy_disk', '💾'], ['cd', '💿'], ['dvd', '📀'], ['abacus', '🧮'], ['movie_camera', '🎥'], ['film_strip', '🎞️'], ['film_projector', '📽️'], ['clapper', '🎬'], ['tv', '📺'], ['camera', '📷'], ['camera_flash', '📸'], ['video_camera', '📹'], ['vhs', '📼'], ['mag', '🔍'], ['mag_right', '🔎'], ['candle', '🕯️'], ['bulb', '💡'], ['flashlight', '🔦'], ['izakaya_lantern', '🏮'], ['diya_lamp', '🪔'], ['notebook_with_decorative_cover', '📔'], ['closed_book', '📕'], ['book', '📖'], ['green_book', '📗'], ['blue_book', '📘'], ['orange_book', '📙'], ['books', '📚'], ['notebook', '📓'], ['ledger', '📒'], ['page_with_curl', '📃'], ['scroll', '📜'], ['page_facing_up', '📄'], ['newspaper', '📰'], ['newspaper_roll', '🗞️'], ['bookmark_tabs', '📑'], ['bookmark', '🔖'], ['label', '🏷️'], ['coin', '🪙'], ['moneybag', '💰'], ['yen', '💴'], ['dollar', '💵'], ['euro', '💶'], ['pound', '💷'], ['money_with_wings', '💸'], ['credit_card', '💳'], ['receipt', '🧾'], ['chart', '💹'], ['envelope', '✉️'], ['email', '📧'], ['incoming_envelope', '📨'], ['envelope_with_arrow', '📩'], ['outbox_tray', '📤'], ['inbox_tray', '📥'], ['package', '📦'], ['mailbox', '📫'], ['mailbox_closed', '📪'], ['mailbox_with_mail', '📬'], ['mailbox_with_no_mail', '📭'], ['postbox', '📮'], ['ballot_box', '🗳️'], ['pencil2', '✏️'], ['black_nib', '✒️'], ['fountain_pen', '🖋️'], ['pen', '🖊️'], ['paintbrush', '🖌️'], ['crayon', '🖍️'], ['memo', '📝'], ['briefcase', '💼'], ['file_folder', '📁'], ['open_file_folder', '📂'], ['card_index_dividers', '🗂️'], ['date', '📅'], ['calendar', '📆'], ['spiral_notepad', '🗒️'], ['spiral_calendar', '🗓️'], ['card_index', '📇'], ['chart_with_upwards_trend', '📈'], ['chart_with_downwards_trend', '📉'], ['bar_chart', '📊'], ['clipboard', '📋'], ['pushpin', '📌'], ['round_pushpin', '📍'], ['paperclip', '📎'], ['paperclips', '🖇️'], ['straight_ruler', '📏'], ['triangular_ruler', '📐'], ['scissors', '✂️'], ['card_file_box', '🗃️'], ['file_cabinet', '🗄️'], ['wastebasket', '🗑️'], ['lock', '🔒'], ['unlock', '🔓'], ['lock_with_ink_pen', '🔏'], ['closed_lock_with_key', '🔐'], ['key', '🔑'], ['old_key', '🗝️'], ['hammer', '🔨'], ['axe', '🪓'], ['pick', '⛏️'], ['hammer_and_pick', '⚒️'], ['hammer_and_wrench', '🛠️'], ['dagger', '🗡️'], ['crossed_swords', '⚔️'], ['bomb', '💣'], ['boomerang', '🪃'], ['bow_and_arrow', '🏹'], ['shield', '🛡️'], ['carpentry_saw', '🪚'], ['wrench', '🔧'], ['screwdriver', '🪛'], ['nut_and_bolt', '🔩'], ['gear', '⚙️'], ['clamp', '🗜️'], ['balance_scale', '⚖️'], ['probing_cane', '🦯'], ['link', '🔗'], ['chains', '⛓️'], ['hook', '🪝'], ['toolbox', '🧰'], ['magnet', '🧲'], ['ladder', '🪜'], ['alembic', '⚗️'], ['test_tube', '🧪'], ['petri_dish', '🧫'], ['dna', '🧬'], ['microscope', '🔬'], ['telescope', '🔭'], ['satellite', '📡'], ['syringe', '💉'], ['drop_of_blood', '🩸'], ['pill', '💊'], ['adhesive_bandage', '🩹'], ['crutch', '🩼'], ['stethoscope', '🩺'], ['x_ray', '🩻'], ['door', '🚪'], ['elevator', '🛗'], ['mirror', '🪞'], ['window', '🪟'], ['bed', '🛏️'], ['couch_and_lamp', '🛋️'], ['chair', '🪑'], ['toilet', '🚽'], ['plunger', '🪠'], ['shower', '🚿'], ['bathtub', '🛁'], ['mouse_trap', '🪤'], ['razor', '🪒'], ['lotion_bottle', '🧴'], ['safety_pin', '🧷'], ['broom', '🧹'], ['basket', '🧺'], ['roll_of_paper', '🧻'], ['bucket', '🪣'], ['soap', '🧼'], ['bubbles', '🫧'], ['toothbrush', '🪥'], ['sponge', '🧽'], ['fire_extinguisher', '🧯'], ['shopping_cart', '🛒'], ['smoking', '🚬'], ['coffin', '⚰️'], ['headstone', '🪦'], ['funeral_urn', '⚱️'], ['nazar_amulet', '🧿'], ['hamsa', '🪬'], ['moyai', '🗿'], ['placard', '🪧'], ['identification_card', '🪪']] + }, + { + slug: 'symbols', + name: 'Symbols', + emoji: [['atm', '🏧'], ['put_litter_in_its_place', '🚮'], ['potable_water', '🚰'], ['wheelchair', '♿'], ['mens', '🚹'], ['womens', '🚺'], ['restroom', '🚻'], ['baby_symbol', '🚼'], ['wc', '🚾'], ['passport_control', '🛂'], ['customs', '🛃'], ['baggage_claim', '🛄'], ['left_luggage', '🛅'], ['warning', '⚠️'], ['children_crossing', '🚸'], ['no_entry', '⛔'], ['no_entry_sign', '🚫'], ['no_bicycles', '🚳'], ['no_smoking', '🚭'], ['do_not_litter', '🚯'], ['non-potable_water', '🚱'], ['no_pedestrians', '🚷'], ['no_mobile_phones', '📵'], ['underage', '🔞'], ['radioactive', '☢️'], ['biohazard', '☣️'], ['arrow_up', '⬆️'], ['arrow_upper_right', '↗️'], ['arrow_right', '➡️'], ['arrow_lower_right', '↘️'], ['arrow_down', '⬇️'], ['arrow_lower_left', '↙️'], ['arrow_left', '⬅️'], ['arrow_upper_left', '↖️'], ['arrow_up_down', '↕️'], ['left_right_arrow', '↔️'], ['leftwards_arrow_with_hook', '↩️'], ['arrow_right_hook', '↪️'], ['arrow_heading_up', '⤴️'], ['arrow_heading_down', '⤵️'], ['arrows_clockwise', '🔃'], ['arrows_counterclockwise', '🔄'], ['back', '🔙'], ['end', '🔚'], ['on', '🔛'], ['soon', '🔜'], ['top', '🔝'], ['place_of_worship', '🛐'], ['atom_symbol', '⚛️'], ['om', '🕉️'], ['star_of_david', '✡️'], ['wheel_of_dharma', '☸️'], ['yin_yang', '☯️'], ['latin_cross', '✝️'], ['orthodox_cross', '☦️'], ['star_and_crescent', '☪️'], ['peace_symbol', '☮️'], ['menorah', '🕎'], ['six_pointed_star', '🔯'], ['khanda', '🪯'], ['aries', '♈'], ['taurus', '♉'], ['gemini', '♊'], ['cancer', '♋'], ['leo', '♌'], ['virgo', '♍'], ['libra', '♎'], ['scorpius', '♏'], ['sagittarius', '♐'], ['capricorn', '♑'], ['aquarius', '♒'], ['pisces', '♓'], ['ophiuchus', '⛎'], ['twisted_rightwards_arrows', '🔀'], ['repeat', '🔁'], ['repeat_one', '🔂'], ['arrow_forward', '▶️'], ['fast_forward', '⏩'], ['next_track_button', '⏭️'], ['play_or_pause_button', '⏯️'], ['arrow_backward', '◀️'], ['rewind', '⏪'], ['previous_track_button', '⏮️'], ['arrow_up_small', '🔼'], ['arrow_double_up', '⏫'], ['arrow_down_small', '🔽'], ['arrow_double_down', '⏬'], ['pause_button', '⏸️'], ['stop_button', '⏹️'], ['record_button', '⏺️'], ['eject_button', '⏏️'], ['cinema', '🎦'], ['low_brightness', '🔅'], ['high_brightness', '🔆'], ['signal_strength', '📶'], ['wireless', '🛜'], ['vibration_mode', '📳'], ['mobile_phone_off', '📴'], ['female_sign', '♀️'], ['male_sign', '♂️'], ['transgender_symbol', '⚧️'], ['heavy_multiplication_x', '✖️'], ['heavy_plus_sign', '➕'], ['heavy_minus_sign', '➖'], ['heavy_division_sign', '➗'], ['heavy_equals_sign', '🟰'], ['infinity', '♾️'], ['bangbang', '‼️'], ['interrobang', '⁉️'], ['question', '❓'], ['grey_question', '❔'], ['grey_exclamation', '❕'], ['exclamation', '❗'], ['wavy_dash', '〰️'], ['currency_exchange', '💱'], ['heavy_dollar_sign', '💲'], ['medical_symbol', '⚕️'], ['recycle', '♻️'], ['fleur_de_lis', '⚜️'], ['trident', '🔱'], ['name_badge', '📛'], ['beginner', '🔰'], ['o', '⭕'], ['white_check_mark', '✅'], ['ballot_box_with_check', '☑️'], ['heavy_check_mark', '✔️'], ['x', '❌'], ['negative_squared_cross_mark', '❎'], ['curly_loop', '➰'], ['loop', '➿'], ['part_alternation_mark', '〽️'], ['eight_spoked_asterisk', '✳️'], ['eight_pointed_black_star', '✴️'], ['sparkle', '❇️'], ['copyright', '©️'], ['registered', '®️'], ['tm', '™️'], ['hash', '#️⃣'], ['asterisk', '*️⃣'], ['zero', '0️⃣'], ['one', '1️⃣'], ['two', '2️⃣'], ['three', '3️⃣'], ['four', '4️⃣'], ['five', '5️⃣'], ['six', '6️⃣'], ['seven', '7️⃣'], ['eight', '8️⃣'], ['nine', '9️⃣'], ['keycap_ten', '🔟'], ['capital_abcd', '🔠'], ['abcd', '🔡'], ['1234', '🔢'], ['symbols', '🔣'], ['abc', '🔤'], ['a', '🅰️'], ['ab', '🆎'], ['b', '🅱️'], ['cl', '🆑'], ['cool', '🆒'], ['free', '🆓'], ['information_source', 'ℹ️'], ['id', '🆔'], ['m', 'Ⓜ️'], ['new', '🆕'], ['ng', '🆖'], ['o2', '🅾️'], ['ok', '🆗'], ['parking', '🅿️'], ['sos', '🆘'], ['up', '🆙'], ['vs', '🆚'], ['koko', '🈁'], ['sa', '🈂️'], ['ideograph_advantage', '🉐'], ['accept', '🉑'], ['congratulations', '㊗️'], ['secret', '㊙️'], ['u6e80', '🈵'], ['red_circle', '🔴'], ['orange_circle', '🟠'], ['yellow_circle', '🟡'], ['green_circle', '🟢'], ['large_blue_circle', '🔵'], ['purple_circle', '🟣'], ['brown_circle', '🟤'], ['black_circle', '⚫'], ['white_circle', '⚪'], ['red_square', '🟥'], ['orange_square', '🟧'], ['yellow_square', '🟨'], ['green_square', '🟩'], ['blue_square', '🟦'], ['purple_square', '🟪'], ['brown_square', '🟫'], ['black_large_square', '⬛'], ['white_large_square', '⬜'], ['black_medium_square', '◼️'], ['white_medium_square', '◻️'], ['black_medium_small_square', '◾'], ['white_medium_small_square', '◽'], ['black_small_square', '▪️'], ['white_small_square', '▫️'], ['large_orange_diamond', '🔶'], ['large_blue_diamond', '🔷'], ['small_orange_diamond', '🔸'], ['small_blue_diamond', '🔹'], ['small_red_triangle', '🔺'], ['small_red_triangle_down', '🔻'], ['diamond_shape_with_a_dot_inside', '💠'], ['radio_button', '🔘'], ['white_square_button', '🔳'], ['black_square_button', '🔲']] + }, + { + slug: 'flags', + name: 'Flags', + emoji: [['checkered_flag', '🏁'], ['triangular_flag_on_post', '🚩'], ['crossed_flags', '🎌'], ['black_flag', '🏴'], ['white_flag', '🏳️'], ['rainbow_flag', '🏳️‍🌈'], ['transgender_flag', '🏳️‍⚧️'], ['pirate_flag', '🏴‍☠️'], ['ascension_island', '🇦🇨'], ['andorra', '🇦🇩'], ['united_arab_emirates', '🇦🇪'], ['afghanistan', '🇦🇫'], ['antigua_barbuda', '🇦🇬'], ['anguilla', '🇦🇮'], ['albania', '🇦🇱'], ['armenia', '🇦🇲'], ['angola', '🇦🇴'], ['antarctica', '🇦🇶'], ['argentina', '🇦🇷'], ['american_samoa', '🇦🇸'], ['austria', '🇦🇹'], ['australia', '🇦🇺'], ['aruba', '🇦🇼'], ['aland_islands', '🇦🇽'], ['azerbaijan', '🇦🇿'], ['bosnia_herzegovina', '🇧🇦'], ['barbados', '🇧🇧'], ['bangladesh', '🇧🇩'], ['belgium', '🇧🇪'], ['burkina_faso', '🇧🇫'], ['bulgaria', '🇧🇬'], ['bahrain', '🇧🇭'], ['burundi', '🇧🇮'], ['benin', '🇧🇯'], ['st_barthelemy', '🇧🇱'], ['bermuda', '🇧🇲'], ['brunei', '🇧🇳'], ['bolivia', '🇧🇴'], ['caribbean_netherlands', '🇧🇶'], ['brazil', '🇧🇷'], ['bahamas', '🇧🇸'], ['bhutan', '🇧🇹'], ['bouvet_island', '🇧🇻'], ['botswana', '🇧🇼'], ['belarus', '🇧🇾'], ['belize', '🇧🇿'], ['canada', '🇨🇦'], ['cocos_islands', '🇨🇨'], ['congo_kinshasa', '🇨🇩'], ['central_african_republic', '🇨🇫'], ['congo_brazzaville', '🇨🇬'], ['switzerland', '🇨🇭'], ['cote_divoire', '🇨🇮'], ['cook_islands', '🇨🇰'], ['chile', '🇨🇱'], ['cameroon', '🇨🇲'], ['cn', '🇨🇳'], ['colombia', '🇨🇴'], ['clipperton_island', '🇨🇵'], ['costa_rica', '🇨🇷'], ['cuba', '🇨🇺'], ['cape_verde', '🇨🇻'], ['curacao', '🇨🇼'], ['christmas_island', '🇨🇽'], ['cyprus', '🇨🇾'], ['czech_republic', '🇨🇿'], ['de', '🇩🇪'], ['diego_garcia', '🇩🇬'], ['djibouti', '🇩🇯'], ['denmark', '🇩🇰'], ['dominica', '🇩🇲'], ['dominican_republic', '🇩🇴'], ['algeria', '🇩🇿'], ['ceuta_melilla', '🇪🇦'], ['ecuador', '🇪🇨'], ['estonia', '🇪🇪'], ['egypt', '🇪🇬'], ['western_sahara', '🇪🇭'], ['eritrea', '🇪🇷'], ['es', '🇪🇸'], ['ethiopia', '🇪🇹'], ['eu', '🇪🇺'], ['finland', '🇫🇮'], ['fiji', '🇫🇯'], ['falkland_islands', '🇫🇰'], ['micronesia', '🇫🇲'], ['faroe_islands', '🇫🇴'], ['fr', '🇫🇷'], ['gabon', '🇬🇦'], ['gb', '🇬🇧'], ['grenada', '🇬🇩'], ['georgia', '🇬🇪'], ['french_guiana', '🇬🇫'], ['guernsey', '🇬🇬'], ['ghana', '🇬🇭'], ['gibraltar', '🇬🇮'], ['greenland', '🇬🇱'], ['gambia', '🇬🇲'], ['guinea', '🇬🇳'], ['guadeloupe', '🇬🇵'], ['equatorial_guinea', '🇬🇶'], ['greece', '🇬🇷'], ['south_georgia_south_sandwich_islands', '🇬🇸'], ['guatemala', '🇬🇹'], ['guam', '🇬🇺'], ['guinea_bissau', '🇬🇼'], ['guyana', '🇬🇾'], ['hong_kong', '🇭🇰'], ['heard_mcdonald_islands', '🇭🇲'], ['honduras', '🇭🇳'], ['croatia', '🇭🇷'], ['haiti', '🇭🇹'], ['hungary', '🇭🇺'], ['canary_islands', '🇮🇨'], ['indonesia', '🇮🇩'], ['ireland', '🇮🇪'], ['israel', '🇮🇱'], ['isle_of_man', '🇮🇲'], ['india', '🇮🇳'], ['british_indian_ocean_territory', '🇮🇴'], ['iraq', '🇮🇶'], ['iran', '🇮🇷'], ['iceland', '🇮🇸'], ['it', '🇮🇹'], ['jersey', '🇯🇪'], ['jamaica', '🇯🇲'], ['jordan', '🇯🇴'], ['jp', '🇯🇵'], ['kenya', '🇰🇪'], ['kyrgyzstan', '🇰🇬'], ['cambodia', '🇰🇭'], ['kiribati', '🇰🇮'], ['comoros', '🇰🇲'], ['st_kitts_nevis', '🇰🇳'], ['north_korea', '🇰🇵'], ['kr', '🇰🇷'], ['kuwait', '🇰🇼'], ['cayman_islands', '🇰🇾'], ['kazakhstan', '🇰🇿'], ['laos', '🇱🇦'], ['lebanon', '🇱🇧'], ['st_lucia', '🇱🇨'], ['liechtenstein', '🇱🇮'], ['sri_lanka', '🇱🇰'], ['liberia', '🇱🇷'], ['lesotho', '🇱🇸'], ['lithuania', '🇱🇹'], ['luxembourg', '🇱🇺'], ['latvia', '🇱🇻'], ['libya', '🇱🇾'], ['morocco', '🇲🇦'], ['monaco', '🇲🇨'], ['moldova', '🇲🇩'], ['montenegro', '🇲🇪'], ['st_martin', '🇲🇫'], ['madagascar', '🇲🇬'], ['marshall_islands', '🇲🇭'], ['macedonia', '🇲🇰'], ['mali', '🇲🇱'], ['myanmar', '🇲🇲'], ['mongolia', '🇲🇳'], ['macau', '🇲🇴'], ['northern_mariana_islands', '🇲🇵'], ['martinique', '🇲🇶'], ['mauritania', '🇲🇷'], ['montserrat', '🇲🇸'], ['malta', '🇲🇹'], ['mauritius', '🇲🇺'], ['maldives', '🇲🇻'], ['malawi', '🇲🇼'], ['mexico', '🇲🇽'], ['malaysia', '🇲🇾'], ['mozambique', '🇲🇿'], ['namibia', '🇳🇦'], ['new_caledonia', '🇳🇨'], ['niger', '🇳🇪'], ['norfolk_island', '🇳🇫'], ['nigeria', '🇳🇬'], ['nicaragua', '🇳🇮'], ['netherlands', '🇳🇱'], ['norway', '🇳🇴'], ['nepal', '🇳🇵'], ['nauru', '🇳🇷'], ['niue', '🇳🇺'], ['new_zealand', '🇳🇿'], ['oman', '🇴🇲'], ['panama', '🇵🇦'], ['peru', '🇵🇪'], ['french_polynesia', '🇵🇫'], ['papua_new_guinea', '🇵🇬'], ['philippines', '🇵🇭'], ['pakistan', '🇵🇰'], ['poland', '🇵🇱'], ['st_pierre_miquelon', '🇵🇲'], ['pitcairn_islands', '🇵🇳'], ['puerto_rico', '🇵🇷'], ['palestinian_territories', '🇵🇸'], ['portugal', '🇵🇹'], ['palau', '🇵🇼'], ['paraguay', '🇵🇾'], ['qatar', '🇶🇦'], ['reunion', '🇷🇪'], ['romania', '🇷🇴'], ['serbia', '🇷🇸'], ['ru', '🇷🇺'], ['rwanda', '🇷🇼'], ['saudi_arabia', '🇸🇦'], ['solomon_islands', '🇸🇧'], ['seychelles', '🇸🇨'], ['sudan', '🇸🇩'], ['sweden', '🇸🇪'], ['singapore', '🇸🇬'], ['st_helena', '🇸🇭'], ['slovenia', '🇸🇮'], ['svalbard_jan_mayen', '🇸🇯'], ['slovakia', '🇸🇰'], ['sierra_leone', '🇸🇱'], ['san_marino', '🇸🇲'], ['senegal', '🇸🇳'], ['somalia', '🇸🇴'], ['suriname', '🇸🇷'], ['south_sudan', '🇸🇸'], ['sao_tome_principe', '🇸🇹'], ['el_salvador', '🇸🇻'], ['sint_maarten', '🇸🇽'], ['syria', '🇸🇾'], ['swaziland', '🇸🇿'], ['tristan_da_cunha', '🇹🇦'], ['turks_caicos_islands', '🇹🇨'], ['chad', '🇹🇩'], ['french_southern_territories', '🇹🇫'], ['togo', '🇹🇬'], ['thailand', '🇹🇭'], ['tajikistan', '🇹🇯'], ['tokelau', '🇹🇰'], ['timor_leste', '🇹🇱'], ['turkmenistan', '🇹🇲'], ['tunisia', '🇹🇳'], ['tonga', '🇹🇴'], ['tr', '🇹🇷'], ['trinidad_tobago', '🇹🇹'], ['tuvalu', '🇹🇻'], ['taiwan', '🇹🇼'], ['tanzania', '🇹🇿'], ['ukraine', '🇺🇦'], ['uganda', '🇺🇬'], ['us_outlying_islands', '🇺🇲'], ['united_nations', '🇺🇳'], ['us', '🇺🇸'], ['uruguay', '🇺🇾'], ['uzbekistan', '🇺🇿'], ['vatican_city', '🇻🇦'], ['st_vincent_grenadines', '🇻🇨'], ['venezuela', '🇻🇪'], ['british_virgin_islands', '🇻🇬'], ['us_virgin_islands', '🇻🇮'], ['vietnam', '🇻🇳'], ['vanuatu', '🇻🇺'], ['wallis_futuna', '🇼🇫'], ['samoa', '🇼🇸'], ['kosovo', '🇽🇰'], ['yemen', '🇾🇪'], ['mayotte', '🇾🇹'], ['south_africa', '🇿🇦'], ['zambia', '🇿🇲'], ['zimbabwe', '🇿🇼'], ['england', '🏴󠁧󠁢󠁥󠁮󠁧󠁿'], ['scotland', '🏴󠁧󠁢󠁳󠁣󠁴󠁿'], ['wales', '🏴󠁧󠁢󠁷󠁬󠁳󠁿']] + } +] diff --git a/frontend/src/assets/icons.generated.js b/frontend/src/assets/icons.generated.js index 2f3e17f4b..644b7c651 100644 --- a/frontend/src/assets/icons.generated.js +++ b/frontend/src/assets/icons.generated.js @@ -5,7 +5,7 @@ never waits on (or depends on) the icon service. Regenerate with `npm run icons` after adding or removing an icon; `check-icons.mjs` fails the build if this drifts. - 240 icons. + 249 icons. */ export const BUNDLED_ICONS = { "la:angle-double-right": {"body":"","width":32,"height":32}, @@ -37,7 +37,6 @@ export const BUNDLED_ICONS = { "la:clipboard": {"body":"","width":32,"height":32}, "la:clipboard-list": {"body":"","width":32,"height":32}, "la:clock": {"body":"","width":32,"height":32}, - "la:cloud-download-alt": {"body":"","width":32,"height":32}, "la:cloud-upload-alt": {"body":"","width":32,"height":32}, "la:code": {"body":"","width":32,"height":32}, "la:cog": {"body":"","width":32,"height":32}, @@ -149,7 +148,9 @@ export const BUNDLED_ICONS = { "mdi:alert-box": {"body":"","width":24,"height":24}, "mdi:alpha-t-box-outline": {"body":"","width":24,"height":24}, "mdi:arrow-vertical-lock": {"body":"","width":24,"height":24}, + "mdi:basketball": {"body":"","width":24,"height":24}, "mdi:book-plus": {"body":"","width":24,"height":24}, + "mdi:car": {"body":"","width":24,"height":24}, "mdi:chart-multiline": {"body":"","width":24,"height":24}, "mdi:check": {"body":"","width":24,"height":24}, "mdi:check-circle": {"body":"","width":24,"height":24}, @@ -159,23 +160,28 @@ export const BUNDLED_ICONS = { "mdi:chevron-left": {"body":"","width":24,"height":24}, "mdi:chevron-right": {"body":"","width":24,"height":24}, "mdi:clipboard-text-outline": {"body":"","width":24,"height":24}, + "mdi:clock-outline": {"body":"","width":24,"height":24}, "mdi:close": {"body":"","width":24,"height":24}, "mdi:close-box": {"body":"","width":24,"height":24}, "mdi:code-json": {"body":"","width":24,"height":24}, "mdi:code-tags": {"body":"","width":24,"height":24}, "mdi:cog": {"body":"","width":24,"height":24}, "mdi:cog-box": {"body":"","width":24,"height":24}, - "mdi:cookie-plus": {"body":"","width":24,"height":24}, "mdi:crop-square": {"body":"","width":24,"height":24}, "mdi:database-refresh": {"body":"","width":24,"height":24}, "mdi:dice-5": {"body":"","width":24,"height":24}, + "mdi:dog": {"body":"","width":24,"height":24}, "mdi:drag-horizontal": {"body":"","width":24,"height":24}, + "mdi:emoticon-outline": {"body":"","width":24,"height":24}, + "mdi:emoticon-plus-outline": {"body":"","width":24,"height":24}, "mdi:exclamation": {"body":"","width":24,"height":24}, "mdi:eye": {"body":"","width":24,"height":24}, "mdi:eye-off": {"body":"","width":24,"height":24}, "mdi:eye-off-outline": {"body":"","width":24,"height":24}, "mdi:file-document-outline": {"body":"","width":24,"height":24}, "mdi:file-search-outline": {"body":"","width":24,"height":24}, + "mdi:flag-outline": {"body":"","width":24,"height":24}, + "mdi:food-apple-outline": {"body":"","width":24,"height":24}, "mdi:format-align-center": {"body":"","width":24,"height":24}, "mdi:format-align-justify": {"body":"","width":24,"height":24}, "mdi:format-align-left": {"body":"","width":24,"height":24}, @@ -204,6 +210,7 @@ export const BUNDLED_ICONS = { "mdi:format-subscript": {"body":"","width":24,"height":24}, "mdi:format-superscript": {"body":"","width":24,"height":24}, "mdi:format-title": {"body":"","width":24,"height":24}, + "mdi:hand-wave-outline": {"body":"","width":24,"height":24}, "mdi:image-plus": {"body":"","width":24,"height":24}, "mdi:image-plus-outline": {"body":"","width":24,"height":24}, "mdi:image-sync-outline": {"body":"","width":24,"height":24}, @@ -211,6 +218,7 @@ export const BUNDLED_ICONS = { "mdi:information": {"body":"","width":24,"height":24}, "mdi:information-box": {"body":"","width":24,"height":24}, "mdi:keyboard-variant": {"body":"","width":24,"height":24}, + "mdi:lightbulb-outline": {"body":"","width":24,"height":24}, "mdi:line-scan": {"body":"","width":24,"height":24}, "mdi:link-variant": {"body":"","width":24,"height":24}, "mdi:link-variant-plus": {"body":"","width":24,"height":24}, @@ -222,6 +230,7 @@ export const BUNDLED_ICONS = { "mdi:minus": {"body":"","width":24,"height":24}, "mdi:near-me": {"body":"","width":24,"height":24}, "mdi:palette": {"body":"","width":24,"height":24}, + "mdi:percent-outline": {"body":"","width":24,"height":24}, "mdi:play": {"body":"","width":24,"height":24}, "mdi:playlist-edit": {"body":"","width":24,"height":24}, "mdi:redo-variant": {"body":"","width":24,"height":24}, diff --git a/frontend/src/components/EditorCodeBlockMenu.vue b/frontend/src/components/EditorCodeBlockMenu.vue new file mode 100644 index 000000000..4c58caf76 --- /dev/null +++ b/frontend/src/components/EditorCodeBlockMenu.vue @@ -0,0 +1,204 @@ + + + + + diff --git a/frontend/src/components/EditorEmojiMenu.vue b/frontend/src/components/EditorEmojiMenu.vue new file mode 100644 index 000000000..e806375ef --- /dev/null +++ b/frontend/src/components/EditorEmojiMenu.vue @@ -0,0 +1,421 @@ + + + + + diff --git a/frontend/src/components/EditorMarkdown.vue b/frontend/src/components/EditorMarkdown.vue index 102bd8b54..60733ed12 100644 --- a/frontend/src/components/EditorMarkdown.vue +++ b/frontend/src/components/EditorMarkdown.vue @@ -5,63 +5,60 @@ - - {{ t('editor.markup.insertLink') }} + + {{ + t('editor.markup.insertLink') + }} - - - - - - - - - From File Manager... - - - - - - From Clipboard... - - - - - - From Remote URL... - - - - {{ t('editor.markup.insertAssets') }} + + + {{ + t('editor.markup.insertAssets') + }} - - {{ t('editor.markup.insertCodeBlock') }} + + + {{ + t('editor.markup.insertCodeBlock') + }} - {{ t('editor.markup.insertTable') }} + {{ + t('editor.markup.insertTable') + }} - {{ t('editor.markup.insertTabset') }} + {{ + t('editor.markup.insertTabset') + }} - {{ t('editor.markup.insertBlock') }} + {{ + t('editor.markup.insertBlock') + }} - {{ t('editor.markup.insertDiagram') }} + {{ + t('editor.markup.insertDiagram') + }} - {{ t('editor.markup.insertFootnote') }} + {{ + t('editor.markup.insertFootnote') + }} - - {{ t('editor.markup.insertEmoji') }} + + + {{ + t('editor.markup.insertEmoji') + }} - {{ t('editor.markup.insertHorizontalBar') }} + {{ + t('editor.markup.insertHorizontalBar') + }} Markdown @@ -72,31 +69,41 @@
- {{ t('editor.markup.bold') }} + {{ + t('editor.markup.bold') + }} - {{ t('editor.markup.italic') }} + {{ + t('editor.markup.italic') + }} - {{ t('editor.markup.strikethrough') }} + {{ + t('editor.markup.strikethrough') + }} - {{ t('editor.markup.header') }} + {{ + t('editor.markup.header') + }} - {{ t('editor.markup.headerLevel', { level: lvl }) }} + {{ + t('editor.markup.headerLevel', { level: lvl }) + }} @@ -106,26 +113,32 @@ padding="xs sm" flat @click="toggleMarkup({ start: `~` })"> - {{ t('editor.markup.subscript') }} + {{ + t('editor.markup.subscript') + }} - {{ t('editor.markup.superscript') }} + {{ + t('editor.markup.superscript') + }} - {{ t('editor.markup.blockquoteAdmonitions') }} + {{ + t('editor.markup.blockquoteAdmonitions') + }} - + {{ t('editor.markup.blockquote') }} + @click="insertBeforeEachLine({ content: `> `, after: `{.is-info}` })"> @@ -133,7 +146,7 @@ + @click="insertBeforeEachLine({ content: `> `, after: `{.is-success}` })"> @@ -141,7 +154,7 @@ + @click="insertBeforeEachLine({ content: `> `, after: `{.is-warning}` })"> @@ -149,7 +162,7 @@ + @click="insertBeforeEachLine({ content: `> `, after: `{.is-danger}` })"> @@ -162,40 +175,50 @@ icon="mdi:format-list-bulleted" padding="xs sm" flat - @click="insertBeforeEachLine({ content: `- `})"> - {{ t('editor.markup.unorderedList') }} + @click="insertBeforeEachLine({ content: `- ` })"> + {{ + t('editor.markup.unorderedList') + }} - {{ t('editor.markup.orderedList') }} + @click="insertBeforeEachLine({ content: `1. ` })"> + {{ + t('editor.markup.orderedList') + }} - {{ t('editor.markup.taskList') }} + {{ + t('editor.markup.taskList') + }} - + {{ t('editor.markup.taskListUnchecked') }} - + {{ t('editor.markup.taskListChecked') }} - - {{ t('editor.markup.inlineCode') }} + + {{ + t('editor.markup.inlineCode') + }} - {{ t('editor.markup.keyboardKey') }} + {{ + t('editor.markup.keyboardKey') + }}
@@ -206,7 +229,9 @@
- {{ t('editor.renderPreview') }} + {{ t('editor.renderPreview') }} - {{ t('editor.toggleScrollSync') }} + {{ + t('editor.toggleScrollSync') + }} - {{ t('editor.togglePreviewPane') }} + {{ + t('editor.togglePreviewPane') + }}
+ + + + + + + + + + + + + + + + + + + + + + + + +
+
{{ t('linkPicker.selection') }}
+ +
+
+ + + + + + + + + + + + + diff --git a/frontend/src/components/PageHeader.vue b/frontend/src/components/PageHeader.vue index 07d909f26..4643ecc4f 100644 --- a/frontend/src/components/PageHeader.vue +++ b/frontend/src/components/PageHeader.vue @@ -13,52 +13,53 @@ style="min-height: 64px"> - - -
+
- {{ pageStore.title }} - + + {{ pageStore.title }}
- {{ pageStore.description }} - + + {{ pageStore.description }}
@@ -191,7 +192,7 @@ + + diff --git a/frontend/src/components/PageRelationDialog.vue b/frontend/src/components/PageRelationDialog.vue index 228b7f5fa..275646af2 100644 --- a/frontend/src/components/PageRelationDialog.vue +++ b/frontend/src/components/PageRelationDialog.vue @@ -50,11 +50,18 @@
{{ t('editor.pageRel.target') }}
- +
+ + +
+ {{ state.target || '—' }} +
+
{{ t('editor.pageRel.preview') }}
{ + state.target = href + }) +} + function create() { pageStore.$patch({ relations: [ diff --git a/frontend/src/components/TableEditorOverlay.vue b/frontend/src/components/TableEditorOverlay.vue index b45314d26..5c0124990 100644 --- a/frontend/src/components/TableEditorOverlay.vue +++ b/frontend/src/components/TableEditorOverlay.vue @@ -2,16 +2,16 @@ - {{t(`editor.tableEditor.title`)}} + {{ t(`editor.tableEditor.title`) }} @@ -27,33 +27,146 @@ push color="positive" text-color="white" - :label="t(`common.actions.save`)" - :aria-label="t(`common.actions.save`)" + :label="t(`common.actions.insert`)" + :aria-label="t(`common.actions.insert`)" icon="la:check" - :disabled="state.loading > 0" /> + @click="insert" /> -
- - - +
+ + + +
+ {{ t('editor.tableEditor.pasteHint') }} +
+
+ +
+ + + + + + + + + + + + + + + + +
+ +
+ + + {{ t('editor.tableEditor.align') }}: {{ t(ALIGN_LABELS[align]) }} + + + + {{ t('editor.tableEditor.removeColumn') }} + +
+
+
+ + +
+ + + + {{ t('editor.tableEditor.removeRow') }} + +
+
+ +
{{ t('editor.tableEditor.markdown') }}
+
{{ markdown }}
+ + diff --git a/frontend/src/components/shared/WInput.vue b/frontend/src/components/shared/WInput.vue index 6f3280ba7..be4dba4ec 100644 --- a/frontend/src/components/shared/WInput.vue +++ b/frontend/src/components/shared/WInput.vue @@ -174,6 +174,20 @@ const props = defineProps({ type: Boolean, default: false }, + /** + * Drop the field's own surface and let whatever is behind it show through. + * + * For a field on a surface that is not flat: a translucent (acrylic) menu, where the field's white + * would sit as an opaque slab on a panel meant to be see-through — and where the floating label, + * riding the top border, would have that slab on one side of it and the blur on the other. + * + * Only the fill goes. The border, the focus ring and the label's notch are untouched, so the field is + * still obviously a field. + */ + transparent: { + type: Boolean, + default: false + }, dense: { type: Boolean, default: false @@ -340,8 +354,16 @@ const controlClasses = computed(() => [ The dark value is translucent black rather than a fixed tone so it holds up on each of those surfaces; the light one can be flat white because that IS the surface a field should present. + + `transparent` opts out, for the surfaces where that reasoning inverts -- see the prop. */ - props.outlined ? 'bg-white dark:bg-black/20' : 'rounded-b-none bg-black/4 dark:bg-white/6', + props.transparent + ? props.outlined + ? '' + : 'rounded-b-none' + : props.outlined + ? 'bg-white dark:bg-black/20' + : 'rounded-b-none bg-black/4 dark:bg-white/6', props.disable || props.disabled ? 'pointer-events-none opacity-60' : '', /* `relative` for the outline and the label. The margin is the room the floated label needs above the diff --git a/frontend/src/components/shared/WPopupEdit.vue b/frontend/src/components/shared/WPopupEdit.vue deleted file mode 100644 index b01900fa7..000000000 --- a/frontend/src/components/shared/WPopupEdit.vue +++ /dev/null @@ -1,97 +0,0 @@ - - - diff --git a/frontend/src/components/shared/index.js b/frontend/src/components/shared/index.js index a47a14708..1b9aa0480 100644 --- a/frontend/src/components/shared/index.js +++ b/frontend/src/components/shared/index.js @@ -44,7 +44,6 @@ import WList from './WList.vue' import WMenu from './WMenu.vue' import WPage from './WPage.vue' import WPageContainer from './WPageContainer.vue' -import WPopupEdit from './WPopupEdit.vue' import WPageScroller from './WPageScroller.vue' import WPagination from './WPagination.vue' import WRadio from './WRadio.vue' @@ -105,7 +104,6 @@ export const sharedComponents = { WMenu, WPage, WPageContainer, - WPopupEdit, WPageScroller, WPagination, WRadio, diff --git a/frontend/src/pages/Index.vue b/frontend/src/pages/Index.vue index e558bfa0c..add0099d1 100644 --- a/frontend/src/pages/Index.vue +++ b/frontend/src/pages/Index.vue @@ -377,6 +377,13 @@ watch( and making them press a button first would only add a step. Keyed on the page rather than on the flag, so dismissing the prompt does not immediately reopen it -- the lock screen's own button is the way back in -- while walking to another protected page prompts again. + + Deliberately NOT `immediate`. This component is unmounted and remounted around any route outside the + page view (a search, the profile, the admin area), and the store it reads is global: an immediate run + fires against whatever page was on screen BEFORE that detour, so leaving a locked page for the search + screen and coming back to an unprotected one prompted for the earlier page's password. Every real + case still fires here, because `pageLoad` clears the flag as it starts and the reply sets it again -- + so a locked page always arrives as a change, mount or no mount. */ watch( () => (pageStore.isLocked ? pageStore.id : null), @@ -384,8 +391,7 @@ watch( if (lockedPageId) { promptUnlock() } - }, - { immediate: true } + } ) watch( diff --git a/frontend/src/renderers/markdown.js b/frontend/src/renderers/markdown.js index d2844d9ec..45f1656c1 100644 --- a/frontend/src/renderers/markdown.js +++ b/frontend/src/renderers/markdown.js @@ -107,7 +107,16 @@ export class MarkdownRenderer { } } }) - .use(mdMdc) + /* + MDC's INLINE component syntax is off, and deliberately: `:name` is how it writes one, which is + also how markdown writes an emoji, and MDC parses first. With it on, `:rocket:` came out as + `:` and no emoji shortcode in any page ever rendered — while this file goes to the + trouble of drawing them as twemoji SVGs, and the editor has a picker for them. + + Everything else MDC brings is untouched: block components (`::note`), inline props and inline + spans. Turning this back on means giving up emoji shortcodes again. + */ + .use(mdMdc, { syntax: { inlineComponent: false } }) .use(mdAttrs, { allowedAttributes: ['id', 'class', 'target'] }) diff --git a/frontend/src/stores/page.js b/frontend/src/stores/page.js index 55b053908..6e4aaf4fa 100644 --- a/frontend/src/stores/page.js +++ b/frontend/src/stores/page.js @@ -99,6 +99,15 @@ export const usePageStore = defineStore('page', { async pageLoad({ path, id, withContent = false }) { const editorStore = useEditorStore() const siteStore = useSiteStore() + /* + The lock belongs to the page being loaded, not to the one before it. + + Everything else in this store stays put until the reply arrives, deliberately -- blanking it + would flash an empty page on every navigation. `isLocked` cannot be treated that way: it is + read as "the page on screen is protected", and left standing it makes the NEXT page look + protected for as long as the request takes. + */ + this.isLocked = false try { const pageData = await API_CLIENT.get( `sites/${siteStore.id}/pages/${id ?? fastHash(normalizePath(path))}`,